diff --git a/Cargo.lock b/Cargo.lock index 52bf4e2..6d41179 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -855,7 +855,7 @@ checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" [[package]] name = "kludgine" version = "0.1.0" -source = "git+https://github.com/khonsulabs/kludgine#fd2078efb7212db0f07120b80440699b00ec2a2b" +source = "git+https://github.com/khonsulabs/kludgine#5d728e775b9bf64ac30e1e673c9971fc2184cb97" dependencies = [ "ahash", "alot", @@ -923,9 +923,9 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.4.10" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da2479e8c062e40bf0066ffa0bc823de0a9368974af99c9f6df941d2c231e03f" +checksum = "969488b55f8ac402214f3f5fd243ebb7206cf82de60d3172994707a4bcc2b829" [[package]] name = "lock_api" @@ -1572,9 +1572,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.11.1" +version = "1.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a" +checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" [[package]] name = "smithay-client-toolkit" diff --git a/Cargo.toml b/Cargo.toml index 784adb7..4ab496e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,7 @@ tracing-subscriber = { version = "0.3", optional = true } # [patch."https://github.com/khonsulabs/kludgine"] -# kludgine = { path = "../kludgine2" } +# kludgine = { path = "../kludgine" } # [patch."https://github.com/khonsulabs/appit"] # appit = { path = "../appit" } # [patch."https://github.com/khonsulabs/figures"] diff --git a/examples/login.rs b/examples/login.rs index 96cf122..3075628 100644 --- a/examples/login.rs +++ b/examples/login.rs @@ -43,8 +43,7 @@ fn main() -> gooey::Result { ); Resize::width( - // TODO We need a min/max range for the Resize widget - Lp::points(400), + Lp::points(300)..Lp::points(600), Stack::rows(username_row.and(password_row).and(buttons)), ) .centered() diff --git a/examples/style.rs b/examples/style.rs index f47a0e6..36c2f8d 100644 --- a/examples/style.rs +++ b/examples/style.rs @@ -1,5 +1,5 @@ use gooey::styles::components::TextColor; -use gooey::widget::{MakeWidget, Widget}; +use gooey::widget::MakeWidget; use gooey::widgets::stack::Stack; use gooey::widgets::{Button, Style}; use gooey::Run; @@ -12,6 +12,6 @@ fn main() -> gooey::Result { } /// Creating reusable style helpers that work with any Widget is straightfoward -fn red_text(w: impl Widget) -> Style { +fn red_text(w: impl MakeWidget) -> Style { w.with(&TextColor, Color::RED) } diff --git a/src/styles.rs b/src/styles.rs index ceb92e2..266dca1 100644 --- a/src/styles.rs +++ b/src/styles.rs @@ -2,7 +2,9 @@ use std::borrow::Cow; use std::collections::{hash_map, HashMap}; -use std::ops::Add; +use std::ops::{ + Add, Bound, Range, RangeFrom, RangeFull, RangeInclusive, RangeTo, RangeToInclusive, +}; use std::sync::Arc; use crate::animation::{EasingFunction, ZeroToOne}; @@ -172,8 +174,8 @@ use std::any::Any; use std::fmt::Debug; use std::panic::{RefUnwindSafe, UnwindSafe}; -use kludgine::figures::units::{Lp, Px}; -use kludgine::figures::{ScreenScale, Size}; +use kludgine::figures::units::{Lp, Px, UPx}; +use kludgine::figures::{Fraction, IntoUnsigned, ScreenScale, Size}; use kludgine::Color; /// A value of a style component. @@ -183,6 +185,8 @@ pub enum Component { Color(Color), /// A single-dimension measurement. Dimension(Dimension), + /// A single-dimension measurement. + DimensionRange(DimensionRange), /// A percentage between 0.0 and 1.0. Percent(ZeroToOne), /// A custom component type. @@ -302,7 +306,7 @@ impl From for FlexibleDimension { } /// A 1-dimensional measurement. -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, Eq, PartialEq)] pub enum Dimension { /// Physical Pixels Px(Px), @@ -360,6 +364,158 @@ impl ScreenScale for Dimension { } } +/// A range of [`Dimension`]s. +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub struct DimensionRange { + /// The start bound of the range. + pub start: Bound, + /// The end bound of the range. + pub end: Bound, +} + +impl DimensionRange { + /// Returns this range's dimension if the range represents a single + /// dimension. + #[must_use] + pub fn exact_dimension(&self) -> Option { + match (self.start, self.end) { + (Bound::Excluded(start), Bound::Included(end)) if start == end => Some(start), + _ => None, + } + } + + /// Clamps `size` to the dimensions of this range, converting to unsigned + /// pixels in the process. + #[must_use] + pub fn clamp(&self, mut size: UPx, scale: Fraction) -> UPx { + if let Some(min) = self.minimum() { + size = size.max(min.into_px(scale).into_unsigned()); + } + if let Some(max) = self.maximum() { + size = size.min(max.into_px(scale).into_unsigned()); + } + size + } + + /// Returns the minimum measurement, if the start is bounded. + #[must_use] + pub fn minimum(&self) -> Option { + match self.start { + Bound::Unbounded => None, + Bound::Excluded(Dimension::Lp(lp)) => Some(Dimension::Lp(lp + 1)), + Bound::Excluded(Dimension::Px(px)) => Some(Dimension::Px(px + 1)), + Bound::Included(value) => Some(value), + } + } + + /// Returns the maximum measurement, if the end is bounded. + #[must_use] + pub fn maximum(&self) -> Option { + match self.end { + Bound::Unbounded => None, + Bound::Excluded(Dimension::Lp(lp)) => Some(Dimension::Lp(lp - 1)), + Bound::Excluded(Dimension::Px(px)) => Some(Dimension::Px(px - 1)), + Bound::Included(value) => Some(value), + } + } +} + +impl From for DimensionRange +where + T: Into, +{ + fn from(value: T) -> Self { + let dimension = value.into(); + Self::from(dimension..=dimension) + } +} + +impl From> for DimensionRange +where + T: Into, +{ + fn from(value: Range) -> Self { + Self { + start: Bound::Included(value.start.into()), + end: Bound::Excluded(value.end.into()), + } + } +} + +impl From for DimensionRange { + fn from(_: RangeFull) -> Self { + Self { + start: Bound::Unbounded, + end: Bound::Unbounded, + } + } +} + +impl From> for DimensionRange +where + T: Into + Clone, +{ + fn from(value: RangeInclusive) -> Self { + Self { + start: Bound::Included(value.start().clone().into()), + end: Bound::Excluded(value.end().clone().into()), + } + } +} + +impl From> for DimensionRange +where + T: Into, +{ + fn from(value: RangeFrom) -> Self { + Self { + start: Bound::Included(value.start.into()), + end: Bound::Unbounded, + } + } +} + +impl From> for DimensionRange +where + T: Into, +{ + fn from(value: RangeTo) -> Self { + Self { + start: Bound::Unbounded, + end: Bound::Excluded(value.end.into()), + } + } +} + +impl From> for DimensionRange +where + T: Into, +{ + fn from(value: RangeToInclusive) -> Self { + Self { + start: Bound::Unbounded, + end: Bound::Included(value.end.into()), + } + } +} + +impl From for Component { + fn from(value: DimensionRange) -> Self { + Component::DimensionRange(value) + } +} + +impl TryFrom for DimensionRange { + type Error = Component; + + fn try_from(value: Component) -> Result { + match value { + Component::DimensionRange(value) => Ok(value), + other => Err(other), + } + } +} + /// A custom component value. #[derive(Debug, Clone)] pub struct CustomComponent(Arc); diff --git a/src/widget.rs b/src/widget.rs index 6b9d3f1..9cf564c 100644 --- a/src/widget.rs +++ b/src/widget.rs @@ -156,6 +156,13 @@ pub trait Widget: Send + UnwindSafe + Debug + 'static { ) -> EventHandling { IGNORED } + + /// 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> { + None + } } impl Run for T @@ -170,7 +177,7 @@ where /// A [`Widget`] that contains a single child. pub trait WrapperWidget: Debug + Send + UnwindSafe + 'static { /// Returns the child widget. - fn child(&mut self) -> &mut WidgetRef; + fn child_mut(&mut self) -> &mut WidgetRef; /// Returns the rectangle that the child widget should occupy given /// `available_space`. @@ -180,7 +187,7 @@ pub trait WrapperWidget: Debug + Send + UnwindSafe + 'static { available_space: Size, context: &mut LayoutContext<'_, '_, '_, '_, '_>, ) -> Rect { - let child = self.child().mounted(&mut context.as_event_context()); + let child = self.child_mut().mounted(&mut context.as_event_context()); context .for_other(&child) @@ -312,8 +319,12 @@ impl Widget for T where T: WrapperWidget, { + fn wraps(&mut self) -> Option<&WidgetInstance> { + Some(self.child_mut().widget()) + } + fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_, '_>) { - let child = self.child().mounted(&mut context.as_event_context()); + let child = self.child_mut().mounted(&mut context.as_event_context()); context.for_other(&child).redraw(); } @@ -322,7 +333,7 @@ where available_space: Size, context: &mut LayoutContext<'_, '_, '_, '_, '_>, ) -> Size { - let child = self.child().mounted(&mut context.as_event_context()); + let child = self.child_mut().mounted(&mut context.as_event_context()); let layout = self.layout_child(available_space, context); context.set_child_layout(&child, layout); @@ -1121,6 +1132,15 @@ impl WidgetRef { }; widget.clone() } + + /// Returns the a reference to the underlying widget instance. + #[must_use] + pub fn widget(&self) -> &WidgetInstance { + match self { + WidgetRef::Unmounted(widget) => widget, + WidgetRef::Mounted(managed) => &managed.widget, + } + } } impl AsRef for WidgetRef { diff --git a/src/widgets/align.rs b/src/widgets/align.rs index ad5a223..0fe9f79 100644 --- a/src/widgets/align.rs +++ b/src/widgets/align.rs @@ -178,7 +178,7 @@ impl FrameInfo { } impl WrapperWidget for Align { - fn child(&mut self) -> &mut WidgetRef { + fn child_mut(&mut self) -> &mut WidgetRef { &mut self.child } diff --git a/src/widgets/expand.rs b/src/widgets/expand.rs index 990cd96..6894e38 100644 --- a/src/widgets/expand.rs +++ b/src/widgets/expand.rs @@ -63,7 +63,7 @@ impl Expand { } impl WrapperWidget for Expand { - fn child(&mut self) -> &mut WidgetRef { + fn child_mut(&mut self) -> &mut WidgetRef { &mut self.child } diff --git a/src/widgets/resize.rs b/src/widgets/resize.rs index fa558a4..af7b5ae 100644 --- a/src/widgets/resize.rs +++ b/src/widgets/resize.rs @@ -1,17 +1,17 @@ use kludgine::figures::{Fraction, IntoSigned, IntoUnsigned, Rect, ScreenScale, Size}; use crate::context::{AsEventContext, LayoutContext}; -use crate::styles::Dimension; +use crate::styles::DimensionRange; use crate::widget::{MakeWidget, WidgetRef, WrapperWidget}; use crate::ConstraintLimit; /// A widget that resizes its contained widget to an explicit size. #[derive(Debug)] pub struct Resize { - /// If present, the width to apply to the child widget. - pub width: Option, - /// If present, the height to apply to the child widget. - pub height: Option, + /// The range of allowed width for the child widget. + pub width: DimensionRange, + /// The range of allowed height for the child widget. + pub height: DimensionRange, child: WidgetRef, } @@ -26,38 +26,38 @@ impl Resize { #[must_use] pub fn to(size: Size, child: impl MakeWidget) -> Self where - T: Into, + T: Into, { Self { child: WidgetRef::new(child), - width: Some(size.width.into()), - height: Some(size.height.into()), + width: size.width.into(), + height: size.height.into(), } } /// Resizes `child`'s width to `width`. #[must_use] - pub fn width(width: impl Into, child: impl MakeWidget) -> Self { + pub fn width(width: impl Into, child: impl MakeWidget) -> Self { Self { child: WidgetRef::new(child), - width: Some(width.into()), - height: None, + width: width.into(), + height: DimensionRange::from(..), } } /// Resizes `child`'s height to `height`. #[must_use] - pub fn height(height: impl Into, child: impl MakeWidget) -> Self { + pub fn height(height: impl Into, child: impl MakeWidget) -> Self { Self { child: WidgetRef::new(child), - width: None, - height: Some(height.into()), + width: DimensionRange::from(..), + height: height.into(), } } } impl WrapperWidget for Resize { - fn child(&mut self) -> &mut WidgetRef { + fn child_mut(&mut self) -> &mut WidgetRef { &mut self.child } @@ -67,7 +67,9 @@ impl WrapperWidget for Resize { context: &mut LayoutContext<'_, '_, '_, '_, '_>, ) -> Rect { let child = self.child.mounted(&mut context.as_event_context()); - let size = if let (Some(width), Some(height)) = (self.width, self.height) { + let size = if let (Some(width), Some(height)) = + (self.width.exact_dimension(), self.height.exact_dimension()) + { Size::new( width.into_px(context.gfx.scale()).into_unsigned(), height.into_px(context.gfx.scale()).into_unsigned(), @@ -85,12 +87,13 @@ impl WrapperWidget for Resize { fn override_constraint( constraint: ConstraintLimit, - explicit: Option, + range: DimensionRange, scale: Fraction, ) -> ConstraintLimit { - if let Some(size) = explicit { - ConstraintLimit::Known(size.into_px(scale).into_unsigned()) - } else { - constraint + match constraint { + ConstraintLimit::Known(size) => ConstraintLimit::Known(range.clamp(size, scale)), + ConstraintLimit::ClippedAfter(clipped_after) => { + ConstraintLimit::ClippedAfter(range.clamp(clipped_after, scale)) + } } } diff --git a/src/widgets/stack.rs b/src/widgets/stack.rs index 264b657..8851163 100644 --- a/src/widgets/stack.rs +++ b/src/widgets/stack.rs @@ -1,7 +1,7 @@ //! A widget that combines a collection of [`Children`] widgets into one. // TODO on scale change, all `Lp` children need to resize -use std::ops::Deref; +use std::ops::{Bound, Deref}; use alot::{LotId, OrderedLots}; use kludgine::figures::units::{Lp, UPx}; @@ -95,14 +95,22 @@ impl Stack { ) } else if let Some((child, size)) = guard.downcast_ref::().and_then(|r| { - match self.layout.orientation.orientation { + let range = match self.layout.orientation.orientation { StackOrientation::Row => r.height, StackOrientation::Column => r.width, - } - .map(|size| (r.child().clone(), size)) + }; + range.minimum().map(|min| { + ( + r.child().clone(), + StackDimension::Measured { + min, + _max: range.end, + }, + ) + }) }) { - (child, StackDimension::Exact(size)) + (child, size) } else { ( WidgetRef::Unmounted(widget.clone()), @@ -260,7 +268,7 @@ pub enum StackOrientation { /// The strategy to use when laying a widget out inside of an [`Stack`]. #[derive(Debug, Clone, Copy)] -pub enum StackDimension { +enum StackDimension { /// Attempt to lay out the widget based on its contents. FitContent, /// Use a fractional amount of the available space. @@ -269,8 +277,13 @@ pub enum StackDimension { /// fractionally. weight: u8, }, - /// Use an exact measurement for this widget's size. - Exact(Dimension), + /// Use a range for this widget's size. + Measured { + /// The minimum size for the widget. + min: Dimension, + /// The optional maximum size for the widget. + _max: Bound, + }, } #[derive(Debug)] @@ -322,7 +335,7 @@ impl Layout { self.fractional.retain(|(measured, _)| *measured != id); self.total_weights -= u32::from(weight); } - StackDimension::Exact(size) => match size { + StackDimension::Measured { min, .. } => match min { Dimension::Px(pixels) => { self.allocated_space.0 -= pixels.into_unsigned(); } @@ -357,12 +370,12 @@ impl Layout { self.fractional.push((id, weight)); UPx(0) } - StackDimension::Exact(size) => { - match size { + StackDimension::Measured { min, .. } => { + match min { Dimension::Px(size) => self.allocated_space.0 += size.into_unsigned(), Dimension::Lp(size) => self.allocated_space.1 += size, } - size.into_px(scale).into_unsigned() + min.into_px(scale).into_unsigned() } }; self.layouts.insert( @@ -464,6 +477,7 @@ impl Deref for Layout { #[cfg(test)] mod tests { use std::cmp::Ordering; + use std::ops::Bound; use kludgine::figures::units::UPx; use kludgine::figures::{Fraction, IntoSigned, Size}; @@ -490,7 +504,10 @@ mod tests { } pub fn fixed_size(mut self, size: UPx) -> Self { - self.dimension = StackDimension::Exact(Dimension::Px(size.into_signed())); + self.dimension = StackDimension::Measured { + min: Dimension::Px(size.into_signed()), + _max: Bound::Unbounded, + }; self } diff --git a/src/widgets/style.rs b/src/widgets/style.rs index 546fe48..870e7c4 100644 --- a/src/widgets/style.rs +++ b/src/widgets/style.rs @@ -21,7 +21,7 @@ impl Style { } impl WrapperWidget for Style { - fn child(&mut self) -> &mut WidgetRef { + fn child_mut(&mut self) -> &mut WidgetRef { &mut self.child } diff --git a/src/window.rs b/src/window.rs index c751d1f..26ca088 100644 --- a/src/window.rs +++ b/src/window.rs @@ -10,14 +10,14 @@ use std::path::Path; use std::string::ToString; use std::sync::OnceLock; -use kludgine::app::winit::dpi::PhysicalPosition; +use kludgine::app::winit::dpi::{PhysicalPosition, PhysicalSize}; use kludgine::app::winit::event::{ DeviceId, ElementState, Ime, KeyEvent, MouseButton, MouseScrollDelta, TouchPhase, }; use kludgine::app::winit::keyboard::Key; use kludgine::app::WindowBehavior as _; -use kludgine::figures::units::Px; -use kludgine::figures::{IntoSigned, Point, Rect, Size}; +use kludgine::figures::units::{Px, UPx}; +use kludgine::figures::{IntoSigned, IntoUnsigned, Point, Rect, ScreenScale, Size}; use kludgine::render::Drawing; use kludgine::Kludgine; use tracing::Level; @@ -34,6 +34,7 @@ use crate::value::{Dynamic, IntoDynamic}; use crate::widget::{ EventHandling, ManagedWidget, Widget, WidgetId, WidgetInstance, HANDLED, IGNORED, }; +use crate::widgets::Resize; use crate::window::sealed::WindowCommand; use crate::{initialize_tracing, ConstraintLimit, Run}; @@ -248,6 +249,8 @@ struct GooeyWindow { occluded: Dynamic, focused: Dynamic, keyboard_activated: Option, + min_inner_size: Option>, + max_inner_size: Option>, } impl GooeyWindow @@ -337,6 +340,8 @@ where occluded, focused, keyboard_activated: None, + min_inner_size: None, + max_inner_size: None, } } @@ -348,6 +353,55 @@ where self.redraw_status.refresh_received(); graphics.reset_text_attributes(); self.root.tree.reset_render_order(); + + let resizable = window.winit().is_resizable(); + { + let mut root_or_child = self.root.widget.clone(); + loop { + let mut widget = root_or_child.lock(); + if let Some(resize) = widget.downcast_ref::() { + let min_width = resize + .width + .minimum() + .map_or(Px(0), |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(0), |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 { + 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; + break; + } else if let Some(wraps) = widget.as_widget().wraps().cloned() { + drop(widget); + + root_or_child = wraps; + } else { + break; + } + } + } + let graphics = self.contents.new_frame(graphics); let mut window = RunningWindow::new(window, &self.focused, &self.occluded); let mut context = GraphicsContext { @@ -361,6 +415,18 @@ where ConstraintLimit::ClippedAfter(window_size.height), )); let render_size = actual_size.min(window_size); + if render_size != window_size && !resizable { + let mut new_size = actual_size; + if let Some(min_size) = self.min_inner_size { + new_size = new_size.max(min_size); + } + if let Some(max_size) = self.max_inner_size { + new_size = new_size.min(max_size); + } + let _ = layout_context + .winit() + .request_inner_size(PhysicalSize::from(new_size)); + } self.root.set_layout(Rect::from(render_size.into_signed())); if self.initial_frame {