diff --git a/examples/login.rs b/examples/login.rs index 51af383..96cf122 100644 --- a/examples/login.rs +++ b/examples/login.rs @@ -13,46 +13,39 @@ fn main() -> gooey::Result { let valid = (&username, &password).map_each(|(username, password)| validate(username, password)); + // TODO this should be a grid layout to ensure proper visual alignment. + let username_row = Stack::columns( + Label::new("Username").and(Input::new(username.clone()).fit_horizontally().expand()), + ); + + let password_row = Stack::columns(Label::new("Password").and( + // TODO secure input + Input::new(password.clone()).fit_horizontally().expand(), + )); + + let buttons = Stack::columns( + Button::new("Cancel") + .on_click(|_| { + eprintln!("Login cancelled"); + exit(0) + }) + .into_escape() + .and(Expand::empty()) + .and( + Button::new("Log In") + .enabled(valid) + .on_click(move |_| { + println!("Welcome, {}", username.get()); + exit(0); + }) + .into_default(), + ), + ); + Resize::width( // TODO We need a min/max range for the Resize widget Lp::points(400), - Stack::rows( - Stack::columns( - Label::new("Username").and( - Input::new(username.clone()) - .centered() - .fit_horizontally() - .expand(), - ), - ) - .and(Stack::columns( - Label::new("Password").and( - // TODO secure input - Input::new(password.clone()) - .centered() - .fit_horizontally() - .expand(), - ), - )) - .and(Stack::columns( - Button::new("Cancel") - .on_click(|_| { - eprintln!("Login cancelled"); - exit(0) - }) - .into_escape() - .and(Expand::empty()) - .and( - Button::new("Log In") - .enabled(valid) - .on_click(move |_| { - println!("Welcome, {}", username.get()); - exit(0); - }) - .into_default(), - ), - )), - ), + Stack::rows(username_row.and(password_row).and(buttons)), ) .centered() .expand() diff --git a/src/styles.rs b/src/styles.rs index 573e927..39a3db7 100644 --- a/src/styles.rs +++ b/src/styles.rs @@ -235,9 +235,14 @@ pub enum FlexibleDimension { Dimension(Dimension), } +impl FlexibleDimension { + /// A dimension of 0 pixels. + pub const ZERO: Self = Self::Dimension(Dimension::ZERO); +} + impl Default for FlexibleDimension { fn default() -> Self { - Self::Dimension(Dimension::default()) + Self::ZERO } } @@ -268,9 +273,14 @@ pub enum Dimension { Lp(Lp), } +impl Dimension { + /// A dimension of 0 pixels. + pub const ZERO: Self = Self::Px(Px(0)); +} + impl Default for Dimension { fn default() -> Self { - Self::Px(Px(0)) + Self::ZERO } } diff --git a/src/value.rs b/src/value.rs index b93a540..ce5a776 100644 --- a/src/value.rs +++ b/src/value.rs @@ -117,6 +117,32 @@ impl Dynamic { self.0.get().value } + /// Returns the currently stored value, replacing the current contents with + /// `T::default()`. + #[must_use] + pub fn take(&self) -> T + where + T: Default, + { + std::mem::take(&mut self.lock()) + } + + /// Checks if the currently stored value is different than `T::default()`, + /// and if so, returns `Some(self.take())`. + #[must_use] + pub fn take_if_not_default(&self) -> Option + where + T: Default + PartialEq, + { + let default = T::default(); + let mut guard = self.lock(); + if *guard == default { + None + } else { + Some(std::mem::replace(&mut guard, default)) + } + } + /// Replaces the contents with `new_value`, returning the previous contents. /// Before returning from this function, all observers will be notified that /// the contents have been updated. diff --git a/src/widget.rs b/src/widget.rs index d389804..6b9d3f1 100644 --- a/src/widget.rs +++ b/src/widget.rs @@ -3,7 +3,7 @@ use std::any::Any; use std::clone::Clone; use std::fmt::Debug; -use std::ops::{ControlFlow, Deref}; +use std::ops::{ControlFlow, Deref, DerefMut}; use std::panic::UnwindSafe; use std::sync::atomic::{self, AtomicU64}; use std::sync::{Arc, Mutex, MutexGuard, PoisonError}; @@ -514,6 +514,36 @@ pub trait MakeWidget: Sized { Align::centered(self) } + /// Aligns `self` to the left. + fn align_left(self) -> Align { + self.centered().align_left() + } + + /// Aligns `self` to the right. + fn align_right(self) -> Align { + self.centered().align_right() + } + + /// Aligns `self` to the top. + fn align_top(self) -> Align { + self.centered().align_top() + } + + /// Aligns `self` to the bottom. + fn align_bottom(self) -> Align { + self.centered().align_bottom() + } + + /// Fits `self` horizontally within its parent. + fn fit_horizontally(self) -> Align { + self.centered().fit_horizontally() + } + + /// Fits `self` vertically within its parent. + fn fit_vertically(self) -> Align { + self.centered().fit_vertically() + } + /// Allows scrolling `self` both vertically and horizontally. #[must_use] fn scroll(self) -> Scroll { @@ -1002,6 +1032,14 @@ impl Children { self.ordered.push(widget.make_widget()); } + /// Inserts `widget` into the list at `index`. + pub fn insert(&mut self, index: usize, widget: W) + where + W: MakeWidget, + { + self.ordered.insert(index, widget.make_widget()); + } + /// Adds `widget` to self and returns the updated list. pub fn and(mut self, widget: W) -> Self where @@ -1022,6 +1060,14 @@ impl Children { pub fn is_empty(&self) -> bool { self.ordered.is_empty() } + + /// Truncates the collection of children to `length`. + /// + /// If this collection is already smaller or the same size as `length`, this + /// function does nothing. + pub fn truncate(&mut self, length: usize) { + self.ordered.truncate(length); + } } impl FromIterator for Children @@ -1043,6 +1089,12 @@ impl Deref for Children { } } +impl DerefMut for Children { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.ordered + } +} + /// A child widget #[derive(Debug, Clone)] pub enum WidgetRef { diff --git a/src/widgets/align.rs b/src/widgets/align.rs index 79969cb..8705988 100644 --- a/src/widgets/align.rs +++ b/src/widgets/align.rs @@ -4,7 +4,7 @@ use kludgine::figures::units::UPx; use kludgine::figures::{Fraction, IntoSigned, IntoUnsigned, Point, Rect, ScreenScale, Size}; use crate::context::{AsEventContext, LayoutContext}; -use crate::styles::{Dimension, Edges, FlexibleDimension}; +use crate::styles::{Edges, FlexibleDimension}; use crate::value::{IntoValue, Value}; use crate::widget::{MakeWidget, WidgetRef, WrapperWidget}; use crate::ConstraintLimit; @@ -32,22 +32,54 @@ impl Align { Self::new(FlexibleDimension::Auto, widget) } - /// Sets the left and right edges to 0 and returns self. + /// Sets the left edge of alignment to 0 and returns self. + #[must_use] + pub fn align_left(mut self) -> Self { + self.edges + .map_mut(|edges| edges.left = FlexibleDimension::ZERO); + self + } + + /// Sets the top edge of alignment to 0 and returns self. + #[must_use] + pub fn align_top(mut self) -> Self { + self.edges + .map_mut(|edges| edges.top = FlexibleDimension::ZERO); + self + } + + /// Sets the bottom edge of alignment to 0 and returns self. + #[must_use] + pub fn align_bottom(mut self) -> Self { + self.edges + .map_mut(|edges| edges.bottom = FlexibleDimension::ZERO); + self + } + + /// Sets the right edge of alignment to 0 and returns self. + #[must_use] + pub fn align_right(mut self) -> Self { + self.edges + .map_mut(|edges| edges.right = FlexibleDimension::ZERO); + self + } + + /// Sets the left and right edges of alignment to 0 and returns self. #[must_use] pub fn fit_horizontally(mut self) -> Self { self.edges.map_mut(|edges| { - edges.left = FlexibleDimension::Dimension(Dimension::default()); - edges.right = FlexibleDimension::Dimension(Dimension::default()); + edges.left = FlexibleDimension::ZERO; + edges.right = FlexibleDimension::ZERO; }); self } - /// Sets the top and bottom edges to 0 and returns self. + /// Sets the top and bottom edges of alignment to 0 and returns self. #[must_use] pub fn fit_vertically(mut self) -> Self { self.edges.map_mut(|edges| { - edges.top = FlexibleDimension::Dimension(Dimension::default()); - edges.bottom = FlexibleDimension::Dimension(Dimension::default()); + edges.top = FlexibleDimension::ZERO; + edges.bottom = FlexibleDimension::ZERO; }); self } diff --git a/src/widgets/scroll.rs b/src/widgets/scroll.rs index 91cefb2..8125d0d 100644 --- a/src/widgets/scroll.rs +++ b/src/widgets/scroll.rs @@ -186,7 +186,9 @@ impl Widget for Scroll { let (mut scroll, current_max_scroll) = self.constrain_scroll(); - let control_size = context.graphics.region().size; + let control_size = + Size::::new(available_space.width.max(), available_space.height.max()) + .into_signed(); let max_extents = Size::new( if self.enabled.x { ConstraintLimit::ClippedAfter(UPx::MAX - scroll.x.into_unsigned())