diff --git a/Cargo.lock b/Cargo.lock index 1e3909244..6b4563ac0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1747,7 +1747,7 @@ checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" [[package]] name = "kludgine" version = "0.11.0" -source = "git+https://github.com/khonsulabs/kludgine#586c0df7ab0999c613424fc0d292502960676ade" +source = "git+https://github.com/khonsulabs/kludgine#1a96d3704dcc05caf9e68ccabb7cd7bf60fef361" dependencies = [ "ahash", "alot", diff --git a/examples/wrap.rs b/examples/wrap.rs index c0bb5c43d..09ed48969 100644 --- a/examples/wrap.rs +++ b/examples/wrap.rs @@ -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) diff --git a/src/styles.rs b/src/styles.rs index 917bec6f8..57b4ce9f4 100644 --- a/src/styles.rs +++ b/src/styles.rs @@ -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. @@ -2855,7 +2856,7 @@ impl VerticalAlign { Unit::Representation: CastFrom, { 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, } diff --git a/src/widgets/checkbox.rs b/src/widgets/checkbox.rs index 2d05983fd..f4d5bb6ec 100644 --- a/src/widgets/checkbox.rs +++ b/src/widgets/checkbox.rs @@ -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; @@ -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; @@ -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() @@ -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() } } @@ -194,13 +194,22 @@ impl CheckboxColors { impl IndicatorBehavior for CheckboxIndicator { type Colors = CheckboxColors; - fn size(&self, context: &mut GraphicsContext<'_, '_, '_, '_>) -> Size { - 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( @@ -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, + stroke_options: StrokeOptions, + corners: CornerRadii, + 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, + stroke_options: StrokeOptions, + corners: CornerRadii, + 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 { diff --git a/src/widgets/indicator.rs b/src/widgets/indicator.rs index 38d8788ed..2b7713413 100644 --- a/src/widgets/indicator.rs +++ b/src/widgets/indicator.rs @@ -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; + fn size(&self, context: &mut GraphicsContext<'_, '_, '_, '_>) -> WidgetLayout; } /// The current state of an [`Indicator`] widget. @@ -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()) @@ -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, diff --git a/src/widgets/label.rs b/src/widgets/label.rs index b9300baaa..aad50de44 100644 --- a/src/widgets/label.rs +++ b/src/widgets/label.rs @@ -138,7 +138,7 @@ where self.prepared_text(context, text_color, context.gfx.region().size.width, align); let y_offset = match valign { - VerticalAlign::Top => Px::ZERO, + VerticalAlign::Top | VerticalAlign::Baseline => Px::ZERO, VerticalAlign::Center => { (context.gfx.region().size.height - prepared_text.size.height) / 2 } @@ -168,7 +168,7 @@ where // bottom... WidgetLayout { size: available_space.fit_measured(prepared.size.into_unsigned().ceil()), - baseline: prepared.ascent.into(), + baseline: prepared.line_height.into(), } } diff --git a/src/widgets/radio.rs b/src/widgets/radio.rs index 56c69240b..b8be67fc2 100644 --- a/src/widgets/radio.rs +++ b/src/widgets/radio.rs @@ -1,7 +1,7 @@ //! A labeled widget with a circular indicator representing a value. use std::fmt::Debug; -use figures::units::{Px, UPx}; +use figures::units::Px; use figures::{Point, Rect, Round, ScreenScale, Size}; use kludgine::shapes::{Shape, StrokeOptions}; use kludgine::{Color, DrawableExt}; @@ -15,7 +15,9 @@ use crate::styles::components::{ }; use crate::styles::{ColorExt, Dimension}; 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; @@ -161,8 +163,17 @@ where { type Colors = RadioColors; - fn size(&self, context: &mut GraphicsContext<'_, '_, '_, '_>) -> Size { - Size::squared(context.get(&RadioSize).into_upx(context.gfx.scale()).ceil()) + fn size(&self, context: &mut GraphicsContext<'_, '_, '_, '_>) -> WidgetLayout { + let size = Size::squared(context.get(&RadioSize).into_upx(context.gfx.scale()).ceil()); + let outline_width = context + .get(&OutlineWidth) + .into_upx(context.gfx.scale()) + .ceil(); + + WidgetLayout { + size, + baseline: Baseline::from(size.height - outline_width * 2), + } } fn desired_colors( diff --git a/src/widgets/wrap.rs b/src/widgets/wrap.rs index ecb345757..a9d6690e0 100644 --- a/src/widgets/wrap.rs +++ b/src/widgets/wrap.rs @@ -102,6 +102,7 @@ impl Widget for Wrap { struct RowChild { index: usize, x: UPx, + baseline_offset: UPx, layout: WidgetLayout, } @@ -139,9 +140,6 @@ impl Widget for Wrap { let mut max_baseline = Baseline::NONE; while let Some(child) = self.mounted.children().get(index) { let child_layout = context.for_other(child).layout(child_constraints); - max_baseline = child_layout.baseline.max(max_baseline); - max_height = max_height.max(child_layout.size.height); - let child_x = if x.is_zero() { x } else { @@ -153,10 +151,14 @@ impl Widget for Wrap { break; } + max_baseline = child_layout.baseline.max(max_baseline); + max_height = max_height.max(child_layout.size.height); + row_children.push(RowChild { index, x: child_x, layout: child_layout, + baseline_offset: UPx::ZERO, }); x = after_child; @@ -171,6 +173,18 @@ impl Widget for Wrap { (UPx::ZERO, UPx::ZERO) }; + if let Some(max_baseline) = *max_baseline { + // If we have a baseline, we might need to add additional height + // due to aligning all of the baselines. + for child in &mut row_children { + if let Some(child_baseline) = *child.layout.baseline { + child.baseline_offset = max_baseline - child_baseline; + max_height = + max_height.max(child.layout.size.height + child.baseline_offset); + } + } + } + if y == 0 { first_baseline = max_baseline; } @@ -184,6 +198,7 @@ impl Widget for Wrap { let child_x = additional_x + child.x; let child_y = y + match vertical_align { VerticalAlign::Top => UPx::ZERO, + VerticalAlign::Baseline => child.baseline_offset, VerticalAlign::Center => (max_height - child.layout.size.height) / 2, VerticalAlign::Bottom => max_height - child.layout.size.height, };