diff --git a/examples/counter.rs b/examples/counter.rs index 15256fa..56f79a0 100644 --- a/examples/counter.rs +++ b/examples/counter.rs @@ -1,13 +1,13 @@ use std::string::ToString; use gooey::value::Dynamic; -use gooey::widgets::{Button, Label, Scroll, Stack}; +use gooey::widgets::{Button, Label, Spacing, Stack}; use gooey::{widgets, Run}; fn main() -> gooey::Result { let counter = Dynamic::new(0i32); let label = counter.map_each(ToString::to_string); - Scroll::new(Stack::columns(widgets![ + Spacing::auto(Stack::columns(widgets![ Label::new(label), Button::new("+").on_click(counter.with_clone(|counter| { move |_| { diff --git a/src/lib.rs b/src/lib.rs index be8dd20..72a1a4b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,6 +14,8 @@ pub mod value; pub mod widget; pub mod widgets; pub mod window; +use std::ops::Sub; + pub use with_clone::WithClone; mod with_clone; @@ -44,6 +46,19 @@ impl ConstraintLimit { } } +impl Sub for ConstraintLimit { + type Output = Self; + + fn sub(self, rhs: UPx) -> Self::Output { + match self { + ConstraintLimit::Known(px) => ConstraintLimit::Known(px.saturating_sub(rhs)), + ConstraintLimit::ClippedAfter(px) => { + ConstraintLimit::ClippedAfter(px.saturating_sub(rhs)) + } + } + } +} + /// A result alias that defaults to the result type commonly used throughout /// this crate. pub type Result = std::result::Result; diff --git a/src/styles.rs b/src/styles.rs index 6c0714f..8311c4c 100644 --- a/src/styles.rs +++ b/src/styles.rs @@ -2,10 +2,12 @@ use std::borrow::Cow; use std::collections::{hash_map, HashMap}; +use std::ops::Add; use std::sync::Arc; use crate::names::Name; use crate::utils::Lazy; +use crate::value::{IntoValue, Value}; pub mod components; @@ -132,7 +134,7 @@ use std::fmt::Debug; use std::panic::{RefUnwindSafe, UnwindSafe}; use kludgine::figures::units::{Lp, Px}; -use kludgine::figures::ScreenScale; +use kludgine::figures::{ScreenScale, Size}; use kludgine::Color; /// A value of a style component. @@ -216,6 +218,39 @@ impl TryFrom for Lp { } } +/// A 1-dimensional measurement that may be automatically calculated. +#[derive(Debug, Clone, Copy)] +pub enum FlexibleDimension { + /// Automatically calculate this dimension. + Auto, + /// Use this dimension. + Dimension(Dimension), +} + +impl Default for FlexibleDimension { + fn default() -> Self { + Self::Dimension(Dimension::default()) + } +} + +impl From for FlexibleDimension { + fn from(dimension: Dimension) -> Self { + Self::Dimension(dimension) + } +} + +impl From for FlexibleDimension { + fn from(value: Px) -> Self { + Self::from(Dimension::from(value)) + } +} + +impl From for FlexibleDimension { + fn from(value: Lp) -> Self { + Self::from(Dimension::from(value)) + } +} + /// A 1-dimensional measurement. #[derive(Debug, Clone, Copy)] pub enum Dimension { @@ -453,3 +488,130 @@ impl NamedComponent for Cow<'_, ComponentName> { Cow::Borrowed(self) } } + +/// A type describing characteristics about the edges of a rectangle. +#[derive(Clone, Copy, Debug)] +pub struct Edges { + /// The left edge + pub left: T, + /// The right edge + pub right: T, + /// The top edge + pub top: T, + /// The bottom edge + pub bottom: T, +} + +impl Edges { + /// Returns the sum of the parts as a [`Size`]. + pub fn size(&self) -> Size + where + T: Add + Copy, + { + Size::new(self.left + self.right, self.top + self.bottom) + } +} + +impl Default for Edges +where + T: Default, +{ + fn default() -> Self { + Self { + left: T::default(), + right: T::default(), + top: T::default(), + bottom: Default::default(), + } + } +} + +impl Edges { + /// Updates `top` and returns self. + #[must_use] + pub fn with_top(mut self, top: impl Into) -> Self { + self.top = top.into(); + self + } + + /// Updates `bottom` and returns self. + #[must_use] + pub fn with_bottom(mut self, bottom: impl Into) -> Self { + self.bottom = bottom.into(); + self + } + + /// Updates `right` and returns self. + #[must_use] + pub fn with_right(mut self, right: impl Into) -> Self { + self.right = right.into(); + self + } + + /// Updates `left` and returns self. + #[must_use] + pub fn with_left(mut self, left: impl Into) -> Self { + self.left = left.into(); + self + } + + /// Updates left and right to be `horizontal` and returns self. + #[must_use] + pub fn with_horizontal(mut self, horizontal: impl Into) -> Self + where + T: Clone, + { + self.left = horizontal.into(); + self.right = self.left.clone(); + self + } + + /// Updates top and bottom to be `vertical` and returns self. + #[must_use] + pub fn with_vertical(mut self, vertical: impl Into) -> Self + where + T: Clone, + { + self.top = vertical.into(); + self.bottom = self.top.clone(); + self + } +} + +impl Edges { + /// Returns a new instance with `dimension` for every edge. + #[must_use] + pub fn uniform(dimension: D) -> Self + where + D: Into, + { + let dimension = dimension.into(); + Self::from(dimension) + } +} + +impl From for Edges +where + T: Clone, +{ + fn from(value: T) -> Self { + Self { + left: value.clone(), + right: value.clone(), + top: value.clone(), + bottom: value, + } + } +} + +impl IntoValue> for FlexibleDimension { + fn into_value(self) -> Value> { + Value::Constant(Edges::from(self)) + } +} + +impl IntoValue> for Dimension { + fn into_value(self) -> Value> { + Value::Constant(Edges::from(self)) + } +} diff --git a/src/styles/components.rs b/src/styles/components.rs index e5f47ad..580b4c2 100644 --- a/src/styles/components.rs +++ b/src/styles/components.rs @@ -82,7 +82,7 @@ impl ComponentDefinition for HighlightColor { /// /// This component is opt-in and does not automatically work for all widgets. To /// apply arbitrary, non-uniform padding around another widget, use a -/// [`Cell`](crate::widgets::Cell). +/// [`Spacing`](crate::widgets::Spacing). #[derive(Clone, Copy, Eq, PartialEq, Debug)] pub struct IntrinsicPadding; diff --git a/src/widgets.rs b/src/widgets.rs index 0404d1c..4cd6f6b 100644 --- a/src/widgets.rs +++ b/src/widgets.rs @@ -5,6 +5,7 @@ mod canvas; mod input; mod label; pub mod scroll; +mod spacing; pub mod stack; mod style; mod tilemap; @@ -14,6 +15,7 @@ pub use canvas::Canvas; pub use input::Input; pub use label::Label; pub use scroll::Scroll; +pub use spacing::Spacing; pub use stack::Stack; pub use style::Style; pub use tilemap::TileMap; diff --git a/src/widgets/spacing.rs b/src/widgets/spacing.rs new file mode 100644 index 0000000..2147ed8 --- /dev/null +++ b/src/widgets/spacing.rs @@ -0,0 +1,181 @@ +use std::fmt::Debug; + +use kludgine::figures::units::UPx; +use kludgine::figures::{Fraction, IntoSigned, IntoUnsigned, Point, Rect, ScreenScale, Size}; + +use crate::context::{AsEventContext, EventContext, GraphicsContext}; +use crate::styles::{Edges, FlexibleDimension}; +use crate::value::{IntoValue, Value}; +use crate::widget::{MakeWidget, ManagedWidget, Widget, WidgetInstance}; +use crate::ConstraintLimit; + +/// A widget that provides spacing (padding) around its contents. +#[derive(Debug)] +pub struct Spacing { + child: WidgetInstance, + mounted: Option, + edges: Value>, +} + +impl Spacing { + /// Returns a new spacing widget containing `widget`, surrounding it with + /// `margin`. + pub fn new(margin: impl IntoValue>, widget: impl MakeWidget) -> Self { + Self { + child: widget.make_widget(), + mounted: None, + edges: margin.into_value(), + } + } + + /// Returns a new spacing widget that centers `widget` vertically and + /// horizontally. + pub fn auto(widget: impl MakeWidget) -> Self { + Self::new(FlexibleDimension::Auto, widget) + } + + fn child(&mut self, context: &mut EventContext<'_, '_>) -> ManagedWidget { + if self.mounted.is_none() { + self.mounted = Some(context.push_child(self.child.clone())); + } + self.mounted.as_ref().expect("always initialized").clone() + } + + fn measure( + &mut self, + available_space: Size, + context: &mut GraphicsContext<'_, '_, '_, '_, '_>, + ) -> Layout { + let margin = self.edges.get(); + let vertical = FrameInfo::new(context.graphics.scale(), margin.top, margin.bottom); + let horizontal = FrameInfo::new(context.graphics.scale(), margin.left, margin.right); + + let content_available = Size::new( + horizontal.child_constraint(available_space.width), + vertical.child_constraint(available_space.height), + ); + + let child = self.child(&mut context.as_event_context()); + let content_size = context.for_other(&child).measure(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); + + Layout { + margin: Edges { + left, + right, + top, + bottom, + }, + content: Size::new(width, height), + } + } +} + +struct FrameInfo { + a: Option, + b: Option, +} + +impl FrameInfo { + fn new(scale: Fraction, a: FlexibleDimension, b: FlexibleDimension) -> Self { + let a = match a { + FlexibleDimension::Auto => None, + FlexibleDimension::Dimension(dimension) => { + Some(dimension.into_px(scale).into_unsigned()) + } + }; + let b = match b { + FlexibleDimension::Auto => None, + FlexibleDimension::Dimension(dimension) => { + Some(dimension.into_px(scale).into_unsigned()) + } + }; + Self { a, b } + } + + fn child_constraint(&self, available: ConstraintLimit) -> ConstraintLimit { + match (self.a, self.b) { + (Some(a), Some(b)) => available - (a + b), + // If we have at least one auto-measurement, force the constraint + // into ClippedAfter mode to make the widget attempt to size the + // content to fit. + (Some(one), None) | (None, Some(one)) => { + ConstraintLimit::ClippedAfter(available.max() - one) + } + (None, None) => ConstraintLimit::ClippedAfter(available.max()), + } + } + + fn measure(&self, available: ConstraintLimit, content: UPx) -> (UPx, UPx, UPx) { + match available { + ConstraintLimit::Known(size) => { + let remaining = size - content; + let (a, b) = match (self.a, self.b) { + (Some(a), Some(b)) => (a, b), + (Some(a), None) => (a, remaining - a), + (None, Some(b)) => (remaining - b, b), + (None, None) => { + let a = remaining / 2; + let b = remaining - a; + (a, b) + } + }; + + (a, b, size - a - b) + } + ConstraintLimit::ClippedAfter(_) => ( + self.a.unwrap_or_default(), + self.b.unwrap_or_default(), + content, + ), + } + } +} + +impl Widget for Spacing { + fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_, '_>) { + let layout = self.measure( + Size::new( + ConstraintLimit::Known(context.graphics.size().width), + ConstraintLimit::Known(context.graphics.size().height), + ), + context, + ); + let child = self.child(&mut context.as_event_context()); + context + .for_child( + &child, + Rect::new( + Point::new( + layout.margin.left.into_signed(), + layout.margin.top.into_signed(), + ), + layout.content.into_signed(), + ), + ) + .redraw(); + } + + fn measure( + &mut self, + available_space: Size, + context: &mut GraphicsContext<'_, '_, '_, '_, '_>, + ) -> Size { + self.measure(available_space, context) + .size() + .into_unsigned() + } +} + +struct Layout { + margin: Edges, + content: Size, +} + +impl Layout { + pub fn size(&self) -> Size { + self.margin.size() + } +}