diff --git a/examples/buttons.rs b/examples/buttons.rs index 12ebd4b..a16d587 100644 --- a/examples/buttons.rs +++ b/examples/buttons.rs @@ -1,7 +1,6 @@ use gooey::value::Dynamic; use gooey::widget::MakeWidget; use gooey::widgets::button::ButtonKind; -use gooey::widgets::{Button, Checkbox}; use gooey::Run; fn main() -> gooey::Result { @@ -18,35 +17,39 @@ fn main() -> gooey::Result { clicked_label .clone() .and( - Button::new("Normal Button") + "Normal Button" + .into_button() .on_click( clicked_label.with_clone(|label| { move |_| label.set(String::from("Clicked Normal Button")) }), ) .and( - Button::new("Outline Button") + "Outline Button" + .into_button() .on_click(clicked_label.with_clone(|label| { move |_| label.set(String::from("Clicked Outline Button")) })) .kind(ButtonKind::Outline), ) .and( - Button::new("Transparent Button") + "Transparent Button" + .into_button() .on_click(clicked_label.with_clone(|label| { move |_| label.set(String::from("Clicked Transparent Button")) })) .kind(ButtonKind::Transparent), ) .and( - Button::new("Default Button") + "Default Button" + .into_button() .on_click(clicked_label.with_clone(|label| { move |_| label.set(String::from("Clicked Default Button")) })) .kind(default_button_style) .into_default(), ) - .and(Checkbox::new(default_is_outline, "Set Default to Outline")) + .and("Set Default to Outline".into_checkbox(default_is_outline)) .into_columns(), ) .into_rows() diff --git a/examples/checkbox.rs b/examples/checkbox.rs index 18a5df4..48195d9 100644 --- a/examples/checkbox.rs +++ b/examples/checkbox.rs @@ -1,14 +1,15 @@ use gooey::value::Dynamic; use gooey::widget::MakeWidget; -use gooey::widgets::checkbox::CheckboxState; -use gooey::widgets::Checkbox; +use gooey::widgets::checkbox::{Checkable, CheckboxState}; use gooey::Run; fn main() -> gooey::Result { let checkbox_state = Dynamic::new(CheckboxState::Checked); let label = checkbox_state.map_each(|state| format!("Check Me! Current: {state:?}")); - Checkbox::new(checkbox_state.clone(), label) + checkbox_state + .clone() + .into_checkbox(label) .and("Maybe".into_button().on_click(move |()| { checkbox_state.update(CheckboxState::Indeterminant); })) diff --git a/examples/focus.rs b/examples/focus.rs index f9ab395..df7df49 100644 --- a/examples/focus.rs +++ b/examples/focus.rs @@ -2,7 +2,7 @@ use gooey::value::Dynamic; use gooey::widget::MakeWidget; use gooey::widgets::input::InputValue; use gooey::widgets::slider::Slidable; -use gooey::widgets::{Checkbox, Custom}; +use gooey::widgets::Custom; use gooey::Run; use kludgine::figures::units::Lp; @@ -14,10 +14,7 @@ fn main() -> gooey::Result { .and(Dynamic::::default().slider_between(0_u8, 100_u8)) .and("Range Slider") .and(Dynamic::new(10..=30).slider_between(0_u8, 100_u8)) - .and(Checkbox::new( - allow_blur.clone(), - "Allow Custom Widget to Lose Focus", - )) + .and("Allow Custom Widget to Lose Focus".into_checkbox(allow_blur.clone())) .and( Custom::empty() .on_accept_focus(|_| true) diff --git a/examples/gameui.rs b/examples/gameui.rs index 50ae582..a9e2474 100644 --- a/examples/gameui.rs +++ b/examples/gameui.rs @@ -1,7 +1,6 @@ use gooey::value::Dynamic; use gooey::widget::{MakeWidget, HANDLED, IGNORED}; use gooey::widgets::input::InputValue; -use gooey::widgets::Space; use gooey::Run; use kludgine::app::winit::event::ElementState; use kludgine::app::winit::keyboard::{Key, NamedKey}; @@ -15,7 +14,7 @@ fn main() -> gooey::Result { .clone() .vertical_scroll() .expand() - .and(Space::colored(Color::RED).expand_weighted(2)) + .and(Color::RED.expand_weighted(2)) .into_columns() .expand() .and(chat_message.clone().into_input().on_key(move |input| { diff --git a/examples/progress.rs b/examples/progress.rs index 1fcddc8..e9dc8ba 100644 --- a/examples/progress.rs +++ b/examples/progress.rs @@ -2,7 +2,6 @@ use gooey::value::{Dynamic, MapEach}; use gooey::widget::MakeWidget; use gooey::widgets::progress::Progressable; use gooey::widgets::slider::Slidable; -use gooey::widgets::Checkbox; use gooey::Run; use kludgine::figures::units::Lp; use kludgine::figures::Size; @@ -17,7 +16,7 @@ fn main() -> gooey::Result { .clone() .slider() .and(progress.clone().progress_bar()) - .and(Checkbox::new(indeterminant.clone(), "Indeterminant")) + .and("Indeterminant".into_checkbox(indeterminant)) .into_rows() .fit_horizontally() .expand() diff --git a/examples/theme.rs b/examples/theme.rs index 3dbd5ac..c29fdb7 100644 --- a/examples/theme.rs +++ b/examples/theme.rs @@ -1,4 +1,3 @@ -use gooey::animation::ZeroToOne; use gooey::styles::components::{TextColor, WidgetBackground}; use gooey::styles::{ ColorScheme, ColorSource, ColorTheme, FixedTheme, SurfaceTheme, Theme, ThemePair, @@ -7,7 +6,6 @@ use gooey::value::{Dynamic, MapEach}; use gooey::widget::MakeWidget; use gooey::widgets::input::InputValue; use gooey::widgets::slider::Slidable; -use gooey::widgets::{Slider, Stack}; use gooey::window::ThemeMode; use gooey::Run; use kludgine::Color; @@ -100,13 +98,12 @@ fn color_editor( ( color, - Stack::rows( - label - .and(hue.slider_between(0., 360.)) - .and(hue_text.into_input()) - .and(Slider::::from_value(saturation)) - .and(saturation_text.into_input()), - ), + label + .and(hue.slider_between(0., 360.)) + .and(hue_text.into_input()) + .and(saturation.slider()) + .and(saturation_text.into_input()) + .into_rows(), ) } @@ -179,83 +176,80 @@ fn theme(theme: Dynamic, mode: ThemeMode) -> impl MakeWidget { fn surface_theme(theme: Dynamic) -> impl MakeWidget { let color = theme.map_each(|theme| theme.color); let on_color = theme.map_each(|theme| theme.on_color); - Stack::rows( - Stack::columns( - swatch(color.clone(), "Surface", on_color.clone()) - .and(swatch( - theme.map_each(|theme| theme.bright_color), - "Bright Surface", - on_color.clone(), - )) - .and(swatch( - theme.map_each(|theme| theme.dim_color), - "Dim Surface", - on_color.clone(), - )), - ) + + swatch(color.clone(), "Surface", on_color.clone()) + .and(swatch( + theme.map_each(|theme| theme.bright_color), + "Bright Surface", + on_color.clone(), + )) + .and(swatch( + theme.map_each(|theme| theme.dim_color), + "Dim Surface", + on_color.clone(), + )) + .into_columns() .contain() .expand() .and( - Stack::columns( - swatch( - theme.map_each(|theme| theme.lowest_container), - "Lowest Container", - on_color.clone(), - ) - .and(swatch( - theme.map_each(|theme| theme.low_container), - "Low Container", - on_color.clone(), - )) - .and(swatch( - theme.map_each(|theme| theme.container), - "Container", - on_color.clone(), - )) - .and(swatch( - theme.map_each(|theme| theme.high_container), - "High Container", - on_color.clone(), - )) - .and(swatch( - theme.map_each(|theme| theme.highest_container), - "Highest Container", - on_color.clone(), - )), + swatch( + theme.map_each(|theme| theme.lowest_container), + "Lowest Container", + on_color.clone(), ) + .and(swatch( + theme.map_each(|theme| theme.low_container), + "Low Container", + on_color.clone(), + )) + .and(swatch( + theme.map_each(|theme| theme.container), + "Container", + on_color.clone(), + )) + .and(swatch( + theme.map_each(|theme| theme.high_container), + "High Container", + on_color.clone(), + )) + .and(swatch( + theme.map_each(|theme| theme.highest_container), + "Highest Container", + on_color.clone(), + )) + .into_columns() .contain() .expand(), ) .and( - Stack::columns( - swatch(on_color.clone(), "On Surface", color.clone()) - .and(swatch( - theme.map_each(|theme| theme.on_color_variant), - "On Color Variant", - color.clone(), - )) - .and(swatch( - theme.map_each(|theme| theme.outline), - "Outline", - color.clone(), - )) - .and(swatch( - theme.map_each(|theme| theme.outline_variant), - "Outline Variant", - color, - )) - .and(swatch( - theme.map_each(|theme| theme.opaque_widget), - "Opaque Widget", - on_color, - )), - ) - .contain() - .expand(), - ), - ) - .contain() - .expand() + swatch(on_color.clone(), "On Surface", color.clone()) + .and(swatch( + theme.map_each(|theme| theme.on_color_variant), + "On Color Variant", + color.clone(), + )) + .and(swatch( + theme.map_each(|theme| theme.outline), + "Outline", + color.clone(), + )) + .and(swatch( + theme.map_each(|theme| theme.outline_variant), + "Outline Variant", + color, + )) + .and(swatch( + theme.map_each(|theme| theme.opaque_widget), + "Opaque Widget", + on_color, + )) + .into_columns() + .contain() + .expand(), + ) + .into_rows() + .contain() + .expand() } fn color_theme(theme: Dynamic, label: &str) -> impl MakeWidget { @@ -265,36 +259,36 @@ fn color_theme(theme: Dynamic, label: &str) -> impl MakeWidget { let on_color = theme.map_each(|theme| theme.on_color); let container = theme.map_each(|theme| theme.container); let on_container = theme.map_each(|theme| theme.on_container); - Stack::rows( - swatch(color.clone(), label, on_color.clone()) - .and(swatch( - dim_color.clone(), - &format!("{label} Dim"), - on_color.clone(), - )) - .and(swatch( - bright_color.clone(), - &format!("{label} bright"), - on_color.clone(), - )) - .and(swatch( - on_color.clone(), - &format!("On {label}"), - color.clone(), - )) - .and(swatch( - container.clone(), - &format!("{label} Container"), - on_container.clone(), - )) - .and(swatch( - on_container, - &format!("On {label} Container"), - container, - )), - ) - .contain() - .expand() + + swatch(color.clone(), label, on_color.clone()) + .and(swatch( + dim_color.clone(), + &format!("{label} Dim"), + on_color.clone(), + )) + .and(swatch( + bright_color.clone(), + &format!("{label} bright"), + on_color.clone(), + )) + .and(swatch( + on_color.clone(), + &format!("On {label}"), + color.clone(), + )) + .and(swatch( + container.clone(), + &format!("{label} Container"), + on_container.clone(), + )) + .and(swatch( + on_container, + &format!("On {label} Container"), + container, + )) + .into_rows() + .contain() + .expand() } fn swatch(background: Dynamic, label: &str, text: Dynamic) -> impl MakeWidget { diff --git a/src/animation.rs b/src/animation.rs index c096793..22a0330 100644 --- a/src/animation.rs +++ b/src/animation.rs @@ -293,6 +293,12 @@ pub trait AnimationTarget: Sized + Send + Sync { fn over(self, duration: Duration) -> Animation { Animation::new(self, duration) } + + /// Returns a pending animation that transitions to the target values after + /// no delay. + fn immediately(self) -> Animation { + self.over(Duration::ZERO) + } } /// The target of an [`Animate`] implementor. diff --git a/src/lib.rs b/src/lib.rs index a4c81e8..d7ecef2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -37,7 +37,8 @@ pub use self::tick::{InputState, Tick}; pub enum ConstraintLimit { /// The widget is expected to occupy a known size. Fill(UPx), - /// The widget is expected to resize itself to fit within the size provided. + /// The widget is expected to resize itself to fit its contents, trying to + /// stay within the size given. SizeToFit(UPx), } diff --git a/src/widget.rs b/src/widget.rs index 390f687..63bc0e1 100644 --- a/src/widget.rs +++ b/src/widget.rs @@ -25,9 +25,11 @@ use crate::styles::{ }; use crate::tree::Tree; use crate::utils::IgnorePoison; -use crate::value::{IntoValue, Value}; +use crate::value::{IntoDynamic, IntoValue, Value}; +use crate::widgets::checkbox::{Checkable, CheckboxState}; use crate::widgets::{ - Align, Button, Container, Expand, Resize, Scroll, Stack, Style, Themed, ThemedMode, + Align, Button, Checkbox, Container, Expand, Resize, Scroll, Space, Stack, Style, Themed, + ThemedMode, }; use crate::window::{RunningWindow, ThemeMode, Window, WindowBehavior}; use crate::{ConstraintLimit, Run}; @@ -743,11 +745,16 @@ pub trait MakeWidget: Sized { Resize::from_height(height, self) } - /// Returns this string as a clickable button. + /// Returns this widget as the contents of a clickable button. fn into_button(self) -> Button { Button::new(self) } + /// Returns this widget as the label of a Checkbox. + fn into_checkbox(self, value: impl IntoDynamic) -> Checkbox { + value.into_checkbox(self) + } + /// Aligns `self` to the center vertically and horizontally. #[must_use] fn centered(self) -> Align { @@ -875,6 +882,18 @@ impl MakeWidget for WidgetInstance { } } +impl MakeWidget for Color { + fn make_widget(self) -> WidgetInstance { + Space::colored(self).make_widget() + } +} + +impl MakeWidget for () { + fn make_widget(self) -> WidgetInstance { + Space::clear().make_widget() + } +} + /// A type that represents whether an event has been handled or ignored. pub type EventHandling = ControlFlow; diff --git a/src/widgets/checkbox.rs b/src/widgets/checkbox.rs index a85875a..e6464e7 100644 --- a/src/widgets/checkbox.rs +++ b/src/widgets/checkbox.rs @@ -95,6 +95,26 @@ impl From for CheckboxState { } } +impl From for Option { + fn from(value: CheckboxState) -> Self { + match value { + CheckboxState::Indeterminant => None, + CheckboxState::Unchecked => Some(false), + CheckboxState::Checked => Some(true), + } + } +} + +impl From> for CheckboxState { + fn from(value: Option) -> Self { + match value { + Some(true) => CheckboxState::Checked, + Some(false) => CheckboxState::Unchecked, + None => CheckboxState::Indeterminant, + } + } +} + impl TryFrom for bool { type Error = CheckboxToBoolError; @@ -127,6 +147,15 @@ impl IntoDynamic for Dynamic { } } +impl IntoDynamic for Dynamic> { + fn into_dynamic(self) -> Dynamic { + self.linked( + |bool| CheckboxState::from(*bool), + |tri_state: &CheckboxState| bool::try_from(*tri_state).ok(), + ) + } +} + /// An [`CheckboxState::Indeterminant`] was encountered when converting to a /// `bool`. #[derive(Debug, Clone, Copy, Eq, PartialEq)] @@ -222,3 +251,13 @@ impl WrapperWidget for CheckboxLabel { } } } + +/// A value that can be used as a checkbox. +pub trait Checkable: IntoDynamic + Sized { + /// Returns a new checkbox using `self` as the value and `label`. + fn into_checkbox(self, label: impl MakeWidget) -> Checkbox { + Checkbox::new(self.into_dynamic(), label) + } +} + +impl Checkable for T where T: IntoDynamic {} diff --git a/src/widgets/progress.rs b/src/widgets/progress.rs index 953ead3..92fca29 100644 --- a/src/widgets/progress.rs +++ b/src/widgets/progress.rs @@ -88,7 +88,7 @@ fn update_progress_bar( start.transition_to(ZeroToOne::ZERO), end.transition_to(ZeroToOne::ZERO), ) - .over(Duration::ZERO) + .immediately() .and_then( end.transition_to(ZeroToOne::new(0.66)) .over(Duration::from_millis(500))