Skip to content

Commit

Permalink
Wrap + Indicator baseline alignment
Browse files Browse the repository at this point in the history
  • Loading branch information
ecton committed Dec 1, 2024
1 parent feb3a35 commit 5b8007a
Show file tree
Hide file tree
Showing 8 changed files with 200 additions and 117 deletions.
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions examples/wrap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@ fn main() -> cushy::Result {
.new_radio(VerticalAlign::Top)
.labelled_by("Top"),
)
.and(
vertical_align
.new_radio(VerticalAlign::Baseline)
.labelled_by("Baseline"),
)
.and(
vertical_align
.new_radio(VerticalAlign::Center)
Expand Down
5 changes: 3 additions & 2 deletions src/styles.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2838,8 +2838,9 @@ impl RequireInvalidation for HorizontalAlign {
#[derive(Default, Debug, Clone, Copy, Eq, PartialEq)]
pub enum VerticalAlign {
/// Align towards the top.
#[default] // TODO this should be baseline, not top.
Top,
#[default]
Baseline,
/// Align towards the center/middle.
Center,
/// Align towards the bottom.
Expand All @@ -2855,7 +2856,7 @@ impl VerticalAlign {
Unit::Representation: CastFrom<i32>,
{
match self {
Self::Top => Unit::ZERO,
Self::Top | Self::Baseline => Unit::ZERO,
Self::Center => (available_space - measured) * Unit::from_unscaled(2.cast_into()),
Self::Bottom => available_space - measured,
}
Expand Down
211 changes: 128 additions & 83 deletions src/widgets/checkbox.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use std::error::Error;
use std::fmt::{Debug, Display};
use std::ops::Not;

use figures::units::{Lp, Px, UPx};
use figures::units::{Lp, Px};
use figures::{Point, Rect, Round, ScreenScale, Size, Zero};
use kludgine::shapes::{CornerRadii, PathBuilder, Shape, StrokeOptions};
use kludgine::Color;
Expand All @@ -21,7 +21,9 @@ use crate::styles::components::{
};
use crate::styles::{ColorExt, Dimension, VerticalAlign};
use crate::value::{Destination, Dynamic, DynamicReader, IntoDynamic, IntoValue, Source, Value};
use crate::widget::{MakeWidget, MakeWidgetWithTag, Widget, WidgetInstance, WidgetLayout};
use crate::widget::{
Baseline, MakeWidget, MakeWidgetWithTag, Widget, WidgetInstance, WidgetLayout,
};
use crate::widgets::button::ButtonKind;
use crate::ConstraintLimit;

Expand Down Expand Up @@ -82,11 +84,10 @@ impl MakeWidgetWithTag for Checkbox {
value: self.state.create_reader(),
};
let button_label = if let Some(label) = self.label {
// TODO Set this to Baseline.
adornment
.and(label)
.into_columns()
.with(&VerticalAlignment, VerticalAlign::Center)
.with(&VerticalAlignment, VerticalAlign::Baseline)
.make_widget()
} else {
adornment.make_widget()
Expand All @@ -112,8 +113,7 @@ impl MakeWidgetWithTag for Checkbox {
}
indicator
.make_with_tag(id)
// TODO Set this to Baseline.
.with(&VerticalAlignment, VerticalAlign::Center)
.with(&VerticalAlignment, VerticalAlign::Baseline)
.make_widget()
}
}
Expand Down Expand Up @@ -194,13 +194,22 @@ impl CheckboxColors {
impl IndicatorBehavior for CheckboxIndicator {
type Colors = CheckboxColors;

fn size(&self, context: &mut GraphicsContext<'_, '_, '_, '_>) -> Size<UPx> {
Size::squared(
fn size(&self, context: &mut GraphicsContext<'_, '_, '_, '_>) -> WidgetLayout {
let size = Size::squared(
context
.get(&CheckboxSize)
.into_upx(context.gfx.scale())
.ceil(),
)
);
let icon_inset = Lp::points(3).into_upx(context.gfx.scale()).ceil();
let icon_height = size.height - icon_inset * 2;

let checkmark_lowest_point = (icon_inset + icon_height * 3 / 4).round();

WidgetLayout {
size,
baseline: Baseline::from(checkmark_lowest_point),
}
}

fn desired_colors(
Expand Down Expand Up @@ -271,86 +280,122 @@ fn draw_checkbox(

match state {
state @ (CheckboxState::Checked | CheckboxState::Indeterminant) => {
if corners.is_zero() {
context
.gfx
.draw_shape(&Shape::filled_rect(checkbox_rect, selected_color));
if selected_color != colors.outline {
context.gfx.draw_shape(&Shape::stroked_rect(
checkbox_rect,
stroke_options.colored(colors.outline),
));
}
} else {
context.gfx.draw_shape(&Shape::filled_round_rect(
checkbox_rect,
corners,
selected_color,
));
if selected_color != colors.outline {
context.gfx.draw_shape(&Shape::stroked_round_rect(
checkbox_rect,
corners,
stroke_options.colored(colors.outline),
));
}
}
let icon_area = checkbox_rect.inset(Lp::points(3).into_px(context.gfx.scale()));

let center = icon_area.origin + icon_area.size / 2;
let mut double_stroke = stroke_options;
double_stroke.line_width *= 2;
if matches!(state, CheckboxState::Checked) {
context.gfx.draw_shape(
&PathBuilder::new(Point::new(icon_area.origin.x, center.y))
.line_to(Point::new(
icon_area.origin.x + icon_area.size.width / 4,
icon_area.origin.y + icon_area.size.height * 3 / 4,
))
.line_to(Point::new(
icon_area.origin.x + icon_area.size.width,
icon_area.origin.y,
))
.build()
.stroke(double_stroke.colored(colors.foreground)),
);
} else {
context.gfx.draw_shape(
&PathBuilder::new(Point::new(icon_area.origin.x, center.y))
.line_to(Point::new(
icon_area.origin.x + icon_area.size.width,
center.y,
))
.build()
.stroke(double_stroke.colored(colors.foreground)),
);
}
draw_filled_checkbox(
state,
colors,
selected_color,
checkbox_rect,
stroke_options,
corners,
context,
);
}
CheckboxState::Unchecked => {
if corners.is_zero() {
context
.gfx
.draw_shape(&Shape::filled_rect(checkbox_rect, colors.fill));
context.gfx.draw_shape(&Shape::stroked_rect(
checkbox_rect,
stroke_options.colored(colors.outline),
));
} else {
context.gfx.draw_shape(&Shape::filled_round_rect(
checkbox_rect,
corners,
colors.fill,
));
context.gfx.draw_shape(&Shape::stroked_round_rect(
checkbox_rect,
corners,
stroke_options.colored(colors.outline),
));
}
draw_empty_checkbox(colors, checkbox_rect, stroke_options, corners, context);
}
}
}

fn draw_empty_checkbox(
colors: &CheckboxColors,
checkbox_rect: Rect<Px>,
stroke_options: StrokeOptions<Px>,
corners: CornerRadii<Px>,
context: &mut GraphicsContext<'_, '_, '_, '_>,
) {
if corners.is_zero() {
context
.gfx
.draw_shape(&Shape::filled_rect(checkbox_rect, colors.fill));
context.gfx.draw_shape(&Shape::stroked_rect(
checkbox_rect,
stroke_options.colored(colors.outline),
));
} else {
context.gfx.draw_shape(&Shape::filled_round_rect(
checkbox_rect,
corners,
colors.fill,
));
context.gfx.draw_shape(&Shape::stroked_round_rect(
checkbox_rect,
corners,
stroke_options.colored(colors.outline),
));
}
}

fn draw_filled_checkbox(
state: CheckboxState,
colors: &CheckboxColors,
selected_color: Color,
checkbox_rect: Rect<Px>,
stroke_options: StrokeOptions<Px>,
corners: CornerRadii<Px>,
context: &mut GraphicsContext<'_, '_, '_, '_>,
) {
if corners.is_zero() {
context
.gfx
.draw_shape(&Shape::filled_rect(checkbox_rect, selected_color));
if selected_color != colors.outline {
context.gfx.draw_shape(&Shape::stroked_rect(
checkbox_rect,
stroke_options.colored(colors.outline),
));
}
} else {
context.gfx.draw_shape(&Shape::filled_round_rect(
checkbox_rect,
corners,
selected_color,
));
if selected_color != colors.outline {
context.gfx.draw_shape(&Shape::stroked_round_rect(
checkbox_rect,
corners,
stroke_options.colored(colors.outline),
));
}
}
let icon_area = checkbox_rect.inset(Lp::points(3).into_px(context.gfx.scale()).ceil());

let center = icon_area.origin + icon_area.size / 2;
let mut double_stroke = stroke_options;
double_stroke.line_width *= 2;
if matches!(state, CheckboxState::Checked) {
context.gfx.draw_shape(
&PathBuilder::new(Point::new(icon_area.origin.x, center.y).round())
.line_to(
Point::new(
icon_area.origin.x + icon_area.size.width / 4,
icon_area.origin.y + icon_area.size.height * 3 / 4,
)
.round(),
)
.line_to(
Point::new(
icon_area.origin.x + icon_area.size.width,
icon_area.origin.y,
)
.round(),
)
.build()
.stroke(double_stroke.colored(colors.foreground)),
);
} else {
context.gfx.draw_shape(
&PathBuilder::new(Point::new(icon_area.origin.x, center.y))
.line_to(Point::new(
icon_area.origin.x + icon_area.size.width,
center.y,
))
.build()
.stroke(double_stroke.colored(colors.foreground)),
);
}
}

/// The state/value of a [`Checkbox`].
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum CheckboxState {
Expand Down
50 changes: 28 additions & 22 deletions src/widgets/indicator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ pub trait IndicatorBehavior: Send + Debug + 'static {
context: &mut GraphicsContext<'_, '_, '_, '_>,
);
/// Returns the size of this indicator.
fn size(&self, context: &mut GraphicsContext<'_, '_, '_, '_>) -> Size<UPx>;
fn size(&self, context: &mut GraphicsContext<'_, '_, '_, '_>) -> WidgetLayout;
}

/// The current state of an [`Indicator`] widget.
Expand Down Expand Up @@ -252,10 +252,11 @@ where
context: &mut LayoutContext<'_, '_, '_, '_>,
) -> WidgetLayout {
let window_local = self.per_window.entry(context).or_default();
window_local.size = self.behavior.size(context).ceil();
let indicator_layout = self.behavior.size(context);
window_local.size = indicator_layout.size.ceil();
window_local.checkbox_region.size = window_local.size.into_signed();

let (full_size, baseline) = if let Some(label) = &mut self.label {
let (mut full_size, baseline) = if let Some(label) = &mut self.label {
let padding = context
.get(&IntrinsicPadding)
.into_px(context.gfx.scale())
Expand All @@ -267,45 +268,50 @@ where
);
let mounted = label.mounted(context);
let label_layout = context.for_other(&mounted).layout(remaining_space);
let indicator_baseline = indicator_layout
.baseline
.unwrap_or(indicator_layout.size.height);
let (offset, height) = match *label_layout.baseline {
Some(baseline) if baseline < window_local.size.height => (
window_local.size.height - baseline,
Some(baseline) if baseline < indicator_baseline => (
indicator_baseline.saturating_sub(baseline),
window_local.size.height,
),
_ => (UPx::ZERO, label_layout.size.height),
};

let height = available_space
.height
.fit_measured(height)
.max(window_local.size.height)
.into_signed();

window_local.label_region = Rect::new(
Point::new(
x_offset,
(height - label_layout.size.height.into_signed()) / 2 + offset.into_signed(),
),
Point::new(x_offset, offset.into_signed()),
label_layout.size.into_signed(),
);
context.set_child_layout(&mounted, window_local.label_region);

(
Size::new(label_layout.size.width.into_signed() + x_offset, height).into_unsigned(),
Size::new(label_layout.size.width + x_offset.into_unsigned(), height),
label_layout.baseline.map(|baseline| baseline + offset),
)
} else {
(window_local.size.into_unsigned(), Baseline::NONE)
};

if let Some(baseline) = *baseline {
window_local.checkbox_region.origin.y =
(baseline - window_local.size.height).into_signed();
} else {
window_local.checkbox_region.origin.y =
(full_size.height.into_signed() - window_local.checkbox_region.size.height) / 2;
match (*baseline, *indicator_layout.baseline) {
(Some(label_baseline), Some(indicator_baseline)) => {
window_local.checkbox_region.origin.y =
(label_baseline.saturating_sub(indicator_baseline)).into_signed();
}
(Some(label_baseline), None) => {
window_local.checkbox_region.origin.y =
(label_baseline.saturating_sub(window_local.size.height)).into_signed();
}
_ => {
window_local.checkbox_region.origin.y =
(full_size.height.into_signed() - window_local.checkbox_region.size.height) / 2;
}
}

full_size.height = full_size
.height
.max(window_local.checkbox_region.extent().y.into_unsigned());

WidgetLayout {
size: full_size,
baseline,
Expand Down
Loading

0 comments on commit 5b8007a

Please sign in to comment.