From e15ae59c5c1175365b2a724d58ba3e5c995e4185 Mon Sep 17 00:00:00 2001 From: Jonathan Johnson Date: Sat, 25 Nov 2023 12:00:59 -0800 Subject: [PATCH] Refactored root resize behavior Closes #84, Closes #77, Closes #78 --- examples/buttons.rs | 1 - examples/collapse.rs | 1 - examples/containers.rs | 1 - examples/counter.rs | 1 - examples/cursor-icon.rs | 1 - examples/custom-widgets.rs | 1 - examples/focus-order.rs | 1 - examples/input.rs | 1 - examples/login.rs | 1 - examples/progress.rs | 1 - examples/radio.rs | 1 - examples/slider.rs | 2 +- examples/switcher.rs | 1 - examples/tic-tac-toe.rs | 1 - examples/validation.rs | 2 +- src/styles.rs | 102 +++++++++++++++++++++++- src/widget.rs | 37 ++++++++- src/widgets/align.rs | 8 +- src/widgets/container.rs | 14 +++- src/widgets/expand.rs | 8 +- src/widgets/resize.rs | 8 +- src/window.rs | 157 ++++++++++++++++++++++++------------- 22 files changed, 269 insertions(+), 82 deletions(-) diff --git a/examples/buttons.rs b/examples/buttons.rs index a16d587..509d676 100644 --- a/examples/buttons.rs +++ b/examples/buttons.rs @@ -54,6 +54,5 @@ fn main() -> gooey::Result { ) .into_rows() .centered() - .expand() .run() } diff --git a/examples/collapse.rs b/examples/collapse.rs index 33fc8ae..29d0faa 100644 --- a/examples/collapse.rs +++ b/examples/collapse.rs @@ -21,6 +21,5 @@ fn main() -> gooey::Result { ) .into_columns() .centered() - .expand() .run() } diff --git a/examples/containers.rs b/examples/containers.rs index f67e098..22eca56 100644 --- a/examples/containers.rs +++ b/examples/containers.rs @@ -7,7 +7,6 @@ fn main() -> gooey::Result { let theme_mode = Dynamic::default(); set_of_containers(3, theme_mode.clone()) .centered() - .expand() .into_window() .themed_mode(theme_mode) .run() diff --git a/examples/counter.rs b/examples/counter.rs index 8f9fabd..3ceebc6 100644 --- a/examples/counter.rs +++ b/examples/counter.rs @@ -23,6 +23,5 @@ fn main() -> gooey::Result { }))) .into_columns() .centered() - .expand() .run() } diff --git a/examples/cursor-icon.rs b/examples/cursor-icon.rs index c55b9ae..35c0b8c 100644 --- a/examples/cursor-icon.rs +++ b/examples/cursor-icon.rs @@ -20,6 +20,5 @@ fn main() -> gooey::Result { .on_hit_test(|_location, _context| true) .contain() .centered() - .expand() .run() } diff --git a/examples/custom-widgets.rs b/examples/custom-widgets.rs index 6137a8d..3c46410 100644 --- a/examples/custom-widgets.rs +++ b/examples/custom-widgets.rs @@ -21,7 +21,6 @@ fn main() -> gooey::Result { .and("impl Widget".and(impl_widget()).into_rows()) .into_columns() .centered() - .expand() .run() } diff --git a/examples/focus-order.rs b/examples/focus-order.rs index 0f726f0..a5d1a79 100644 --- a/examples/focus-order.rs +++ b/examples/focus-order.rs @@ -74,7 +74,6 @@ fn main() -> gooey::Result { .width(Lp::points(300)..Lp::points(600)) .scroll() .centered() - .expand() .run() } diff --git a/examples/input.rs b/examples/input.rs index 03c09e9..871cf32 100644 --- a/examples/input.rs +++ b/examples/input.rs @@ -16,6 +16,5 @@ fn main() -> gooey::Result { .width(Px::new(100)..Px::new(800)) .scroll() .centered() - .expand() .run() } diff --git a/examples/login.rs b/examples/login.rs index dd1a996..c9cdba3 100644 --- a/examples/login.rs +++ b/examples/login.rs @@ -78,6 +78,5 @@ fn main() -> gooey::Result { .pad() .scroll() .centered() - .expand() .run() } diff --git a/examples/progress.rs b/examples/progress.rs index e9dc8ba..5d81579 100644 --- a/examples/progress.rs +++ b/examples/progress.rs @@ -26,6 +26,5 @@ fn main() -> gooey::Result { .pad() .size(Size::squared(Lp::inches(3))) .centered() - .expand() .run() } diff --git a/examples/radio.rs b/examples/radio.rs index 387f36f..9b52684 100644 --- a/examples/radio.rs +++ b/examples/radio.rs @@ -19,6 +19,5 @@ fn main() -> gooey::Result { .and(option.new_radio(Choice::C, "C")) .into_rows() .centered() - .expand() .run() } diff --git a/examples/slider.rs b/examples/slider.rs index ac77310..6b2a2d2 100644 --- a/examples/slider.rs +++ b/examples/slider.rs @@ -20,8 +20,8 @@ fn main() -> gooey::Result { .expand_horizontally() .contain() .width(..Lp::points(800)) + .pad() .centered() - .expand() .run() } diff --git a/examples/switcher.rs b/examples/switcher.rs index 7a0cd08..4b2d246 100644 --- a/examples/switcher.rs +++ b/examples/switcher.rs @@ -18,7 +18,6 @@ fn main() -> gooey::Result { }) .contain() .centered() - .expand() .run() } diff --git a/examples/tic-tac-toe.rs b/examples/tic-tac-toe.rs index a702ed1..7a8e2c1 100644 --- a/examples/tic-tac-toe.rs +++ b/examples/tic-tac-toe.rs @@ -22,7 +22,6 @@ fn main() -> gooey::Result { .width(Lp::inches(2)..Lp::inches(6)) .height(Lp::inches(2)..Lp::inches(6)) .centered() - .expand() .run() } diff --git a/examples/validation.rs b/examples/validation.rs index 26af42c..8426546 100644 --- a/examples/validation.rs +++ b/examples/validation.rs @@ -35,9 +35,9 @@ fn main() -> gooey::Result { validations.reset(); })) .into_rows() + .pad() .width(Lp::inches(6)) .centered() - .expand() .run() } diff --git a/src/styles.rs b/src/styles.rs index a1d25f7..afdc277 100644 --- a/src/styles.rs +++ b/src/styles.rs @@ -5,7 +5,7 @@ use std::borrow::Cow; use std::collections::hash_map; use std::fmt::Debug; use std::ops::{ - Add, Bound, Deref, Div, Mul, Range, RangeFrom, RangeFull, RangeInclusive, RangeTo, + Add, AddAssign, Bound, Deref, Div, Mul, Range, RangeFrom, RangeFull, RangeInclusive, RangeTo, RangeToInclusive, }; use std::panic::{RefUnwindSafe, UnwindSafe}; @@ -608,6 +608,15 @@ pub struct DimensionRange { pub end: Bound, } +impl Default for DimensionRange { + fn default() -> Self { + Self { + start: Bound::Unbounded, + end: Bound::Unbounded, + } + } +} + impl DimensionRange { /// Returns this range's dimension if the range represents a single /// dimension. @@ -1117,6 +1126,97 @@ impl IntoValue for Lp { } } +impl ScreenScale for Edges +where + U: ScreenScale, +{ + type Lp = Edges; + type Px = Edges; + type UPx = Edges; + + fn into_px(self, scale: Fraction) -> Self::Px { + Edges { + left: self.left.into_px(scale), + top: self.top.into_px(scale), + right: self.right.into_px(scale), + bottom: self.bottom.into_px(scale), + } + } + + fn from_px(px: Self::Px, scale: Fraction) -> Self { + Self { + left: U::from_px(px.left, scale), + top: U::from_px(px.top, scale), + right: U::from_px(px.right, scale), + bottom: U::from_px(px.bottom, scale), + } + } + + fn into_upx(self, scale: Fraction) -> Self::UPx { + Edges { + left: self.left.into_upx(scale), + top: self.top.into_upx(scale), + right: self.right.into_upx(scale), + bottom: self.bottom.into_upx(scale), + } + } + + fn from_upx(px: Self::UPx, scale: Fraction) -> Self { + Self { + left: U::from_upx(px.left, scale), + top: U::from_upx(px.top, scale), + right: U::from_upx(px.right, scale), + bottom: U::from_upx(px.bottom, scale), + } + } + + fn into_lp(self, scale: Fraction) -> Self::Lp { + Edges { + left: self.left.into_lp(scale), + top: self.top.into_lp(scale), + right: self.right.into_lp(scale), + bottom: self.bottom.into_lp(scale), + } + } + + fn from_lp(lp: Self::Lp, scale: Fraction) -> Self { + Self { + left: U::from_lp(lp.left, scale), + top: U::from_lp(lp.top, scale), + right: U::from_lp(lp.right, scale), + bottom: U::from_lp(lp.bottom, scale), + } + } +} + +impl Add for Edges +where + U: Add, +{ + type Output = Edges; + + fn add(self, rhs: Self) -> Self::Output { + Edges { + left: self.left + rhs.left, + top: self.top + rhs.top, + right: self.right + rhs.right, + bottom: self.bottom + rhs.bottom, + } + } +} + +impl AddAssign> for Edges +where + U: AddAssign, +{ + fn add_assign(&mut self, rhs: Edges) { + self.left += rhs.left; + self.top += rhs.top; + self.right += rhs.right; + self.bottom += rhs.bottom; + } +} + /// A set of light and dark [`Theme`]s. #[derive(Clone, Debug, PartialEq)] pub struct ThemePair { diff --git a/src/widget.rs b/src/widget.rs index a85d0e1..81292f1 100644 --- a/src/widget.rs +++ b/src/widget.rs @@ -199,7 +199,11 @@ pub trait Widget: Send + UnwindSafe + Debug + 'static { /// Returns a reference to a single child widget if this widget is a widget /// that primarily wraps a single other widget to customize its behavior. #[must_use] - fn wraps(&mut self) -> Option<&WidgetInstance> { + #[allow(unused_variables)] + fn root_behavior( + &mut self, + context: &mut EventContext<'_, '_>, + ) -> Option<(RootBehavior, &WidgetInstance)> { None } } @@ -213,6 +217,23 @@ where } } +/// A behavior that should be applied to a root widget. +#[derive(Debug, Clone, Copy)] +pub enum RootBehavior { + /// This widget does not care about root behaviors, and its child should be + /// allowed to specify a behavior. + PassThrough, + /// This widget will try to expand to fill the window. + Expand, + /// This widget will measure its contents to fit its child, but Gooey should + /// still stretch this widget to fill the window. + Align, + /// This widget adjusts its child layout with padding. + Pad(Edges), + /// This widget changes the size of its child. + Resize(Size), +} + /// The layout of a [wrapped](WrapperWidget) child widget. #[derive(Clone, Copy, Debug)] pub struct WrappedLayout { @@ -254,6 +275,13 @@ pub trait WrapperWidget: Debug + Send + UnwindSafe + 'static { /// Returns the child widget. fn child_mut(&mut self) -> &mut WidgetRef; + /// Returns the behavior this widget should apply when positioned at the + /// root of the window. + #[allow(unused_variables)] + fn root_behavior(&mut self, context: &mut EventContext<'_, '_>) -> Option { + None + } + /// Draws the background of the widget. /// /// This is invoked before the wrapped widget is drawn. @@ -472,8 +500,11 @@ impl Widget for T where T: WrapperWidget, { - fn wraps(&mut self) -> Option<&WidgetInstance> { - Some(self.child_mut().widget()) + fn root_behavior( + &mut self, + context: &mut EventContext<'_, '_>, + ) -> Option<(RootBehavior, &WidgetInstance)> { + T::root_behavior(self, context).map(|behavior| (behavior, T::child_mut(self).widget())) } fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_, '_>) { diff --git a/src/widgets/align.rs b/src/widgets/align.rs index 71fa7c2..eb5da22 100644 --- a/src/widgets/align.rs +++ b/src/widgets/align.rs @@ -3,10 +3,10 @@ use std::fmt::Debug; use kludgine::figures::units::UPx; use kludgine::figures::{Fraction, IntoSigned, Point, Rect, ScreenScale, Size}; -use crate::context::{AsEventContext, LayoutContext}; +use crate::context::{AsEventContext, EventContext, LayoutContext}; use crate::styles::{Edges, FlexibleDimension}; use crate::value::{IntoValue, Value}; -use crate::widget::{MakeWidget, WidgetRef, WrappedLayout, WrapperWidget}; +use crate::widget::{MakeWidget, RootBehavior, WidgetRef, WrappedLayout, WrapperWidget}; use crate::ConstraintLimit; /// A widget aligns its contents to its container's boundaries. @@ -178,6 +178,10 @@ impl WrapperWidget for Align { &mut self.child } + fn root_behavior(&mut self, _context: &mut EventContext<'_, '_>) -> Option { + Some(RootBehavior::Align) + } + fn layout_child( &mut self, available_space: Size, diff --git a/src/widgets/container.rs b/src/widgets/container.rs index 27ea465..d75cbcd 100644 --- a/src/widgets/container.rs +++ b/src/widgets/container.rs @@ -4,11 +4,11 @@ use kludgine::figures::units::Px; use kludgine::figures::{IntoUnsigned, Point, Rect, ScreenScale, Size}; use kludgine::Color; -use crate::context::{GraphicsContext, LayoutContext, WidgetContext}; +use crate::context::{EventContext, GraphicsContext, LayoutContext, WidgetContext}; use crate::styles::components::{IntrinsicPadding, SurfaceColor}; use crate::styles::{Component, ContainerLevel, Dimension, Edges, RequireInvalidation, Styles}; use crate::value::{IntoValue, Value}; -use crate::widget::{MakeWidget, WidgetRef, WrappedLayout, WrapperWidget}; +use crate::widget::{MakeWidget, RootBehavior, WidgetRef, WrappedLayout, WrapperWidget}; use crate::ConstraintLimit; /// A visual container widget, optionally applying padding and a background @@ -159,6 +159,16 @@ impl WrapperWidget for Container { &mut self.child } + fn root_behavior(&mut self, _context: &mut EventContext<'_, '_>) -> Option { + Some( + self.padding + .as_ref() + .map_or(RootBehavior::PassThrough, |padding| { + RootBehavior::Pad(padding.get()) + }), + ) + } + fn background_color(&mut self, context: &WidgetContext<'_, '_>) -> Option { let background = match self.background.get() { ContainerBackground::Color(color) => EffectiveBackground::Color(color), diff --git a/src/widgets/expand.rs b/src/widgets/expand.rs index 7746b26..83bf4fc 100644 --- a/src/widgets/expand.rs +++ b/src/widgets/expand.rs @@ -1,7 +1,7 @@ use kludgine::figures::{IntoSigned, Size}; -use crate::context::{AsEventContext, LayoutContext}; -use crate::widget::{MakeWidget, WidgetRef, WrappedLayout, WrapperWidget}; +use crate::context::{AsEventContext, EventContext, LayoutContext}; +use crate::widget::{MakeWidget, RootBehavior, WidgetRef, WrappedLayout, WrapperWidget}; use crate::widgets::Space; use crate::ConstraintLimit; @@ -97,6 +97,10 @@ impl WrapperWidget for Expand { &mut self.child } + fn root_behavior(&mut self, _context: &mut EventContext<'_, '_>) -> Option { + Some(RootBehavior::Expand) + } + fn layout_child( &mut self, available_space: Size, diff --git a/src/widgets/resize.rs b/src/widgets/resize.rs index 89247e0..99d641b 100644 --- a/src/widgets/resize.rs +++ b/src/widgets/resize.rs @@ -1,8 +1,8 @@ use kludgine::figures::{Fraction, IntoSigned, ScreenScale, Size}; -use crate::context::{AsEventContext, LayoutContext}; +use crate::context::{AsEventContext, EventContext, LayoutContext}; use crate::styles::DimensionRange; -use crate::widget::{MakeWidget, WidgetRef, WrappedLayout, WrapperWidget}; +use crate::widget::{MakeWidget, RootBehavior, WidgetRef, WrappedLayout, WrapperWidget}; use crate::ConstraintLimit; /// A widget that resizes its contained widget to an explicit size. @@ -89,6 +89,10 @@ impl WrapperWidget for Resize { &mut self.child } + fn root_behavior(&mut self, _context: &mut EventContext<'_, '_>) -> Option { + Some(RootBehavior::Resize(Size::new(self.width, self.height))) + } + fn layout_child( &mut self, available_space: Size, diff --git a/src/window.rs b/src/window.rs index 43ee9b8..a5cd0d7 100644 --- a/src/window.rs +++ b/src/window.rs @@ -32,14 +32,13 @@ use crate::context::{ WidgetContext, }; use crate::graphics::Graphics; -use crate::styles::{FontFamilyList, ThemePair}; +use crate::styles::{Edges, FontFamilyList, ThemePair}; use crate::tree::Tree; use crate::utils::{IgnorePoison, ModifiersExt}; use crate::value::{Dynamic, DynamicReader, IntoDynamic, IntoValue, Value}; use crate::widget::{ - EventHandling, ManagedWidget, Widget, WidgetId, WidgetInstance, HANDLED, IGNORED, + EventHandling, ManagedWidget, RootBehavior, Widget, WidgetId, WidgetInstance, HANDLED, IGNORED, }; -use crate::widgets::{Expand, Resize}; use crate::window::sealed::WindowCommand; use crate::{initialize_tracing, ConstraintLimit, Run}; @@ -417,62 +416,104 @@ where fn constrain_window_resizing( &mut self, resizable: bool, - window: &kludgine::app::Window<'_, WindowCommand>, + window: &mut RunningWindow<'_>, graphics: &mut kludgine::Graphics<'_>, - ) -> bool { + ) -> RootMode { let mut root_or_child = self.root.widget.clone(); - let mut is_expanded = false; + let mut root_mode = None; + let mut padding = Edges::::default(); + loop { - let mut widget = root_or_child.lock(); - if let Some(resize) = widget.downcast_ref::() { - let min_width = resize - .width - .minimum() - .map_or(Px::ZERO, |width| width.into_px(graphics.scale())); - let max_width = resize - .width - .maximum() - .map_or(Px::MAX, |width| width.into_px(graphics.scale())); - let min_height = resize - .height - .minimum() - .map_or(Px::ZERO, |height| height.into_px(graphics.scale())); - let max_height = resize - .height - .maximum() - .map_or(Px::MAX, |height| height.into_px(graphics.scale())); - - let new_min_size = (min_width > 0 || min_height > 0) - .then_some(Size::new(min_width, min_height).into_unsigned()); - - if new_min_size != self.min_inner_size && resizable { - window.set_min_inner_size(new_min_size); - self.min_inner_size = new_min_size; - } - let new_max_size = (max_width > 0 || max_height > 0) - .then_some(Size::new(max_width, max_height).into_unsigned()); - - if new_max_size != self.max_inner_size && resizable { - window.set_max_inner_size(new_max_size); - } - self.max_inner_size = new_max_size; - } else if widget.downcast_ref::().is_some() { - is_expanded = true; - } - - if let Some(wraps) = widget.as_widget().wraps().cloned() { - drop(widget); - - root_or_child = wraps; - } else { + let Some(managed) = self.root.tree.widget(root_or_child.id()) else { break; + }; + + let mut context = EventContext::new( + WidgetContext::new( + managed, + &self.redraw_status, + &self.current_theme, + window, + self.theme_mode.get(), + &mut self.cursor, + ), + graphics, + ); + let mut widget = root_or_child.lock(); + match widget.as_widget().root_behavior(&mut context) { + Some((behavior, child)) => { + let child = child.clone(); + match behavior { + RootBehavior::PassThrough => {} + RootBehavior::Expand => { + root_mode = root_mode.or(Some(RootMode::Expand)); + } + RootBehavior::Align => { + root_mode = root_mode.or(Some(RootMode::Align)); + } + RootBehavior::Pad(edges) => { + padding += edges.into_px(context.kludgine.scale()); + } + RootBehavior::Resize(range) => { + let padding = padding.size(); + let min_width = range + .width + .minimum() + .map_or(Px::ZERO, |width| width.into_px(context.kludgine.scale())) + .saturating_add(padding.width); + let max_width = range + .width + .maximum() + .map_or(Px::MAX, |width| width.into_px(context.kludgine.scale())) + .saturating_add(padding.width); + let min_height = range + .height + .minimum() + .map_or(Px::ZERO, |height| height.into_px(context.kludgine.scale())) + .saturating_add(padding.height); + let max_height = range + .height + .maximum() + .map_or(Px::MAX, |height| height.into_px(context.kludgine.scale())) + .saturating_add(padding.height); + + let new_min_size = (min_width > 0 || min_height > 0) + .then_some(Size::new(min_width, min_height).into_unsigned()); + + if new_min_size != self.min_inner_size && resizable { + context.set_min_inner_size(new_min_size); + self.min_inner_size = new_min_size; + } + let new_max_size = (max_width > 0 || max_height > 0) + .then_some(Size::new(max_width, max_height).into_unsigned()); + + if new_max_size != self.max_inner_size && resizable { + context.set_max_inner_size(new_max_size); + } + self.max_inner_size = new_max_size; + + break; + } + } + drop(widget); + + root_or_child = child.clone(); + } + None => break, } } - is_expanded + root_mode.unwrap_or(RootMode::Fit) } } +#[derive(Clone, Copy, Eq, PartialEq)] +enum RootMode { + Fit, + Expand, + Align, +} + impl kludgine::app::WindowBehavior for GooeyWindow where T: WindowBehavior, @@ -592,10 +633,10 @@ where self.root.tree.new_frame(invalidations.iter().copied()); let resizable = window.winit().is_resizable(); - let is_expanded = self.constrain_window_resizing(resizable, &window, graphics); + let mut window = RunningWindow::new(window, &self.clipboard, &self.focused, &self.occluded); + let root_mode = self.constrain_window_resizing(resizable, &mut window, graphics); let graphics = self.contents.new_frame(graphics); - let mut window = RunningWindow::new(window, &self.clipboard, &self.focused, &self.occluded); let mut context = GraphicsContext { widget: WidgetContext::new( self.root.clone(), @@ -616,11 +657,17 @@ where layout_context.graphics.gfx.fill(background_color); } - let actual_size = layout_context.layout(if is_expanded { - window_size.map(ConstraintLimit::Fill) + 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) + }); + let actual_size = if root_mode == RootMode::Align { + window_size } else { - window_size.map(ConstraintLimit::SizeToFit) - }); + layout_size + }; let render_size = actual_size.min(window_size); if actual_size != window_size && !resizable { let mut new_size = actual_size;