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/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/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/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..57b4ce9f4 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, } } @@ -2835,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. @@ -2852,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/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..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}; +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 { @@ -483,12 +528,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..2b7713413 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; @@ -58,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. @@ -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,72 @@ 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; + 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 = 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()) .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 height = available_space - .height - .fit_measured(label_size.height.into_unsigned()) - .into_signed() - .max(window_local.size.height); + 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 < indicator_baseline => ( + indicator_baseline.saturating_sub(baseline), + window_local.size.height, + ), + _ => (UPx::ZERO, label_layout.size.height), + }; window_local.label_region = Rect::new( - Point::new(x_offset, (height - label_size.height) / 2), - label_size, + Point::new(x_offset, 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 + x_offset.into_unsigned(), height), + 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; + 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()); - 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..aad50de44 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}; @@ -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 } @@ -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.line_height.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..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}; +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( @@ -272,9 +283,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..a9d6690e0 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,12 @@ impl Widget for Wrap { &mut self, available_space: Size, context: &mut LayoutContext<'_, '_, '_, '_>, - ) -> Size { + ) -> WidgetLayout { struct RowChild { index: usize, - x: Px, - size: Size, + x: UPx, + baseline_offset: UPx, + layout: WidgetLayout, } let order = context.get(&LayoutOrder).horizontal; @@ -117,46 +118,47 @@ 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); 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; } + 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, - size: child_size, + layout: child_layout, + baseline_offset: UPx::ZERO, }); x = after_child; @@ -164,13 +166,29 @@ 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 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; + } + // Position the children let mut additional_x = x; for (child_index, child) in row_children.drain(..).enumerate() { @@ -179,21 +197,25 @@ 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::Baseline => child.baseline_offset, + 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 {