From feb3a35b6d795733a450e621dc5c0e70da6bc3cf Mon Sep 17 00:00:00 2001 From: Jonathan Johnson Date: Sat, 30 Nov 2024 09:23:08 -0800 Subject: [PATCH 1/2] Added WidgetLayout + Baseline Refs #88 This set of changes starts working towards supporting baseline alignment. The Grid widget currently does not honor vertical align in any way, but it in theory propagates the first row's baseline information -- which will need to be adjusted if Baseline vertical alignment is used. Next step is to actually add Baseline and try to make it work. The set of changes thus far was just so massive and I finally resolved all the errors from this refactoring. --- examples/custom-widgets.rs | 9 +- .../examples/composition-widget.rs | 17 +- .../examples/composition-wrapperwidget.rs | 11 +- src/context.rs | 8 +- src/styles.rs | 9 +- src/tree.rs | 17 +- src/widget.rs | 191 ++++++++++++++++-- src/widgets/align.rs | 11 +- src/widgets/button.rs | 15 +- src/widgets/canvas.rs | 7 +- src/widgets/checkbox.rs | 6 +- src/widgets/collapse.rs | 17 +- src/widgets/color.rs | 8 +- src/widgets/container.rs | 15 +- src/widgets/custom.rs | 34 ++-- src/widgets/delimiter.rs | 11 +- src/widgets/disclose.rs | 52 +++-- src/widgets/expand.rs | 21 +- src/widgets/grid.rs | 110 ++++++---- src/widgets/image.rs | 5 +- src/widgets/indicator.rs | 63 ++++-- src/widgets/input.rs | 12 +- src/widgets/label.rs | 16 +- src/widgets/layers.rs | 40 ++-- src/widgets/menu.rs | 18 +- src/widgets/pile.rs | 19 +- src/widgets/progress.rs | 8 +- src/widgets/radio.rs | 6 +- src/widgets/resize.rs | 21 +- src/widgets/scroll.rs | 20 +- src/widgets/slider.rs | 5 +- src/widgets/space.rs | 7 +- src/widgets/stack.rs | 6 +- src/widgets/tilemap.rs | 8 +- src/widgets/virtual_list.rs | 15 +- src/widgets/wrap.rs | 75 +++---- src/window.rs | 7 +- 37 files changed, 616 insertions(+), 304 deletions(-) diff --git a/examples/custom-widgets.rs b/examples/custom-widgets.rs index 4aa5c31a9..bce463f59 100644 --- a/examples/custom-widgets.rs +++ b/examples/custom-widgets.rs @@ -1,11 +1,13 @@ //! This example shows two approaches to writing custom widgets: implementing //! traits or using the [`Custom`] widget with callbacks. -use cushy::figures::units::{Lp, UPx}; +use cushy::figures::units::Lp; use cushy::figures::{ScreenScale, Size}; use cushy::kludgine::Color; use cushy::value::{Destination, Dynamic, Source}; -use cushy::widget::{MakeWidget, MakeWidgetWithTag, Widget, WidgetInstance, WidgetTag, HANDLED}; +use cushy::widget::{ + MakeWidget, MakeWidgetWithTag, Widget, WidgetInstance, WidgetLayout, WidgetTag, HANDLED, +}; use cushy::widgets::Custom; use cushy::window::DeviceId; use cushy::Run; @@ -95,11 +97,12 @@ impl Widget for Toggle { &mut self, available_space: Size, context: &mut cushy::context::LayoutContext<'_, '_, '_, '_>, - ) -> Size { + ) -> WidgetLayout { Size::new( available_space.width.min(), Lp::inches(1).into_upx(context.gfx.scale()), ) + .into() } fn hit_test( diff --git a/guide/guide-examples/examples/composition-widget.rs b/guide/guide-examples/examples/composition-widget.rs index 80fcb7363..5aa9888c0 100644 --- a/guide/guide-examples/examples/composition-widget.rs +++ b/guide/guide-examples/examples/composition-widget.rs @@ -4,7 +4,7 @@ use cushy::figures::{IntoSigned, IntoUnsigned, Point, Rect, ScreenScale, Size, Z use cushy::kludgine::text::{MeasuredText, TextOrigin}; use cushy::styles::components::IntrinsicPadding; use cushy::value::{Dynamic, IntoValue, Value}; -use cushy::widget::Widget; +use cushy::widget::{Widget, WidgetLayout}; use cushy::widgets::input::InputValue; use cushy::ConstraintLimit; @@ -51,34 +51,37 @@ fn composition_widget() -> impl cushy::widget::MakeWidget { &mut self, available_space: Size, context: &mut LayoutContext<'_, '_, '_, '_>, - ) -> Size { + ) -> WidgetLayout { let label_and_padding = self.label_and_padding_size(context); let field_available_space = Size::new( available_space.width, available_space.height - label_and_padding.height, ); let field = self.field.mounted(context); - let field_size = context.for_other(&field).layout(field_available_space); + let field_layout = context.for_other(&field).layout(field_available_space); let full_size = Size::new( available_space .width .min() - .max(field_size.width) + .max(field_layout.size.width) .max(label_and_padding.width), - field_size.height + label_and_padding.height, + field_layout.size.height + label_and_padding.height, ); context.set_child_layout( &field, Rect::new( Point::new(UPx::ZERO, label_and_padding.height), - Size::new(full_size.width, field_size.height), + Size::new(full_size.width, field_layout.size.height), ) .into_signed(), ); - full_size + WidgetLayout { + size: full_size, + baseline: field_layout.baseline, + } } // ANCHOR_END: widget-a diff --git a/guide/guide-examples/examples/composition-wrapperwidget.rs b/guide/guide-examples/examples/composition-wrapperwidget.rs index 52d2b6962..9b37e4207 100644 --- a/guide/guide-examples/examples/composition-wrapperwidget.rs +++ b/guide/guide-examples/examples/composition-wrapperwidget.rs @@ -4,7 +4,7 @@ use cushy::figures::{IntoSigned, IntoUnsigned, Point, Rect, ScreenScale, Size, Z use cushy::kludgine::text::{MeasuredText, TextOrigin}; use cushy::styles::components::IntrinsicPadding; use cushy::value::{Dynamic, IntoValue, Value}; -use cushy::widget::{WrappedLayout, WrapperWidget}; +use cushy::widget::{WidgetLayout, WrappedLayout, WrapperWidget}; use cushy::widgets::input::InputValue; use cushy::ConstraintLimit; @@ -75,7 +75,7 @@ fn composition_wrapperwidget() -> impl cushy::widget::MakeWidget { // ANCHOR: wrapperwidget-c fn position_child( &mut self, - child_size: Size, + child_layout: WidgetLayout, available_space: Size, context: &mut LayoutContext<'_, '_, '_, '_>, ) -> WrappedLayout { @@ -84,17 +84,18 @@ fn composition_wrapperwidget() -> impl cushy::widget::MakeWidget { available_space .width .min() + .max(child_layout.size.width) .into_signed() - .max(child_size.width) .max(label_and_padding.width), - child_size.height + label_and_padding.height, + child_layout.size.height.into_signed() + label_and_padding.height, ); WrappedLayout { child: Rect::new( Point::new(Px::ZERO, label_and_padding.height), - Size::new(full_size.width, child_size.height), + Size::new(full_size.width, child_layout.size.height.into_signed()), ), size: full_size.into_unsigned(), + baseline: child_layout.baseline, } } diff --git a/src/context.rs b/src/context.rs index 0c3b6ece8..ced6e2fd1 100644 --- a/src/context.rs +++ b/src/context.rs @@ -20,7 +20,9 @@ use crate::styles::components::{ use crate::styles::{ComponentDefinition, FontFamilyList, Styles, Theme, ThemePair}; use crate::tree::Tree; use crate::value::{IntoValue, Source, Value}; -use crate::widget::{EventHandling, MountedWidget, RootBehavior, WidgetId, WidgetInstance}; +use crate::widget::{ + EventHandling, MountedWidget, RootBehavior, WidgetId, WidgetInstance, WidgetLayout, +}; use crate::window::{ CursorState, DeviceId, KeyEvent, PlatformWindow, ThemeMode, WidgetCursorState, }; @@ -809,7 +811,7 @@ impl<'context, 'clip, 'gfx, 'pass> LayoutContext<'context, 'clip, 'gfx, 'pass> { /// Invokes [`Widget::layout()`](crate::widget::Widget::layout) on this /// context's widget and returns the result. - pub fn layout(&mut self, available_space: Size) -> Size { + pub fn layout(&mut self, available_space: Size) -> WidgetLayout { if self.persist_layout { if let Some(cached) = self.graphics.current_node.begin_layout(available_space) { return cached; @@ -822,7 +824,7 @@ impl<'context, 'clip, 'gfx, 'pass> LayoutContext<'context, 'clip, 'gfx, 'pass> { .lock() .as_widget() .layout(available_space, self) - .map(Round::ceil); + .ceil(); if self.persist_layout { self.graphics .current_node diff --git a/src/styles.rs b/src/styles.rs index d24daba49..917bec6f8 100644 --- a/src/styles.rs +++ b/src/styles.rs @@ -2797,12 +2797,15 @@ impl HorizontalAlign { /// within `available_space`. pub fn alignment_offset(self, measured: Unit, available_space: Unit) -> Unit where - Unit: Sub + Mul + UnscaledUnit + Zero, - Unit::Representation: CastFrom, + Unit: Sub + Mul + UnscaledUnit + Zero + Round, + Unit::Representation: Div + From, { match self { Self::Left => Unit::ZERO, - Self::Center => (available_space - measured) * Unit::from_unscaled(2.cast_into()), + Self::Center => Unit::from_unscaled( + (available_space - measured).into_unscaled() / ::from(2), + ) + .round(), Self::Right => available_space - measured, } } diff --git a/src/tree.rs b/src/tree.rs index a632ef1c4..97e0613ee 100644 --- a/src/tree.rs +++ b/src/tree.rs @@ -3,13 +3,13 @@ use std::sync::{Arc, Weak}; use ahash::AHashMap; use alot::{LotId, Lots}; -use figures::units::{Px, UPx}; +use figures::units::Px; use figures::{Point, Rect, Size}; use parking_lot::Mutex; use crate::styles::{Styles, ThemePair, VisualOrder}; use crate::value::Value; -use crate::widget::{MountedWidget, WidgetId, WidgetInstance}; +use crate::widget::{MountedWidget, WidgetId, WidgetInstance, WidgetLayout}; use crate::window::{ThemeMode, WindowHandle}; use crate::ConstraintLimit; @@ -121,7 +121,7 @@ impl Tree { &self, parent: LotId, constraints: Size, - ) -> Option> { + ) -> Option { let mut data = self.data.lock(); let node = &mut data.nodes[parent]; @@ -129,7 +129,7 @@ impl Tree { if constraints.width.max() <= cached_layout.constraints.width.max() && constraints.height.max() <= cached_layout.constraints.height.max() { - return Some(cached_layout.size); + return Some(cached_layout.layout); } node.last_layout_query = None; @@ -147,10 +147,13 @@ impl Tree { &self, id: LotId, constraints: Size, - size: Size, + size: WidgetLayout, ) { let mut data = self.data.lock(); - data.nodes[id].last_layout_query = Some(CachedLayoutQuery { constraints, size }); + data.nodes[id].last_layout_query = Some(CachedLayoutQuery { + constraints, + layout: size, + }); } pub(crate) fn visually_ordered_children( @@ -655,7 +658,7 @@ impl Node { struct CachedLayoutQuery { constraints: Size, - size: Size, + layout: WidgetLayout, } #[derive(Clone, Debug)] diff --git a/src/widget.rs b/src/widget.rs index 5d7e6b56f..657082b2e 100644 --- a/src/widget.rs +++ b/src/widget.rs @@ -2,6 +2,7 @@ use std::any::Any; use std::clone::Clone; +use std::cmp::Ordering; use std::fmt::{self, Debug}; use std::ops::{ControlFlow, Deref, DerefMut}; use std::sync::atomic::{self, AtomicU64}; @@ -10,7 +11,7 @@ use std::{slice, vec}; use alot::LotId; use figures::units::{Px, UPx}; -use figures::{IntoSigned, IntoUnsigned, Point, Rect, Size, Zero}; +use figures::{IntoSigned, IntoUnsigned, Point, Rect, Round, Size, Zero}; use intentional::Assert; use kludgine::app::winit::event::{Ime, MouseButton, MouseScrollDelta, TouchPhase}; use kludgine::app::winit::keyboard::ModifiersState; @@ -296,8 +297,8 @@ pub trait Widget: Send + Debug + 'static { &mut self, available_space: Size, context: &mut LayoutContext<'_, '_, '_, '_>, - ) -> Size { - available_space.map(ConstraintLimit::min) + ) -> WidgetLayout { + available_space.map(ConstraintLimit::min).into() } /// The widget has been mounted into a parent widget. @@ -505,6 +506,148 @@ where } // ANCHOR_END: run +#[derive(Default, Debug, Clone, Copy, Eq, PartialEq)] +pub struct Baseline(pub Option); + +impl Baseline { + pub const NONE: Self = Self(None); + + #[must_use] + pub fn map(self, map: impl FnOnce(UPx) -> UPx) -> Self { + Self(self.0.map(map)) + } +} + +impl Round for Baseline { + fn ceil(self) -> Self { + Self(self.0.map(Round::ceil)) + } + + fn floor(self) -> Self { + Self(self.0.map(Round::floor)) + } + + fn round(self) -> Self { + Self(self.0.map(Round::round)) + } +} + +impl Deref for Baseline { + type Target = Option; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl From for Baseline { + fn from(value: Px) -> Self { + Self::from(value.into_unsigned()) + } +} + +impl From> for Baseline { + fn from(value: Option) -> Self { + Self::from(value.map(Px::into_unsigned)) + } +} + +impl From for Baseline { + fn from(value: UPx) -> Self { + Self::from(Some(value)) + } +} + +impl From> for Baseline { + fn from(value: Option) -> Self { + Self(value) + } +} + +impl Ord for Baseline { + fn cmp(&self, other: &Self) -> Ordering { + self.0.cmp(&other.0) + } + + fn min(self, other: Self) -> Self + where + Self: Sized, + { + match (self.0, other.0) { + (Some(lhs), Some(rhs)) => Self(Some(lhs.min(rhs))), + (Some(value), _) | (_, Some(value)) => Self(Some(value)), + (None, None) => Self(None), + } + } +} + +impl PartialOrd for Baseline { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +#[derive(Clone, Copy, Eq, PartialEq, Debug)] +pub struct WidgetLayout { + pub size: Size, + pub baseline: Baseline, +} + +impl Zero for WidgetLayout { + const ZERO: Self = Self { + size: Size::ZERO, + baseline: Baseline::NONE, + }; + + fn is_zero(&self) -> bool { + self.size.is_zero() && self.baseline.map_or(true, |baseline| baseline.is_zero()) + } +} + +impl From> for WidgetLayout +where + T: IntoUnsigned, +{ + fn from(value: Size) -> Self { + Self { + size: value.map(IntoUnsigned::into_unsigned), + baseline: Baseline::NONE, + } + } +} + +impl From for WidgetLayout { + fn from(layout: WrappedLayout) -> Self { + Self { + size: layout.size, + baseline: layout.baseline, + } + } +} + +impl Round for WidgetLayout { + fn round(self) -> Self { + Self { + size: self.size.round(), + baseline: self.baseline.round(), + } + } + + fn ceil(self) -> Self { + Self { + size: self.size.ceil(), + baseline: self.baseline.ceil(), + } + } + + fn floor(self) -> Self { + Self { + size: self.size.floor(), + baseline: self.baseline.floor(), + } + } +} + /// A behavior that should be applied to a root widget. #[derive(Debug, Clone, Copy)] pub enum RootBehavior { @@ -529,17 +672,23 @@ pub struct WrappedLayout { pub child: Rect, /// The size the wrapper widget should report as. pub size: Size, + /// The offset from the top of this widget to the baseline of the first line + /// of text contained in this widget. + /// + /// If the widget has no text, this value should be set to `None`. + pub baseline: Baseline, } impl WrappedLayout { /// Returns a layout that positions `size` within `available_space` while /// respecting [`HOrizontalAlignment`] and [`VerticalAlignment`]. pub fn aligned( - size: Size, + layout: impl Into, available_space: Size, context: &mut LayoutContext<'_, '_, '_, '_>, ) -> Self { - let child_size = size.into_signed(); + let layout = layout.into(); + let child_size = layout.size.into_signed(); let fill_width = available_space .width .fit_measured(child_size.width) @@ -567,6 +716,7 @@ impl WrappedLayout { WrappedLayout { child: Rect::new(Point::new(x, y), Size::new(width, height)), size: Size::new(padded_width, padded_height).into_unsigned(), + baseline: layout.baseline.map(|baseline| baseline + y.into_unsigned()), } } } @@ -576,6 +726,7 @@ impl From> for WrappedLayout { WrappedLayout { child: size.into(), size: size.into_unsigned(), + baseline: Baseline::NONE, } } } @@ -585,6 +736,17 @@ impl From> for WrappedLayout { WrappedLayout { child: size.into_signed().into(), size, + baseline: Baseline::NONE, + } + } +} + +impl From for WrappedLayout { + fn from(layout: WidgetLayout) -> Self { + Self { + child: layout.size.into_signed().into(), + size: layout.size, + baseline: layout.baseline, } } } @@ -643,10 +805,7 @@ pub trait WrapperWidget: Debug + Send + 'static { ) -> WrappedLayout { let adjusted_space = self.adjust_child_constraints(available_space, context); let child = self.child_mut().mounted(&mut context.as_event_context()); - let size = context - .for_other(&child) - .layout(adjusted_space) - .into_signed(); + let size = context.for_other(&child).layout(adjusted_space); self.position_child(size, available_space, context) } @@ -667,14 +826,14 @@ pub trait WrapperWidget: Debug + Send + 'static { #[must_use] fn position_child( &mut self, - size: Size, + layout: WidgetLayout, available_space: Size, context: &mut LayoutContext<'_, '_, '_, '_>, ) -> WrappedLayout { if self.align_child() { - WrappedLayout::aligned(size.into_unsigned(), available_space, context) + WrappedLayout::aligned(layout, available_space, context) } else { - WrappedLayout::from(size) + WrappedLayout::from(layout) } } @@ -862,11 +1021,11 @@ where &mut self, available_space: Size, context: &mut LayoutContext<'_, '_, '_, '_>, - ) -> Size { + ) -> WidgetLayout { let layout = self.layout_child(available_space, context); let child = self.child_mut().mounted(&mut context.as_event_context()); context.set_child_layout(&child, layout.child); - layout.size + layout.into() } fn mounted(&mut self, context: &mut EventContext<'_>) { @@ -2100,11 +2259,11 @@ impl MountedWidget { self.tree().overridden_theme(self.node_id) } - pub(crate) fn begin_layout(&self, constraints: Size) -> Option> { + pub(crate) fn begin_layout(&self, constraints: Size) -> Option { self.tree().begin_layout(self.node_id, constraints) } - pub(crate) fn persist_layout(&self, constraints: Size, size: Size) { + pub(crate) fn persist_layout(&self, constraints: Size, size: WidgetLayout) { self.tree().persist_layout(self.node_id, constraints, size); } diff --git a/src/widgets/align.rs b/src/widgets/align.rs index 28e2cad59..8dc8190e0 100644 --- a/src/widgets/align.rs +++ b/src/widgets/align.rs @@ -6,7 +6,7 @@ use figures::{Fraction, IntoSigned, Point, Rect, ScreenScale, Size, Zero}; use crate::context::{AsEventContext, EventContext, LayoutContext}; use crate::styles::{Edges, FlexibleDimension}; use crate::value::{IntoValue, Value}; -use crate::widget::{MakeWidget, RootBehavior, WidgetRef, WrappedLayout, WrapperWidget}; +use crate::widget::{Baseline, MakeWidget, RootBehavior, WidgetRef, WrappedLayout, WrapperWidget}; use crate::ConstraintLimit; /// A widget aligns its contents to its container's boundaries. @@ -99,10 +99,10 @@ impl Align { ); let child = self.child.mounted(&mut context.as_event_context()); - let content_size = context.for_other(&child).layout(content_available); + let layout = context.for_other(&child).layout(content_available); - let (left, right, width) = horizontal.measure(available_space.width, content_size.width); - let (top, bottom, height) = vertical.measure(available_space.height, content_size.height); + let (left, right, width) = horizontal.measure(available_space.width, layout.size.width); + let (top, bottom, height) = vertical.measure(available_space.height, layout.size.height); Layout { margin: Edges { @@ -112,6 +112,7 @@ impl Align { bottom, }, content: Size::new(width, height), + baseline: layout.baseline, } } } @@ -196,6 +197,7 @@ impl WrapperWidget for Align { layout.content.into_signed(), ), size: layout.content + layout.margin.size(), + baseline: layout.baseline.map(|baseline| baseline + layout.margin.top), } } } @@ -210,4 +212,5 @@ impl AsMut for Align { struct Layout { margin: Edges, content: Size, + baseline: Baseline, } diff --git a/src/widgets/button.rs b/src/widgets/button.rs index 6490085b0..5d3a684df 100644 --- a/src/widgets/button.rs +++ b/src/widgets/button.rs @@ -1,7 +1,7 @@ //! A clickable, labeled button use std::time::{Duration, Instant}; -use figures::units::{Px, UPx}; +use figures::units::Px; use figures::{IntoSigned, Point, Rect, Round, ScreenScale, Size, Zero}; use kludgine::app::winit::event::{Modifiers, MouseButton}; use kludgine::app::winit::window::CursorIcon; @@ -22,7 +22,7 @@ use crate::styles::components::{ use crate::styles::{ColorExt, Styles}; use crate::value::{Destination, Dynamic, IntoValue, Source, Value}; use crate::widget::{ - Callback, EventHandling, MakeWidget, SharedCallback, Widget, WidgetRef, HANDLED, + Callback, EventHandling, MakeWidget, SharedCallback, Widget, WidgetLayout, WidgetRef, HANDLED, }; use crate::window::{DeviceId, WindowLocal}; use crate::FitMeasuredSize; @@ -507,7 +507,7 @@ impl Widget for Button { &mut self, available_space: Size, context: &mut LayoutContext<'_, '_, '_, '_>, - ) -> Size { + ) -> WidgetLayout { let outline_width = context .get(&OutlineWidth) .into_upx(context.gfx.scale()) @@ -521,13 +521,16 @@ impl Widget for Button { let double_padding = padding * 2; let mounted = self.content.mounted(context); let available_space = available_space.map(|space| space - double_padding); - let size = context.for_other(&mounted).layout(available_space); - let size = available_space.fit_measured(size); + let layout = context.for_other(&mounted).layout(available_space); + let size = available_space.fit_measured(layout.size); context.set_child_layout( &mounted, Rect::new(Point::squared(padding), size).into_signed(), ); - size + double_padding + WidgetLayout { + size: size + double_padding, + baseline: layout.baseline.map(|baseline| baseline + padding), + } } fn unhover(&mut self, context: &mut EventContext<'_>) { diff --git a/src/widgets/canvas.rs b/src/widgets/canvas.rs index 02e0c1dcf..059e52fa5 100644 --- a/src/widgets/canvas.rs +++ b/src/widgets/canvas.rs @@ -1,11 +1,10 @@ use std::fmt::Debug; -use figures::units::UPx; use figures::Size; use crate::context::{GraphicsContext, LayoutContext}; use crate::value::Dynamic; -use crate::widget::Widget; +use crate::widget::{Widget, WidgetLayout}; use crate::{ConstraintLimit, Tick}; /// A 2d drawable surface. @@ -52,8 +51,8 @@ impl Widget for Canvas { &mut self, available_space: Size, _context: &mut LayoutContext<'_, '_, '_, '_>, - ) -> Size { - available_space.map(ConstraintLimit::max) + ) -> WidgetLayout { + available_space.map(ConstraintLimit::max).into() } } diff --git a/src/widgets/checkbox.rs b/src/widgets/checkbox.rs index d037ccaee..2d05983fd 100644 --- a/src/widgets/checkbox.rs +++ b/src/widgets/checkbox.rs @@ -21,7 +21,7 @@ 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}; +use crate::widget::{MakeWidget, MakeWidgetWithTag, Widget, WidgetInstance, WidgetLayout}; use crate::widgets::button::ButtonKind; use crate::ConstraintLimit; @@ -483,12 +483,12 @@ impl Widget for CheckboxOrnament { &mut self, _available_space: Size, context: &mut LayoutContext<'_, '_, '_, '_>, - ) -> Size { + ) -> WidgetLayout { let checkbox_size = context .get(&CheckboxSize) .into_upx(context.gfx.scale()) .ceil(); - Size::squared(checkbox_size) + Size::squared(checkbox_size).into() } } diff --git a/src/widgets/collapse.rs b/src/widgets/collapse.rs index d11d6102b..6f5207650 100644 --- a/src/widgets/collapse.rs +++ b/src/widgets/collapse.rs @@ -1,13 +1,15 @@ use std::time::Duration; use figures::units::Px; -use figures::{Size, Zero}; +use figures::{IntoSigned, IntoUnsigned, Size, Zero}; use crate::animation::{AnimationHandle, AnimationTarget, Spawn}; use crate::context::LayoutContext; use crate::styles::components::{EasingIn, EasingOut}; use crate::value::{Dynamic, Generation, IntoDynamic, Source}; -use crate::widget::{MakeWidget, WidgetInstance, WidgetRef, WrappedLayout, WrapperWidget}; +use crate::widget::{ + MakeWidget, WidgetInstance, WidgetLayout, WidgetRef, WrappedLayout, WrapperWidget, +}; use crate::ConstraintLimit; /// A widget that collapses/hides its contents based on a [`Dynamic`]. @@ -106,12 +108,13 @@ impl WrapperWidget for Collapse { fn position_child( &mut self, - size: Size, + layout: WidgetLayout, _available_space: Size, context: &mut LayoutContext<'_, '_, '_, '_>, ) -> WrappedLayout { + let size = layout.size.into_signed(); let clip_size = self.size.get_tracking_invalidate(context); - if self.vertical { + let size = if self.vertical { let height = self.note_child_size(size.height, clip_size, context); Size::new(size.width, height) @@ -119,8 +122,12 @@ impl WrapperWidget for Collapse { let width = self.note_child_size(size.width, clip_size, context); Size::new(width, size.height) + }; + WrappedLayout { + child: size.into(), + size: size.into_unsigned(), + baseline: layout.baseline, } - .into() } fn summarize(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { diff --git a/src/widgets/color.rs b/src/widgets/color.rs index 96dd89ba6..1df08fb4a 100644 --- a/src/widgets/color.rs +++ b/src/widgets/color.rs @@ -1,7 +1,7 @@ //! Widgets for selecting colors. use std::ops::Range; -use figures::units::{Lp, Px, UPx}; +use figures::units::{Lp, Px}; use figures::{FloatConversion, Point, Rect, Round, ScreenScale, Size, Zero}; use intentional::Cast; use kludgine::app::winit::event::MouseButton; @@ -19,7 +19,8 @@ use crate::value::{ Source, Value, }; use crate::widget::{ - EventHandling, MakeWidget, MakeWidgetWithTag, Widget, WidgetInstance, WidgetTag, HANDLED, + EventHandling, MakeWidget, MakeWidgetWithTag, Widget, WidgetInstance, WidgetLayout, WidgetTag, + HANDLED, }; use crate::window::DeviceId; use crate::ConstraintLimit; @@ -468,7 +469,7 @@ where &mut self, available_space: Size, context: &mut LayoutContext<'_, '_, '_, '_>, - ) -> Size { + ) -> WidgetLayout { let ideal_height = Lp::points(24).into_upx(context.gfx.scale()).ceil(); Size::new( match available_space.width { @@ -480,6 +481,7 @@ where ConstraintLimit::SizeToFit(max_height) => max_height.min(ideal_height), }, ) + .into() } fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_>) { diff --git a/src/widgets/container.rs b/src/widgets/container.rs index 09f4f0148..59aeddf6f 100644 --- a/src/widgets/container.rs +++ b/src/widgets/container.rs @@ -11,7 +11,7 @@ use crate::context::{EventContext, GraphicsContext, LayoutContext, WidgetContext use crate::styles::components::{CornerRadius, IntrinsicPadding, Opacity, SurfaceColor}; use crate::styles::{Component, ContainerLevel, Dimension, Edges, RequireInvalidation, Styles}; use crate::value::{Dynamic, IntoValue, Source, Value}; -use crate::widget::{MakeWidget, RootBehavior, Widget, WidgetInstance, WidgetRef}; +use crate::widget::{MakeWidget, RootBehavior, Widget, WidgetInstance, WidgetLayout, WidgetRef}; use crate::ConstraintLimit; /// A visual container widget, optionally applying padding and a background @@ -256,7 +256,7 @@ impl Widget for Container { &mut self, available_space: Size, context: &mut LayoutContext<'_, '_, '_, '_>, - ) -> Size { + ) -> WidgetLayout { let child = self.child.mounted(context); let corner_radii = context @@ -297,7 +297,7 @@ impl Widget for Container { let shadow_spread = shadow.spread.into_unsigned(); let child_shadow_offset_amount = shadow.offset.abs().into_unsigned(); - let child_size = context.for_other(&child).layout( + let child_layout = context.for_other(&child).layout( available_space - padding_amount - child_shadow_offset_amount - shadow_spread * 2, ); @@ -306,12 +306,17 @@ impl Widget for Container { &child, Rect::new( Point::new(padding.left, padding.top) + child_shadow_offset + shadow_spread, - child_size, + child_layout.size, ) .into_signed(), ); - child_size + padding_amount + child_shadow_offset_amount + shadow_spread * 2 + let size = + child_layout.size + padding_amount + child_shadow_offset_amount + shadow_spread * 2; + WidgetLayout { + size, + baseline: child_layout.baseline.map(|baseline| baseline + padding.top), + } } fn root_behavior( diff --git a/src/widgets/custom.rs b/src/widgets/custom.rs index b165dd596..a52d6642a 100644 --- a/src/widgets/custom.rs +++ b/src/widgets/custom.rs @@ -1,7 +1,7 @@ use std::fmt::Debug; use figures::units::Px; -use figures::{Point, Size}; +use figures::{IntoSigned, Point, Rect, Size}; use kludgine::app::winit::event::{Ime, MouseButton, MouseScrollDelta, TouchPhase}; use kludgine::app::winit::window::CursorIcon; use kludgine::Color; @@ -9,7 +9,9 @@ use kludgine::Color; use crate::context::{EventContext, GraphicsContext, LayoutContext, WidgetContext}; use crate::styles::VisualOrder; use crate::value::{IntoValue, Value}; -use crate::widget::{EventHandling, MakeWidget, WidgetRef, WrappedLayout, WrapperWidget, IGNORED}; +use crate::widget::{ + EventHandling, MakeWidget, WidgetLayout, WidgetRef, WrappedLayout, WrapperWidget, IGNORED, +}; use crate::widgets::Space; use crate::window::{DeviceId, KeyEvent}; use crate::ConstraintLimit; @@ -298,7 +300,7 @@ impl Custom { PositionChild: Send + 'static + for<'context, 'clip, 'gfx, 'pass> FnMut( - Size, + WidgetLayout, Size, &mut LayoutContext<'context, 'clip, 'gfx, 'pass>, ) -> WrappedLayout, @@ -469,18 +471,22 @@ impl WrapperWidget for Custom { fn position_child( &mut self, - size: Size, + layout: WidgetLayout, available_space: Size, context: &mut LayoutContext<'_, '_, '_, '_>, ) -> WrappedLayout { if let Some(position_child) = &mut self.position_child { - position_child.invoke(size, available_space, context) + position_child.invoke(layout, available_space, context) } else { - Size::new( - available_space.width.fit_measured(size.width), - available_space.height.fit_measured(size.height), - ) - .into() + let size = Size::new( + available_space.width.fit_measured(layout.size.width), + available_space.height.fit_measured(layout.size.height), + ); + WrappedLayout { + size, + child: Rect::from(size.into_signed()), + baseline: layout.baseline, + } } } @@ -694,7 +700,7 @@ where trait PositionChildFunc: Send { fn invoke( &mut self, - size: Size, + size: WidgetLayout, available_space: Size, context: &mut LayoutContext<'_, '_, '_, '_>, ) -> WrappedLayout; @@ -705,18 +711,18 @@ where Func: Send + 'static + for<'context, 'clip, 'gfx, 'pass> FnMut( - Size, + WidgetLayout, Size, &mut LayoutContext<'context, 'clip, 'gfx, 'pass>, ) -> WrappedLayout, { fn invoke( &mut self, - size: Size, + layout: WidgetLayout, available_space: Size, context: &mut LayoutContext<'_, '_, '_, '_>, ) -> WrappedLayout { - self(size, available_space, context) + self(layout, available_space, context) } } diff --git a/src/widgets/delimiter.rs b/src/widgets/delimiter.rs index 3eec80bc1..d9ac73b51 100644 --- a/src/widgets/delimiter.rs +++ b/src/widgets/delimiter.rs @@ -1,6 +1,6 @@ //! A visual delimiter widget. -use figures::units::{Lp, UPx}; +use figures::units::Lp; use figures::{Point, ScreenScale, Size}; use kludgine::shapes::{PathBuilder, StrokeOptions}; use kludgine::Color; @@ -9,7 +9,7 @@ use crate::context::{GraphicsContext, LayoutContext}; use crate::styles::components::TextColor; use crate::styles::{Dimension, FlexibleDimension}; use crate::value::{IntoValue, Value}; -use crate::widget::Widget; +use crate::widget::{Widget, WidgetLayout}; use crate::ConstraintLimit; #[derive(Debug)] @@ -95,12 +95,13 @@ impl Widget for Delimiter { &mut self, available_space: Size, context: &mut LayoutContext<'_, '_, '_, '_>, - ) -> Size { + ) -> WidgetLayout { let size = self.get_size(context).into_upx(context.gfx.scale()); - match self.orientation { + let measured = match self.orientation { Orientation::Horizontal => Size::new(available_space.width.max(), size), Orientation::Vertical => Size::new(size, available_space.height.max()), - } + }; + measured.into() } } diff --git a/src/widgets/disclose.rs b/src/widgets/disclose.rs index 4d4bbd270..b2763a37d 100644 --- a/src/widgets/disclose.rs +++ b/src/widgets/disclose.rs @@ -15,8 +15,8 @@ use crate::styles::components::{HighlightColor, IntrinsicPadding, LineHeight, Ou use crate::styles::Dimension; use crate::value::{Destination, Dynamic, IntoDynamic, IntoValue, Source, Value}; use crate::widget::{ - EventHandling, MakeWidget, MakeWidgetWithTag, Widget, WidgetInstance, WidgetRef, WidgetTag, - HANDLED, IGNORED, + EventHandling, MakeWidget, MakeWidgetWithTag, Widget, WidgetInstance, WidgetLayout, WidgetRef, + WidgetTag, HANDLED, IGNORED, }; use crate::window::DeviceId; use crate::ConstraintLimit; @@ -219,7 +219,7 @@ impl Widget for DiscloseIndicator { &mut self, mut available_space: Size, context: &mut LayoutContext<'_, '_, '_, '_>, - ) -> Size { + ) -> WidgetLayout { let indicator_size = context .get(&IndicatorSize) .into_upx(context.gfx.scale()) @@ -232,43 +232,55 @@ impl Widget for DiscloseIndicator { let content_inset = indicator_size + padding; available_space.width -= content_inset; - let label_size = if let Some(label) = &mut self.label { + let label_layout = if let Some(label) = &mut self.label { let label = label.mounted(context); - let label_size = context.for_other(&label).layout(available_space); - let label_vertical_offset = if label_size.height < indicator_size { - (indicator_size - label_size.height).round() + let label_layout = context.for_other(&label).layout(available_space); + let label_vertical_offset = if label_layout.size.height < indicator_size { + (indicator_size - label_layout.size.height).round() } else { UPx::ZERO }; context.set_child_layout( &label, - Rect::new(Point::new(content_inset, label_vertical_offset), label_size) - .into_signed(), + Rect::new( + Point::new(content_inset, label_vertical_offset), + label_layout.size, + ) + .into_signed(), ); - Size::new(label_size.width, label_size.height.max(indicator_size)) + WidgetLayout { + size: Size::new( + label_layout.size.width, + label_layout.size.height.max(indicator_size), + ), + baseline: label_layout.baseline, + } } else { - Size::ZERO + WidgetLayout::ZERO }; - let content_vertical_offset = if label_size.height > 0 { - label_size.height + padding + let content_vertical_offset = if label_layout.size.height > 0 { + label_layout.size.height + padding } else { - label_size.height + label_layout.size.height }; available_space.height -= content_vertical_offset; let contents = self.contents.mounted(context); - let content_size = context.for_other(&contents).layout(available_space); + let content_layout = context.for_other(&contents).layout(available_space); let content_rect = Rect::new( Point::new(content_inset, content_vertical_offset), - content_size, + content_layout.size, ); context.set_child_layout(&contents, content_rect.into_signed()); - Size::new( - content_inset + content_rect.size.width.max(label_size.width), - indicator_size.max(content_rect.origin.y + content_rect.size.height), - ) + WidgetLayout { + size: Size::new( + content_inset + content_rect.size.width.max(label_layout.size.width), + indicator_size.max(content_rect.origin.y + content_rect.size.height), + ), + baseline: label_layout.baseline, + } } fn accept_focus(&mut self, _context: &mut EventContext<'_>) -> bool { diff --git a/src/widgets/expand.rs b/src/widgets/expand.rs index c4e5cdf86..ff8cd534e 100644 --- a/src/widgets/expand.rs +++ b/src/widgets/expand.rs @@ -145,23 +145,28 @@ impl WrapperWidget for Expand { ), }; let child = self.child.mounted(&mut context.as_event_context()); - let size = context.for_other(&child).layout(available_space); + let layout = context.for_other(&child).layout(available_space); let (width, height) = match &self.kind { ExpandKind::Weighted(_) => ( - available_space.width.fit_measured(size.width), - available_space.height.fit_measured(size.height), + available_space.width.fit_measured(layout.size.width), + available_space.height.fit_measured(layout.size.height), ), ExpandKind::Horizontal => ( - available_space.width.fit_measured(size.width), - size.height.min(available_space.height.max()), + available_space.width.fit_measured(layout.size.width), + layout.size.height.min(available_space.height.max()), ), ExpandKind::Vertical => ( - size.width.min(available_space.width.max()), - available_space.height.fit_measured(size.height), + layout.size.width.min(available_space.width.max()), + available_space.height.fit_measured(layout.size.height), ), }; - Size::new(width, height).into_signed().into() + let size = Size::new(width, height); + WrappedLayout { + child: size.into_signed().into(), + size, + baseline: layout.baseline, + } } } diff --git a/src/widgets/grid.rs b/src/widgets/grid.rs index e8dde71ea..0187d424a 100644 --- a/src/widgets/grid.rs +++ b/src/widgets/grid.rs @@ -14,7 +14,7 @@ use crate::context::{AsEventContext, EventContext, GraphicsContext, LayoutContex use crate::styles::components::IntrinsicPadding; use crate::styles::Dimension; use crate::value::{Generation, IntoValue, Value}; -use crate::widget::{MakeWidget, MountedWidget, Widget, WidgetInstance}; +use crate::widget::{Baseline, MakeWidget, MountedWidget, Widget, WidgetInstance, WidgetLayout}; use crate::ConstraintLimit; /// A 2D grid of widgets. @@ -135,10 +135,8 @@ impl Widget for Grid { fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_>) { for (row, widgets) in self.live_rows.iter_mut().enumerate() { if self.layout.others[row] > 0 { - for (column, cell) in widgets.iter().enumerate() { - if self.layout[column].size > 0 { - context.for_other(cell).redraw(); - } + for cell in widgets.iter() { + context.for_other(cell).redraw(); } } } @@ -156,10 +154,10 @@ impl Widget for Grid { &mut self, available_space: Size, context: &mut LayoutContext<'_, '_, '_, '_>, - ) -> Size { + ) -> WidgetLayout { self.synchronize_children(&mut context.as_event_context()); - let content_size = self.layout.update( + let content_layout = self.layout.update( available_space, context .get(&IntrinsicPadding) @@ -179,27 +177,25 @@ impl Widget for Grid { for (&other_size, row) in self.layout.others.iter().zip(&self.live_rows) { if other_size > 0 { for (layout, cell) in self.layout.iter().zip(row) { - if layout.size > 0 { - context.set_child_layout( - cell, - Rect::new( - self.layout - .orientation - .make_point(layout.offset, other_offset) - .into_signed(), - self.layout - .orientation - .make_size(layout.size, other_size) - .into_signed(), - ), - ); - } + context.set_child_layout( + cell, + Rect::new( + self.layout + .orientation + .make_point(layout.offset, other_offset) + .into_signed(), + self.layout + .orientation + .make_size(layout.size, other_size) + .into_signed(), + ), + ); } other_offset = other_offset.saturating_add(other_size); } } - content_size + content_layout } fn summarize(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { @@ -282,12 +278,20 @@ pub(crate) struct GridLayout { pub orientation: Orientation, } -#[derive(Debug, Clone, Copy, Eq, PartialEq)] +#[derive(Debug, Clone, Eq, PartialEq)] pub(crate) struct StackLayout { pub offset: UPx, + pub baselines: Vec, pub size: UPx, } +impl StackLayout { + fn reset_baselines(&mut self, elements: usize) { + self.baselines.clear(); + self.baselines.resize(elements, Baseline::NONE); + } +} + impl GridLayout { pub fn new(orientation: Orientation) -> Self { Self { @@ -379,6 +383,7 @@ impl GridLayout { StackLayout { offset: UPx::ZERO, size: layout, + baselines: Vec::new(), }, ); } @@ -389,8 +394,8 @@ impl GridLayout { available: Size, gutter: UPx, scale: Fraction, - mut measure: impl FnMut(usize, usize, Size, bool) -> Size, - ) -> Size { + mut measure: impl FnMut(usize, usize, Size, bool) -> WidgetLayout, + ) -> WidgetLayout { self.update_measured(scale); let (space_constraint, mut other_constraint) = self.orientation.split_size(available); let available_space = space_constraint.max(); @@ -425,8 +430,10 @@ impl GridLayout { let mut max_measured = UPx::ZERO; + self.layouts[index].reset_baselines(self.elements_per_child); + for element in 0..self.elements_per_child { - let (measured, other) = self.orientation.split_size(measure( + let layout = measure( index, element, self.orientation.make_size( @@ -438,7 +445,9 @@ impl GridLayout { other_constraint, ), !needs_final_layout, - )); + ); + self.layouts[index].baselines[element] = layout.baseline; + let (measured, other) = self.orientation.split_size(layout.size); if measured > 0 { max_measured = max_measured.max(measured); @@ -459,8 +468,9 @@ impl GridLayout { // Measure measure the "other" dimension for children that we know their size already. for &id in &self.premeasured { let index = self.children.index_of_id(id).expect("child not found"); + self.layouts[index].reset_baselines(self.elements_per_child); for element in 0..self.elements_per_child { - let (_, other) = self.orientation.split_size(measure( + let layout = measure( index, element, self.orientation.make_size( @@ -468,7 +478,9 @@ impl GridLayout { other_constraint, ), !needs_final_layout, - )); + ); + self.layouts[index].baselines[element] = layout.baseline; + let (_, other) = self.orientation.split_size(layout.size); self.others[element] = self.others[element].max(other); } } @@ -502,8 +514,9 @@ impl GridLayout { // to get the other measurement using the constrainted measurement. for (id, _) in &self.fractional { let index = self.children.index_of_id(*id).expect("child not found"); + self.layouts[index].reset_baselines(self.elements_per_child); for element in 0..self.elements_per_child { - let (_, measured) = self.orientation.split_size(measure( + let layout = measure( index, element, self.orientation.make_size( @@ -511,7 +524,8 @@ impl GridLayout { other_constraint, ), !needs_final_layout, - )); + ); + let (_, measured) = self.orientation.split_size(layout.size); self.others[element] = self.others[element].max(measured); } } @@ -532,9 +546,12 @@ impl GridLayout { } } - let measured = self.update_offsets(needs_final_layout, gutter, scale, measure); + let (measured, baseline) = self.update_offsets(needs_final_layout, gutter, scale, measure); - self.orientation.make_size(measured, total_other) + WidgetLayout { + size: self.orientation.make_size(measured, total_other), + baseline, + } } fn update_measured(&mut self, scale: Fraction) { @@ -562,9 +579,20 @@ impl GridLayout { needs_final_layout: bool, gutter: UPx, scale: Fraction, - mut measure: impl FnMut(usize, usize, Size, bool) -> Size, - ) -> UPx { + mut measure: impl FnMut(usize, usize, Size, bool) -> WidgetLayout, + ) -> (UPx, Baseline) { let mut offset = UPx::ZERO; + let first_baseline = match self.orientation { + Orientation::Column => self.layouts.iter().fold(Baseline::NONE, |max, layout| { + let baseline = layout.baselines.first().copied().unwrap_or_default(); + baseline.max(max) + }), + Orientation::Row => self + .layouts + .first() + .and_then(|layout| layout.baselines.first().copied()) + .unwrap_or_default(), + }; for index in 0..self.children.len() { let visible = self.layouts[index].size > 0; @@ -591,7 +619,7 @@ impl GridLayout { } } } - offset + (offset, first_baseline) } } @@ -662,7 +690,7 @@ mod tests { flex.push(child.dimension, Fraction::ONE); } - let computed_size = flex.update( + let computed_layout = flex.update( available, UPx::ZERO, Fraction::ONE, @@ -682,12 +710,12 @@ mod tests { } _ => (child.size, child.other), }; - orientation.make_size(measured, other) + orientation.make_size(measured, other).into() }, ); - assert_eq!(computed_size, expected_size); + assert_eq!(computed_layout.size, expected_size); let mut offset = UPx::ZERO; - for ((index, &child), &expected) in flex.iter().enumerate().zip(expected) { + for ((index, child), &expected) in flex.iter().enumerate().zip(expected) { assert_eq!( child.size, expected, "child {index} measured to {}, expected {expected}", diff --git a/src/widgets/image.rs b/src/widgets/image.rs index 7cd8cbc93..0a8f3c970 100644 --- a/src/widgets/image.rs +++ b/src/widgets/image.rs @@ -11,7 +11,7 @@ use crate::animation::ZeroToOne; use crate::context::{LayoutContext, Trackable}; use crate::styles::Dimension; use crate::value::{IntoValue, Source, Value}; -use crate::widget::Widget; +use crate::widget::{Widget, WidgetLayout}; use crate::ConstraintLimit; /// A widget that displays an image/texture. @@ -156,10 +156,11 @@ impl Widget for Image { &mut self, available_space: Size, context: &mut LayoutContext<'_, '_, '_, '_>, - ) -> Size { + ) -> WidgetLayout { let scaling = self.scaling.get_tracking_invalidate(context); self.contents .map(|texture| scaling.layout_size(texture.size(), available_space)) + .into() } } diff --git a/src/widgets/indicator.rs b/src/widgets/indicator.rs index 80430ac4c..38d8788ed 100644 --- a/src/widgets/indicator.rs +++ b/src/widgets/indicator.rs @@ -15,7 +15,9 @@ use crate::styles::components::{ }; use crate::styles::ColorExt; use crate::value::{Destination, Dynamic, Source}; -use crate::widget::{EventHandling, MakeWidget, Widget, WidgetRef, HANDLED, IGNORED}; +use crate::widget::{ + Baseline, EventHandling, MakeWidget, Widget, WidgetLayout, WidgetRef, HANDLED, IGNORED, +}; use crate::window::WindowLocal; use crate::ConstraintLimit; @@ -85,7 +87,7 @@ struct WindowLocalState { focused: bool, hovered: bool, mouse_buttons_pressed: usize, - size: Size, + size: Size, } impl Default for WindowLocalState { @@ -248,47 +250,66 @@ where &mut self, available_space: Size, context: &mut LayoutContext<'_, '_, '_, '_>, - ) -> Size { + ) -> WidgetLayout { let window_local = self.per_window.entry(context).or_default(); - window_local.size = self.behavior.size(context).into_signed().ceil(); - window_local.checkbox_region.size = window_local.size; + window_local.size = self.behavior.size(context).ceil(); + window_local.checkbox_region.size = window_local.size.into_signed(); - let full_size = if let Some(label) = &mut self.label { + let (full_size, baseline) = if let Some(label) = &mut self.label { let padding = context .get(&IntrinsicPadding) .into_px(context.gfx.scale()) .ceil(); - let x_offset = window_local.size.width + padding; + let x_offset = window_local.checkbox_region.size.width + padding; let remaining_space = Size::new( available_space.width - x_offset.into_unsigned(), available_space.height, ); let mounted = label.mounted(context); - let label_size = context - .for_other(&mounted) - .layout(remaining_space) - .into_signed(); + let label_layout = context.for_other(&mounted).layout(remaining_space); + let (offset, height) = match *label_layout.baseline { + Some(baseline) if baseline < window_local.size.height => ( + window_local.size.height - baseline, + window_local.size.height, + ), + _ => (UPx::ZERO, label_layout.size.height), + }; + let height = available_space .height - .fit_measured(label_size.height.into_unsigned()) - .into_signed() - .max(window_local.size.height); + .fit_measured(height) + .max(window_local.size.height) + .into_signed(); window_local.label_region = Rect::new( - Point::new(x_offset, (height - label_size.height) / 2), - label_size, + Point::new( + x_offset, + (height - label_layout.size.height.into_signed()) / 2 + offset.into_signed(), + ), + label_layout.size.into_signed(), ); context.set_child_layout(&mounted, window_local.label_region); - Size::new(label_size.width + x_offset, height).into_unsigned() + ( + Size::new(label_layout.size.width.into_signed() + x_offset, height).into_unsigned(), + label_layout.baseline.map(|baseline| baseline + offset), + ) } else { - window_local.size.into_unsigned() + (window_local.size.into_unsigned(), Baseline::NONE) }; - window_local.checkbox_region.origin.y = - (full_size.height.into_signed() - window_local.size.height) / 2; + 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; + } - full_size + WidgetLayout { + size: full_size, + baseline, + } } fn hit_test(&mut self, location: Point, context: &mut EventContext<'_>) -> bool { diff --git a/src/widgets/input.rs b/src/widgets/input.rs index eff590db5..80dd0d69f 100644 --- a/src/widgets/input.rs +++ b/src/widgets/input.rs @@ -8,7 +8,7 @@ use std::ops::{Deref, DerefMut}; use std::sync::{Arc, OnceLock}; use std::time::Duration; -use figures::units::{Lp, Px, UPx}; +use figures::units::{Lp, Px}; use figures::{ Abs, FloatConversion, IntoSigned, IntoUnsigned, Point, Rect, Round, ScreenScale, Size, Zero, }; @@ -26,7 +26,7 @@ use crate::context::{EventContext, GraphicsContext, LayoutContext}; use crate::styles::components::{HighlightColor, IntrinsicPadding, OutlineColor, TextColor}; use crate::utils::ModifiersExt; use crate::value::{Destination, Dynamic, Generation, IntoDynamic, IntoValue, Source, Value}; -use crate::widget::{Callback, EventHandling, Widget, HANDLED, IGNORED}; +use crate::widget::{Baseline, Callback, EventHandling, Widget, WidgetLayout, HANDLED, IGNORED}; use crate::window::KeyEvent; use crate::{ConstraintLimit, FitMeasuredSize, Lazy}; @@ -1166,7 +1166,7 @@ where &mut self, available_space: Size, context: &mut LayoutContext<'_, '_, '_, '_>, - ) -> Size { + ) -> WidgetLayout { let padding = context .get(&IntrinsicPadding) .into_upx(context.gfx.scale()) @@ -1184,7 +1184,11 @@ where .max(info.cache.placeholder.size) .into_unsigned() + Size::squared(padding * 2); - available_space.fit_measured(measured_size) + + WidgetLayout { + size: available_space.fit_measured(measured_size), + baseline: Baseline::from(padding + info.cache.measured.ascent.into_unsigned()), + } } fn keyboard_input( diff --git a/src/widgets/label.rs b/src/widgets/label.rs index 259a63716..b9300baaa 100644 --- a/src/widgets/label.rs +++ b/src/widgets/label.rs @@ -3,7 +3,7 @@ use std::borrow::Cow; use std::fmt::{Debug, Display, Write}; -use figures::units::{Px, UPx}; +use figures::units::Px; use figures::{IntoUnsigned, Point, Round, Size, Zero}; use kludgine::text::{MeasuredText, Text, TextOrigin}; use kludgine::{cosmic_text, CanRenderTo, Color, DrawableExt}; @@ -15,7 +15,7 @@ use crate::styles::{FontFamilyList, HorizontalAlign, VerticalAlign}; use crate::value::{ Dynamic, DynamicReader, Generation, IntoDynamic, IntoReadOnly, IntoValue, ReadOnly, Value, }; -use crate::widget::{MakeWidgetWithTag, Widget, WidgetInstance, WidgetTag}; +use crate::widget::{MakeWidgetWithTag, Widget, WidgetInstance, WidgetLayout, WidgetTag}; use crate::window::WindowLocal; use crate::{ConstraintLimit, FitMeasuredSize}; @@ -155,13 +155,21 @@ where &mut self, available_space: Size, context: &mut LayoutContext<'_, '_, '_, '_>, - ) -> Size { + ) -> WidgetLayout { let align = context.get(&HorizontalAlignment); let color = context.get(&TextColor); let width = available_space.width.max().try_into().unwrap_or(Px::MAX); let prepared = self.prepared_text(context, color, width, align); - available_space.fit_measured(prepared.size.into_unsigned().ceil()) + // TODO if vertical alignment isn't top and we are using Fill on the + // height constraint limit, we should calculate the actual baseline. On + // that topic, if the text is wrapped, is the baseline of bottom-aligned + // text the bottom line's baseline or the top line's baseline? Probably + // bottom... + WidgetLayout { + size: available_space.fit_measured(prepared.size.into_unsigned().ceil()), + baseline: prepared.ascent.into(), + } } fn summarize(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { diff --git a/src/widgets/layers.rs b/src/widgets/layers.rs index 73ccbc280..43f4bad6e 100644 --- a/src/widgets/layers.rs +++ b/src/widgets/layers.rs @@ -18,8 +18,8 @@ use crate::context::{AsEventContext, EventContext, GraphicsContext, LayoutContex use crate::styles::components::{EasingIn, ScrimColor}; use crate::value::{Destination, Dynamic, DynamicGuard, DynamicRead, IntoValue, Source, Value}; use crate::widget::{ - Callback, MakeWidget, MakeWidgetWithTag, MountedChildren, SharedCallback, Widget, WidgetId, - WidgetList, WidgetRef, WidgetTag, WrapperWidget, + Baseline, Callback, MakeWidget, MakeWidgetWithTag, MountedChildren, SharedCallback, Widget, + WidgetId, WidgetLayout, WidgetList, WidgetRef, WidgetTag, WrappedLayout, WrapperWidget, }; use crate::widgets::container::ContainerShadow; use crate::ConstraintLimit; @@ -71,18 +71,21 @@ impl Widget for Layers { &mut self, available_space: Size, context: &mut LayoutContext<'_, '_, '_, '_>, - ) -> Size { + ) -> WidgetLayout { self.synchronize_children(&mut context.as_event_context()); let mut size = Size::ZERO; + let mut last_baseline = Baseline::NONE; for child in self.mounted.children() { - size = size.max( - context - .for_other(child) - .as_temporary() - .layout(available_space), - ); + let layout = context + .for_other(child) + .as_temporary() + .layout(available_space); + size = size.max(layout.size); + if layout.baseline.is_some() { + last_baseline = layout.baseline; + } } // Now we know the size of the widget, we can request the widgets fill @@ -99,7 +102,10 @@ impl Widget for Layers { context.set_child_layout(child, layout); } - size + WidgetLayout { + size, + baseline: last_baseline, + } } fn mounted(&mut self, context: &mut EventContext<'_>) { @@ -207,7 +213,7 @@ impl Widget for OverlayLayer { &mut self, available_space: Size, context: &mut LayoutContext<'_, '_, '_, '_>, - ) -> Size { + ) -> WidgetLayout { let mut state = self.state.lock(); state.prevent_notifications(); @@ -244,7 +250,7 @@ impl Widget for OverlayLayer { // layers, despite what layouts its children are assigned. This may seem // weird, but it would also be weird for a tooltop to expand its window // when shown. - Size::ZERO + Size::::ZERO.into() } fn hit_test(&mut self, location: Point, context: &mut EventContext<'_>) -> bool { @@ -429,6 +435,7 @@ impl OverlayState { let size = context .for_other(widget) .layout(constraints.map(ConstraintLimit::SizeToFit)) + .size .into_signed(); let mut layout_direction = positioning; @@ -545,6 +552,7 @@ impl OverlayState { let size = context .for_other(widget) .layout(available_space.map(ConstraintLimit::SizeToFit)) + .size .into_signed(); let available_space = available_space.into_signed(); @@ -1029,10 +1037,10 @@ impl WrapperWidget for ModalLayer { fn position_child( &mut self, - size: Size, + layout: WidgetLayout, available_space: Size, context: &mut LayoutContext<'_, '_, '_, '_>, - ) -> crate::widget::WrappedLayout { + ) -> WrappedLayout { if self.focus_top_layer { self.focus_top_layer = false; if let Some(mut ctx) = self @@ -1044,8 +1052,8 @@ impl WrapperWidget for ModalLayer { } } Size::new( - available_space.width.fit_measured(size.width), - available_space.height.fit_measured(size.height), + available_space.width.fit_measured(layout.size.width), + available_space.height.fit_measured(layout.size.height), ) .into() } diff --git a/src/widgets/menu.rs b/src/widgets/menu.rs index 18e00af39..949f1490b 100644 --- a/src/widgets/menu.rs +++ b/src/widgets/menu.rs @@ -25,7 +25,7 @@ use crate::styles::Styles; use crate::value::{Dynamic, IntoValue, Source, Value}; use crate::widget::{ Callback, EventHandling, MakeWidget, MakeWidgetWithTag, SharedCallback, Widget, WidgetId, - WidgetInstance, WidgetRef, WidgetTag, HANDLED, + WidgetInstance, WidgetLayout, WidgetRef, WidgetTag, HANDLED, }; use crate::ConstraintLimit; @@ -642,7 +642,7 @@ where &mut self, available_space: Size, context: &mut LayoutContext<'_, '_, '_, '_>, - ) -> Size { + ) -> WidgetLayout { let mut maximum_item_width = UPx::ZERO; let mut remaining_height = available_space.height.max(); self.padding = context.get(&IntrinsicPadding).into_upx(context.gfx.scale()); @@ -662,10 +662,13 @@ where ItemKind::Item(item) => { let mounted = item.contents.mounted(context); let available_width = available_width - submenu_space; - let size = context.for_other(&mounted).layout(Size::new( - ConstraintLimit::SizeToFit(available_width), - ConstraintLimit::SizeToFit(remaining_height), - )); + let size = context + .for_other(&mounted) + .layout(Size::new( + ConstraintLimit::SizeToFit(available_width), + ConstraintLimit::SizeToFit(remaining_height), + )) + .size; maximum_item_width = maximum_item_width.max(size.width); (size.height, size.height + double_padding) } @@ -694,7 +697,8 @@ where ); } - Size::new(maximum_item_width + double_padding * 2 + submenu_space, y) + // TODO should a Menu report its baseline? + Size::new(maximum_item_width + double_padding * 2 + submenu_space, y).into() } fn hit_test( diff --git a/src/widgets/pile.rs b/src/widgets/pile.rs index df5ba3f6d..577d4523c 100644 --- a/src/widgets/pile.rs +++ b/src/widgets/pile.rs @@ -5,13 +5,14 @@ use std::sync::Arc; use ahash::AHashMap; use alot::{LotId, Lots}; -use figures::units::UPx; use figures::{IntoSigned, Rect, Size}; use intentional::Assert; use crate::context::{EventContext, GraphicsContext, LayoutContext}; use crate::value::{Dynamic, DynamicRead, DynamicReader}; -use crate::widget::{MakeWidget, MakeWidgetWithTag, Widget, WidgetInstance, WidgetRef, WidgetTag}; +use crate::widget::{ + MakeWidget, MakeWidgetWithTag, Widget, WidgetInstance, WidgetLayout, WidgetRef, WidgetTag, +}; use crate::ConstraintLimit; /// A pile of widgets that shows the top widget. @@ -110,12 +111,12 @@ impl Widget for WidgetPile { &mut self, available_space: Size, context: &mut LayoutContext<'_, '_, '_, '_>, - ) -> Size { + ) -> WidgetLayout { context.invalidate_when_changed(&self.pile); self.synchronize_widgets(); let pile = self.pile.read(); let visible = pile.visible.front().copied(); - let size = if let Some(id) = visible { + let layout = if let Some(id) = visible { let visible = self .widgets .get_mut(&id) @@ -125,17 +126,17 @@ impl Widget for WidgetPile { if pile.focus_visible && self.last_visible != Some(id) { child_context.focus(); } - let size = child_context.layout(available_space); + let layout = child_context.layout(available_space); drop(child_context); - context.set_child_layout(&visible, Rect::from(size).into_signed()); - size + context.set_child_layout(&visible, Rect::from(layout.size).into_signed()); + layout } else { - available_space.map(ConstraintLimit::min) + available_space.map(ConstraintLimit::min).into() }; self.last_visible = visible; - size + layout } fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_>) { diff --git a/src/widgets/progress.rs b/src/widgets/progress.rs index e0672113f..0e03f9fcc 100644 --- a/src/widgets/progress.rs +++ b/src/widgets/progress.rs @@ -15,7 +15,7 @@ use crate::animation::{ use crate::styles::components::{EasingIn, EasingOut}; use crate::styles::ContextFreeComponent; use crate::value::{Destination, Dynamic, IntoReadOnly, IntoReader, MapEach, ReadOnly, Source}; -use crate::widget::{MakeWidget, MakeWidgetWithTag, Widget, WidgetInstance}; +use crate::widget::{MakeWidget, MakeWidgetWithTag, Widget, WidgetInstance, WidgetLayout}; use crate::widgets::slider::{InactiveTrackColor, Slidable, TrackColor, TrackSize}; use crate::widgets::Data; @@ -403,10 +403,12 @@ impl Widget for Spinner { &mut self, available_space: figures::Size, context: &mut crate::context::LayoutContext<'_, '_, '_, '_>, - ) -> figures::Size { + ) -> WidgetLayout { let track_size = context.get(&TrackSize).into_px(context.gfx.scale()); let minimum_size = track_size * 4; - available_space.map(|constraint| constraint.fit_measured(minimum_size)) + available_space + .map(|constraint| constraint.fit_measured(minimum_size)) + .into() } } diff --git a/src/widgets/radio.rs b/src/widgets/radio.rs index 5a8845f65..56c69240b 100644 --- a/src/widgets/radio.rs +++ b/src/widgets/radio.rs @@ -15,7 +15,7 @@ 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}; +use crate::widget::{MakeWidget, MakeWidgetWithTag, Widget, WidgetInstance, WidgetLayout}; use crate::widgets::button::ButtonKind; use crate::ConstraintLimit; @@ -272,9 +272,9 @@ where &mut self, _available_space: Size, context: &mut LayoutContext<'_, '_, '_, '_>, - ) -> Size { + ) -> WidgetLayout { let radio_size = context.get(&RadioSize).into_upx(context.gfx.scale()); - Size::squared(radio_size) + Size::squared(radio_size).into() } } diff --git a/src/widgets/resize.rs b/src/widgets/resize.rs index e472a57d6..d807f8bf4 100644 --- a/src/widgets/resize.rs +++ b/src/widgets/resize.rs @@ -99,11 +99,13 @@ impl WrapperWidget for Resize { context: &mut LayoutContext<'_, '_, '_, '_>, ) -> WrappedLayout { let child = self.child.mounted(&mut context.as_event_context()); - let (size, fill_layout) = if let (Some(width), Some(height)) = + let (mut layout, fill_layout) = if let (Some(width), Some(height)) = (self.width.exact_dimension(), self.height.exact_dimension()) { ( - Size::new(width, height).map(|i| i.into_upx(context.gfx.scale())), + Size::new(width, height) + .map(|i| i.into_upx(context.gfx.scale())) + .into(), true, ) } else { @@ -117,21 +119,22 @@ impl WrapperWidget for Resize { || matches!(available_space.height, ConstraintLimit::SizeToFit(_)), ) }; - let mut size = Size::new( - self.width.clamp(size.width, context.gfx.scale()), - self.height.clamp(size.height, context.gfx.scale()), + layout.size = Size::new( + self.width.clamp(layout.size.width, context.gfx.scale()), + self.height.clamp(layout.size.height, context.gfx.scale()), ); if fill_layout { // Now that we have our known dimension, give the child an opportunity // to lay out with Fill semantics. - size = context + let filled_layout = context .for_other(&child) - .layout(size.map(ConstraintLimit::Fill)) - .min(size); + .layout(layout.size.map(ConstraintLimit::Fill)); + layout.size = filled_layout.size.min(layout.size); + layout.baseline = filled_layout.baseline; } - WrappedLayout::aligned(size, available_space, context) + WrappedLayout::aligned(layout, available_space, context) } } diff --git a/src/widgets/scroll.rs b/src/widgets/scroll.rs index be753ee87..4493b57ff 100644 --- a/src/widgets/scroll.rs +++ b/src/widgets/scroll.rs @@ -21,7 +21,9 @@ use crate::styles::Dimension; use crate::value::{ Destination, Dynamic, DynamicReader, IntoDynamic, IntoValue, MapEachCloned, Source, Value, }; -use crate::widget::{EventHandling, MakeWidget, Widget, WidgetId, WidgetRef, HANDLED, IGNORED}; +use crate::widget::{ + EventHandling, MakeWidget, Widget, WidgetId, WidgetLayout, WidgetRef, HANDLED, IGNORED, +}; use crate::window::DeviceId; use crate::ConstraintLimit; @@ -292,7 +294,7 @@ impl Widget for Scroll { &mut self, available_space: Size, context: &mut LayoutContext<'_, '_, '_, '_>, - ) -> Size { + ) -> WidgetLayout { let max_extents = Size::new( if self.enabled.x { ConstraintLimit::SizeToFit(UPx::MAX) @@ -306,7 +308,7 @@ impl Widget for Scroll { }, ); let contents = self.contents.mounted(&mut context.as_event_context()); - let new_content_size = context.for_other(&contents).layout(max_extents); + let new_content_size = context.for_other(&contents).layout(max_extents).size; self.content_size.set(new_content_size); let new_control_size = Size::new( @@ -326,7 +328,7 @@ impl Widget for Scroll { .horizontal_widget .make_if_needed() .mounted(&mut context.as_event_context()); - let layout = context.for_other(&horizontal).layout(available_space); + let layout = context.for_other(&horizontal).layout(available_space).size; context.set_child_layout( &horizontal, Rect::new( @@ -345,7 +347,7 @@ impl Widget for Scroll { .vertical_widget .make_if_needed() .mounted(&mut context.as_event_context()); - let layout = context.for_other(&vertical).layout(available_space); + let layout = context.for_other(&vertical).layout(available_space).size; context.set_child_layout( &vertical, Rect::new( @@ -372,7 +374,7 @@ impl Widget for Scroll { ); context.set_child_layout(&contents, region); - new_control_size + new_control_size.into() } fn mouse_wheel( @@ -802,7 +804,7 @@ impl Widget for ScrollBar { &mut self, available_space: Size, context: &mut LayoutContext<'_, '_, '_, '_>, - ) -> Size { + ) -> WidgetLayout { self.bar_width = context .get(&ScrollBarThickness) .into_upx(context.gfx.scale()) @@ -810,9 +812,9 @@ impl Widget for ScrollBar { self.line_height = context.get(&LineHeight).into_upx(context.gfx.scale()); if self.vertical { - Size::new(self.bar_width, available_space.height.max()) + Size::new(self.bar_width, available_space.height.max()).into() } else { - Size::new(available_space.width.max(), self.bar_width) + Size::new(available_space.width.max(), self.bar_width).into() } } diff --git a/src/widgets/slider.rs b/src/widgets/slider.rs index adfd1b354..a4ac4c84b 100644 --- a/src/widgets/slider.rs +++ b/src/widgets/slider.rs @@ -20,7 +20,7 @@ use crate::styles::components::{ }; use crate::styles::{Dimension, HorizontalOrder, VerticalOrder, VisualOrder}; use crate::value::{Destination, Dynamic, IntoDynamic, IntoValue, Source, Value}; -use crate::widget::{EventHandling, Widget, HANDLED, IGNORED}; +use crate::widget::{EventHandling, Widget, WidgetLayout, HANDLED, IGNORED}; use crate::window::{DeviceId, KeyEvent}; use crate::ConstraintLimit; @@ -502,7 +502,7 @@ where &mut self, available_space: Size, context: &mut LayoutContext<'_, '_, '_, '_>, - ) -> Size { + ) -> WidgetLayout { self.knob_size = if self.knob_visible { context.get(&KnobSize).into_upx(context.gfx.scale()) } else { @@ -551,6 +551,7 @@ where Size::new(width.min(minimum_size), static_side) } } + .into() } fn hit_test(&mut self, _location: Point, _context: &mut EventContext<'_>) -> bool { diff --git a/src/widgets/space.rs b/src/widgets/space.rs index 64c89772f..a214a6638 100644 --- a/src/widgets/space.rs +++ b/src/widgets/space.rs @@ -1,4 +1,3 @@ -use figures::units::UPx; use figures::Size; use kludgine::Color; @@ -6,7 +5,7 @@ use crate::context::{GraphicsContext, LayoutContext}; use crate::styles::components::PrimaryColor; use crate::styles::{DynamicComponent, IntoDynamicComponentValue}; use crate::value::{IntoValue, Value}; -use crate::widget::Widget; +use crate::widget::{Widget, WidgetLayout}; use crate::ConstraintLimit; /// A widget that occupies space, optionally filling it with a color. @@ -73,8 +72,8 @@ impl Widget for Space { &mut self, available_space: Size, _context: &mut LayoutContext<'_, '_, '_, '_>, - ) -> Size { - available_space.map(ConstraintLimit::min) + ) -> WidgetLayout { + available_space.map(ConstraintLimit::min).into() } } diff --git a/src/widgets/stack.rs b/src/widgets/stack.rs index 412c0590f..4476cbe84 100644 --- a/src/widgets/stack.rs +++ b/src/widgets/stack.rs @@ -8,7 +8,9 @@ use crate::context::{AsEventContext, EventContext, GraphicsContext, LayoutContex use crate::styles::components::IntrinsicPadding; use crate::styles::FlexibleDimension; use crate::value::{Generation, IntoValue, Value}; -use crate::widget::{ChildrenSyncChange, MountedWidget, Widget, WidgetList, WidgetRef}; +use crate::widget::{ + ChildrenSyncChange, MountedWidget, Widget, WidgetLayout, WidgetList, WidgetRef, +}; use crate::widgets::grid::{GridDimension, GridLayout, Orientation}; use crate::widgets::{Expand, Resize}; use crate::ConstraintLimit; @@ -153,7 +155,7 @@ impl Widget for Stack { &mut self, available_space: Size, context: &mut LayoutContext<'_, '_, '_, '_>, - ) -> Size { + ) -> WidgetLayout { self.synchronize_children(&mut context.as_event_context()); self.gutter.invalidate_when_changed(context); diff --git a/src/widgets/tilemap.rs b/src/widgets/tilemap.rs index 69205a4bc..5e2e67b46 100644 --- a/src/widgets/tilemap.rs +++ b/src/widgets/tilemap.rs @@ -1,6 +1,6 @@ use std::fmt::Debug; -use figures::units::{Px, UPx}; +use figures::units::Px; use figures::{Point, Size}; use intentional::Cast; use kludgine::app::winit::event::{ElementState, MouseScrollDelta, TouchPhase}; @@ -11,7 +11,7 @@ use kludgine::tilemap::TileMapFocus; use crate::context::{EventContext, GraphicsContext, LayoutContext, Trackable}; use crate::tick::Tick; use crate::value::{Dynamic, IntoValue, Value}; -use crate::widget::{EventHandling, Widget, HANDLED, IGNORED}; +use crate::widget::{EventHandling, Widget, WidgetLayout, HANDLED, IGNORED}; use crate::window::{DeviceId, KeyEvent}; use crate::ConstraintLimit; @@ -120,8 +120,8 @@ where &mut self, available_space: Size, _context: &mut LayoutContext<'_, '_, '_, '_>, - ) -> Size { - Size::new(available_space.width.max(), available_space.height.max()) + ) -> WidgetLayout { + Size::new(available_space.width.max(), available_space.height.max()).into() } fn mouse_wheel( diff --git a/src/widgets/virtual_list.rs b/src/widgets/virtual_list.rs index cf4b33406..3e1c8028e 100644 --- a/src/widgets/virtual_list.rs +++ b/src/widgets/virtual_list.rs @@ -16,7 +16,8 @@ use crate::value::{ Destination, Dynamic, DynamicReader, IntoDynamic, IntoValue, MapEachCloned, Source, Watcher, }; use crate::widget::{ - Callback, EventHandling, MakeWidget, MountedWidget, Widget, WidgetInstance, HANDLED, IGNORED, + Callback, EventHandling, MakeWidget, MountedWidget, Widget, WidgetInstance, WidgetLayout, + HANDLED, IGNORED, }; use crate::widgets::scroll::ScrollBar; use crate::window::DeviceId; @@ -200,7 +201,7 @@ impl VirtualList { .horizontal_scroll .make_if_needed() .mounted(&mut context.as_event_context()); - let scrollbar_layout = context.for_other(&horizontal).layout(available_space); + let scrollbar_layout = context.for_other(&horizontal).layout(available_space).size; context.set_child_layout( &horizontal, Rect::new( @@ -219,7 +220,7 @@ impl VirtualList { .vertical_scroll .make_if_needed() .mounted(&mut context.as_event_context()); - let scrollbar_layout = context.for_other(&vertical).layout(available_space); + let scrollbar_layout = context.for_other(&vertical).layout(available_space).size; context.set_child_layout( &vertical, Rect::new( @@ -314,7 +315,7 @@ impl VirtualList { let mut y = -(scroll.y % item_size.height).into_signed(); let constraint = item_size.map(ConstraintLimit::Fill); for item in &self.items { - let child_size = context.for_other(&item.mounted).layout(constraint); + let child_size = context.for_other(&item.mounted).layout(constraint).size; context.set_child_layout( &item.mounted, @@ -349,6 +350,7 @@ impl VirtualList { .mounted, ) .layout(available_space.map(|space| ConstraintLimit::SizeToFit(space.max()))) + .size } } @@ -407,13 +409,14 @@ impl Widget for VirtualList { &mut self, available_space: Size, context: &mut LayoutContext<'_, '_, '_, '_>, - ) -> Size { + ) -> WidgetLayout { let item_count = self.item_count.get_tracking_invalidate(context); if item_count == 0 { - return available_space.map(ConstraintLimit::min); + return available_space.map(ConstraintLimit::min).into(); } self.layout_rows(item_count, available_space, context) + .into() } fn mouse_wheel( diff --git a/src/widgets/wrap.rs b/src/widgets/wrap.rs index da9897eae..ecb345757 100644 --- a/src/widgets/wrap.rs +++ b/src/widgets/wrap.rs @@ -1,7 +1,7 @@ //! A widget for laying out multiple widgets in a similar fashion as how words //! are wrapped in a paragraph. -use figures::units::{Px, UPx}; +use figures::units::UPx; use figures::{IntoSigned, IntoUnsigned, Point, Rect, Round, ScreenScale, Size, Zero}; use intentional::Cast; @@ -9,7 +9,7 @@ use crate::context::{AsEventContext, GraphicsContext, LayoutContext, Trackable}; use crate::styles::components::{IntrinsicPadding, LayoutOrder, VerticalAlignment}; use crate::styles::{FlexibleDimension, HorizontalOrder, VerticalAlign}; use crate::value::{IntoValue, Value}; -use crate::widget::{MountedChildren, Widget, WidgetList}; +use crate::widget::{Baseline, MountedChildren, Widget, WidgetLayout, WidgetList}; use crate::ConstraintLimit; /// A widget that lays its children out horizontally, wrapping into multiple @@ -58,28 +58,28 @@ impl Wrap { fn horizontal_alignment( align: WrapAlign, order: HorizontalOrder, - remaining: Px, + remaining: UPx, row_children_len: usize, - ) -> (Px, Px) { + ) -> (UPx, UPx) { match (align, order) { (WrapAlign::Start, HorizontalOrder::LeftToRight) - | (WrapAlign::End, HorizontalOrder::RightToLeft) => (Px::ZERO, Px::ZERO), + | (WrapAlign::End, HorizontalOrder::RightToLeft) => (UPx::ZERO, UPx::ZERO), (WrapAlign::End, HorizontalOrder::LeftToRight) - | (WrapAlign::Start, HorizontalOrder::RightToLeft) => (remaining, Px::ZERO), - (WrapAlign::Center, _) => (remaining / 2, Px::ZERO), + | (WrapAlign::Start, HorizontalOrder::RightToLeft) => (remaining, UPx::ZERO), + (WrapAlign::Center, _) => (remaining / 2, UPx::ZERO), (WrapAlign::SpaceBetween, _) => { if row_children_len > 1 { - (Px::ZERO, remaining / (row_children_len - 1).cast::()) + (UPx::ZERO, remaining / (row_children_len - 1).cast::()) } else { - (Px::ZERO, Px::ZERO) + (UPx::ZERO, UPx::ZERO) } } (WrapAlign::SpaceEvenly, _) => { - let spacing = remaining / row_children_len.cast::(); + let spacing = remaining / row_children_len.cast::(); (spacing / 2, spacing) } (WrapAlign::SpaceAround, _) => { - let spacing = remaining / (row_children_len + 1).cast::(); + let spacing = remaining / (row_children_len + 1).cast::(); (spacing, spacing) } } @@ -98,11 +98,11 @@ impl Widget for Wrap { &mut self, available_space: Size, context: &mut LayoutContext<'_, '_, '_, '_>, - ) -> Size { + ) -> WidgetLayout { struct RowChild { index: usize, - x: Px, - size: Size, + x: UPx, + layout: WidgetLayout, } let order = context.get(&LayoutOrder).horizontal; @@ -117,37 +117,37 @@ impl Widget for Wrap { FlexibleDimension::Auto => context.get(&IntrinsicPadding), FlexibleDimension::Dimension(dimension) => dimension, }) - .into_px(context.gfx.scale()) + .into_upx(context.gfx.scale()) .round(); self.mounted .synchronize_with(&self.children, &mut context.as_event_context()); - let mut y = Px::ZERO; + let mut y = UPx::ZERO; let mut row_children = Vec::new(); let mut index = 0; - let width = available_space.width.max().into_signed(); + let width = available_space.width.max(); let child_constraints = available_space.map(|limit| ConstraintLimit::SizeToFit(limit.max())); + let mut first_baseline = Baseline::NONE; while index < self.mounted.children().len() { - if y != Px::ZERO { + if y != UPx::ZERO { y += spacing.height; } // Find all children that can fit on this next row. - let mut x = Px::ZERO; - let mut max_height = Px::ZERO; + let mut x = UPx::ZERO; + let mut max_height = UPx::ZERO; + let mut max_baseline = Baseline::NONE; while let Some(child) = self.mounted.children().get(index) { - let child_size = context - .for_other(child) - .layout(child_constraints) - .into_signed(); - max_height = max_height.max(child_size.height); + 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 { x.saturating_add(spacing.width) }; - let after_child = child_x.saturating_add(child_size.width); + let after_child = child_x.saturating_add(child_layout.size.width); if x > 0 && after_child > width { break; @@ -156,7 +156,7 @@ impl Widget for Wrap { row_children.push(RowChild { index, x: child_x, - size: child_size, + layout: child_layout, }); x = after_child; @@ -164,13 +164,17 @@ impl Widget for Wrap { } // Calculate the horizontal alignment. - let remaining = (width - x).max(Px::ZERO); + let remaining = width.saturating_sub(x); let (x, space_between) = if remaining > 0 { Self::horizontal_alignment(align, order, remaining, row_children.len()) } else { - (Px::ZERO, Px::ZERO) + (UPx::ZERO, UPx::ZERO) }; + if y == 0 { + first_baseline = max_baseline; + } + // Position the children let mut additional_x = x; for (child_index, child) in row_children.drain(..).enumerate() { @@ -179,21 +183,24 @@ impl Widget for Wrap { } let child_x = additional_x + child.x; let child_y = y + match vertical_align { - VerticalAlign::Top => Px::ZERO, - VerticalAlign::Center => (max_height - child.size.height) / 2, - VerticalAlign::Bottom => max_height - child.size.height, + VerticalAlign::Top => UPx::ZERO, + VerticalAlign::Center => (max_height - child.layout.size.height) / 2, + VerticalAlign::Bottom => max_height - child.layout.size.height, }; context.set_child_layout( &self.mounted.children()[child.index], - Rect::new(Point::new(child_x, child_y), child.size), + Rect::new(Point::new(child_x, child_y), child.layout.size).into_signed(), ); } y += max_height; } - Size::new(width, y).into_unsigned() + WidgetLayout { + size: Size::new(width, y).into_unsigned(), + baseline: first_baseline, + } } } diff --git a/src/window.rs b/src/window.rs index 709c1a7c5..77da2ef55 100644 --- a/src/window.rs +++ b/src/window.rs @@ -1896,12 +1896,13 @@ where layout_context.graphics.gfx.fill(background_color); } - let layout_size = - layout_context.layout(if matches!(root_mode, RootMode::Expand | RootMode::Align) { + let layout_size = layout_context + .layout(if matches!(root_mode, RootMode::Expand | RootMode::Align) { window_size.map(ConstraintLimit::Fill) } else { window_size.map(ConstraintLimit::SizeToFit) - }); + }) + .size; let actual_size = if root_mode == RootMode::Align { window_size.max(layout_size) } else { From 5b8007a17b67b2e565fa4cc46d967546bae26acb Mon Sep 17 00:00:00 2001 From: Jonathan Johnson Date: Sun, 1 Dec 2024 07:57:48 -0800 Subject: [PATCH 2/2] Wrap + Indicator baseline alignment --- Cargo.lock | 2 +- examples/wrap.rs | 5 + src/styles.rs | 5 +- src/widgets/checkbox.rs | 211 ++++++++++++++++++++++++--------------- src/widgets/indicator.rs | 50 ++++++---- src/widgets/label.rs | 4 +- src/widgets/radio.rs | 19 +++- src/widgets/wrap.rs | 21 +++- 8 files changed, 200 insertions(+), 117 deletions(-) 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, };