diff --git a/.crate-docs.md b/.crate-docs.md index 36caa9349..a1998a7fa 100644 --- a/.crate-docs.md +++ b/.crate-docs.md @@ -16,16 +16,22 @@ reactive data models work, consider this example that displays a button that increments its own label: ```rust,ignore -// Create a dynamic usize. -let count = Dynamic::new(0_usize); - -// Create a new button with a label that is produced by mapping the contents -// of `count`. -Button::new(count.map_each(ToString::to_string)) - // Set the `on_click` callback to a closure that increments the counter. - .on_click(count.with_clone(|count| move |_| count.set(count.get() + 1))) - // Run the button as an an application. - .run() +fn main() -> gooey::Result { + // Create a dynamic usize. + let count = Dynamic::new(0_isize); + // Create a dynamic that contains `count.to_string()` + let count_label = count.map_each(ToString::to_string); + + // Create a new button whose text is our dynamic string. + count_label + .into_button() + // Set the `on_click` callback to a closure that increments the counter. + .on_click(count.with_clone(|count| move |_| count.set(count.get() + 1))) + // Position the button in the center + .centered() + // Run the application + .run() +} ``` [widget]: crate::widget::Widget @@ -33,7 +39,7 @@ Button::new(count.map_each(ToString::to_string)) [wgpu]: https://github.com/gfx-rs/wgpu [winit]: https://github.com/rust-windowing/winit [widgets]: mod@crate::widgets -[button-example]: https://github.com/khonsulabs/gooey/tree/main/examples/button.rs +[button-example]: https://github.com/khonsulabs/gooey/tree/main/examples/basic-button.rs ## Open-source Licenses diff --git a/.rustme/docs.md b/.rustme/docs.md index 7859fb646..25438703a 100644 --- a/.rustme/docs.md +++ b/.rustme/docs.md @@ -16,7 +16,7 @@ reactive data models work, consider this example that displays a button that increments its own label: ```rust,ignore -$../examples/button.rs:readme$ +$../examples/basic-button.rs:readme$ ``` [widget]: $widget$ @@ -24,4 +24,4 @@ $../examples/button.rs:readme$ [wgpu]: https://github.com/gfx-rs/wgpu [winit]: https://github.com/rust-windowing/winit [widgets]: $widgets$ -[button-example]: https://github.com/khonsulabs/gooey/tree/$ref-name$/examples/button.rs +[button-example]: https://github.com/khonsulabs/gooey/tree/$ref-name$/examples/basic-button.rs diff --git a/README.md b/README.md index 30bebeb50..ff46a568e 100644 --- a/README.md +++ b/README.md @@ -18,16 +18,22 @@ reactive data models work, consider this example that displays a button that increments its own label: ```rust,ignore -// Create a dynamic usize. -let count = Dynamic::new(0_usize); - -// Create a new button with a label that is produced by mapping the contents -// of `count`. -Button::new(count.map_each(ToString::to_string)) - // Set the `on_click` callback to a closure that increments the counter. - .on_click(count.with_clone(|count| move |_| count.set(count.get() + 1))) - // Run the button as an an application. - .run() +fn main() -> gooey::Result { + // Create a dynamic usize. + let count = Dynamic::new(0_isize); + // Create a dynamic that contains `count.to_string()` + let count_label = count.map_each(ToString::to_string); + + // Create a new button whose text is our dynamic string. + count_label + .into_button() + // Set the `on_click` callback to a closure that increments the counter. + .on_click(count.with_clone(|count| move |_| count.set(count.get() + 1))) + // Position the button in the center + .centered() + // Run the application + .run() +} ``` [widget]: https://gooey.rs/main/gooey/widget/trait.Widget.html @@ -35,7 +41,7 @@ Button::new(count.map_each(ToString::to_string)) [wgpu]: https://github.com/gfx-rs/wgpu [winit]: https://github.com/rust-windowing/winit [widgets]: https://gooey.rs/main/gooey/widgets/index.html -[button-example]: https://github.com/khonsulabs/gooey/tree/main/examples/button.rs +[button-example]: https://github.com/khonsulabs/gooey/tree/main/examples/basic-button.rs ## Open-source Licenses diff --git a/examples/animation.rs b/examples/animation.rs index 33a144408..4e7a1a2bd 100644 --- a/examples/animation.rs +++ b/examples/animation.rs @@ -1,9 +1,8 @@ use std::time::Duration; use gooey::animation::{AnimationHandle, AnimationTarget, IntoAnimate, Spawn}; -use gooey::value::Dynamic; +use gooey::value::{Dynamic, StringValue}; use gooey::widget::MakeWidget; -use gooey::widgets::{Button, Label, Stack}; use gooey::{Run, WithClone}; fn main() -> gooey::Result { @@ -18,13 +17,17 @@ fn main() -> gooey::Result { .on_complete(|| println!("Gooey animations are neat!")) .launch(); - Stack::columns( - Button::new("To 0") - .on_click(animate_to(&animation, &value, 0)) - .and(Label::new(label)) - .and(Button::new("To 100").on_click(animate_to(&animation, &value, 100))), - ) - .run() + "To 0" + .into_button() + .on_click(animate_to(&animation, &value, 0)) + .and(label) + .and( + "To 100" + .into_button() + .on_click(animate_to(&animation, &value, 100)), + ) + .into_columns() + .run() } fn animate_to( diff --git a/examples/basic-button.rs b/examples/basic-button.rs new file mode 100644 index 000000000..a78f524a2 --- /dev/null +++ b/examples/basic-button.rs @@ -0,0 +1,22 @@ +use gooey::value::{Dynamic, StringValue}; +use gooey::widget::MakeWidget; +use gooey::Run; + +// begin rustme snippet: readme +fn main() -> gooey::Result { + // Create a dynamic usize. + let count = Dynamic::new(0_isize); + // Create a dynamic that contains `count.to_string()` + let count_label = count.map_each(ToString::to_string); + + // Create a new button whose text is our dynamic string. + count_label + .into_button() + // Set the `on_click` callback to a closure that increments the counter. + .on_click(count.with_clone(|count| move |_| count.set(count.get() + 1))) + // Position the button in the center + .centered() + // Run the application + .run() +} +// end rustme snippet diff --git a/examples/button.rs b/examples/buttons.rs similarity index 100% rename from examples/button.rs rename to examples/buttons.rs diff --git a/examples/containers.rs b/examples/containers.rs index dde1b6506..98216fda4 100644 --- a/examples/containers.rs +++ b/examples/containers.rs @@ -1,6 +1,5 @@ -use gooey::value::Dynamic; +use gooey::value::{Dynamic, StringValue}; use gooey::widget::{MakeWidget, WidgetInstance}; -use gooey::widgets::{Button, Label}; use gooey::window::ThemeMode; use gooey::Run; @@ -10,7 +9,7 @@ fn main() -> gooey::Result { .centered() .expand() .into_window() - .with_theme_mode(theme_mode) + .themed_mode(theme_mode) .run() } @@ -18,20 +17,21 @@ fn set_of_containers(repeat: usize, theme_mode: Dynamic) -> WidgetIns let inner = if let Some(remaining_iters) = repeat.checked_sub(1) { set_of_containers(remaining_iters, theme_mode) } else { - Button::new("Toggle Theme Mode") + "Toggle Theme Mode" + .into_button() .on_click(move |_| { theme_mode.map_mut(|mode| mode.toggle()); }) .make_widget() }; - Label::new("Lowest") + "Lowest" .and( - Label::new("Low") + "Low" .and( - Label::new("Mid") + "Mid" .and( - Label::new("High") - .and(Label::new("Highest").and(inner).into_rows().contain()) + "High" + .and("Highest".and(inner).into_rows().contain()) .into_rows() .contain(), ) diff --git a/examples/counter.rs b/examples/counter.rs index cabdb9338..99bffea46 100644 --- a/examples/counter.rs +++ b/examples/counter.rs @@ -1,8 +1,7 @@ use std::string::ToString; -use gooey::value::Dynamic; +use gooey::value::{Dynamic, StringValue}; use gooey::widget::MakeWidget; -use gooey::widgets::{Button, Label}; use gooey::Run; use kludgine::figures::units::Lp; @@ -10,14 +9,14 @@ fn main() -> gooey::Result { let counter = Dynamic::new(0i32); let label = counter.map_each(ToString::to_string); - Label::new(label) + label .width(Lp::points(100)) - .and(Button::new("+").on_click(counter.with_clone(|counter| { + .and("+".into_button().on_click(counter.with_clone(|counter| { move |_| { *counter.lock() += 1; } }))) - .and(Button::new("-").on_click(counter.with_clone(|counter| { + .and("-".into_button().on_click(counter.with_clone(|counter| { move |_| { *counter.lock() -= 1; } diff --git a/examples/gameui.rs b/examples/gameui.rs index 50c5a7eff..f3e172d1c 100644 --- a/examples/gameui.rs +++ b/examples/gameui.rs @@ -1,6 +1,6 @@ -use gooey::value::Dynamic; +use gooey::value::{Dynamic, StringValue}; use gooey::widget::{MakeWidget, HANDLED, IGNORED}; -use gooey::widgets::{Input, Label, Space}; +use gooey::widgets::Space; use gooey::Run; use kludgine::app::winit::event::ElementState; use kludgine::app::winit::keyboard::Key; @@ -10,13 +10,14 @@ fn main() -> gooey::Result { let chat_log = Dynamic::new("Chat log goes here.\n".repeat(100)); let chat_message = Dynamic::new(String::new()); - Label::new(chat_log.clone()) + chat_log + .clone() .vertical_scroll() .expand() .and(Space::colored(Color::RED).expand_weighted(2)) .into_columns() .expand() - .and(Input::new(chat_message.clone()).on_key(move |input| { + .and(chat_message.clone().into_input().on_key(move |input| { match (input.state, input.logical_key) { (ElementState::Pressed, Key::Enter) => { let new_message = chat_message.map_mut(std::mem::take); diff --git a/examples/input.rs b/examples/input.rs index 48c1cefe9..65e39823f 100644 --- a/examples/input.rs +++ b/examples/input.rs @@ -1,7 +1,7 @@ +use gooey::value::StringValue; use gooey::widget::MakeWidget; -use gooey::widgets::Input; use gooey::Run; fn main() -> gooey::Result { - Input::new("Hello").expand().run() + "Hello".into_input().expand().run() } diff --git a/examples/login.rs b/examples/login.rs index 60a7be9d6..f81dfe595 100644 --- a/examples/login.rs +++ b/examples/login.rs @@ -1,8 +1,8 @@ use std::process::exit; -use gooey::value::{Dynamic, MapEach}; +use gooey::value::{Dynamic, MapEach, StringValue}; use gooey::widget::MakeWidget; -use gooey::widgets::{Button, Expand, Input, Label}; +use gooey::widgets::Expand; use gooey::Run; use kludgine::figures::units::Lp; @@ -14,18 +14,19 @@ fn main() -> gooey::Result { (&username, &password).map_each(|(username, password)| validate(username, password)); // TODO this should be a grid layout to ensure proper visual alignment. - let username_row = Label::new("Username") - .and(Input::new(username.clone()).expand()) + let username_row = "Username" + .and(username.clone().into_input().expand()) .into_columns(); - let password_row = Label::new("Password") + let password_row = "Password" .and( // TODO secure input - Input::new(password.clone()).expand(), + password.clone().into_input().expand(), ) .into_columns(); - let buttons = Button::new("Cancel") + let buttons = "Cancel" + .into_button() .on_click(|_| { eprintln!("Login cancelled"); exit(0) @@ -33,7 +34,8 @@ fn main() -> gooey::Result { .into_escape() .and(Expand::empty()) .and( - Button::new("Log In") + "Log In" + .into_button() .enabled(valid) .on_click(move |_| { println!("Welcome, {}", username.get()); diff --git a/examples/scroll.rs b/examples/scroll.rs index 745ec1f40..89daf674a 100644 --- a/examples/scroll.rs +++ b/examples/scroll.rs @@ -1,9 +1,8 @@ use gooey::widget::MakeWidget; -use gooey::widgets::Label; use gooey::Run; fn main() -> gooey::Result { - Label::new(include_str!("../src/widgets/scroll.rs")) + include_str!("../src/widgets/scroll.rs") .scroll() .expand() .run() diff --git a/examples/stack-align-test.rs b/examples/stack-align-test.rs new file mode 100644 index 000000000..fa27f5d06 --- /dev/null +++ b/examples/stack-align-test.rs @@ -0,0 +1,53 @@ +use gooey::value::StringValue; +use gooey::widget::MakeWidget; +use gooey::Run; + +/// This example shows a tricky layout problem. The hierarchy of widgets is +/// this: +/// +/// ```text +/// Expand (.expand()) +/// | Align (.centered()) +/// | | Stack (.into_rows()) +/// | | | Label +/// | | | Align (.centered()) +/// | | | | Button +/// ``` +/// +/// When the Stack widget attempted to implmement a single-pass layout, this +/// caused the Button to be aligned to the left inside of the stack. The Stack +/// widget now utilizes two `layout()` operations for layouts like this. Here's +/// the reasoning: +/// +/// At the window root, we have an Align wrapped by an Expand. The Align widget +/// during layout asks its children to size-to-fit. This means the Stack is +/// asking its children to size-to-fit as well. +/// +/// The Stack's orientation is Rows, and since the children are Resizes or +/// Expands, the widgets are size-to-fit. This means that the Stack will measure +/// these widgets asking them to size to fit. +/// +/// After running this pass of measurement, we can assign the heights of each of +/// the rows to the measurements we received. The width of the stack becomes the +/// maximum width of all children measured. +/// +/// In a single-pass layout, this means the Align widget inside of the Stack +/// never receives an opportunity to lay its children out with the final width. +/// The Button does end up centered because of this. Fixing it also becomes +/// tricky, because if surround the button in an Expand, it now instructs the +/// Stack to expand to fill its parent. +/// +/// After some careful deliberation, @ecton reasoned that in the situation where +/// a Stack is asked to layout with the Stack's non-primary being a size-to-fit +/// measurement, a second layout call for all children is required with Known +/// measurements to allow layouts like this example to work correctly. +fn main() -> gooey::Result { + // TODO once we have offscreen rendering, turn this into a test case + "Really Long Label" + .and("Short".into_button().centered()) + .into_rows() + .contain() + .centered() + .expand() + .run() +} diff --git a/examples/style.rs b/examples/style.rs index 36c2f8db7..d2c1ffa75 100644 --- a/examples/style.rs +++ b/examples/style.rs @@ -1,12 +1,12 @@ use gooey::styles::components::TextColor; use gooey::widget::MakeWidget; use gooey::widgets::stack::Stack; -use gooey::widgets::{Button, Style}; +use gooey::widgets::Style; use gooey::Run; use kludgine::Color; fn main() -> gooey::Result { - Stack::rows(Button::new("Green").and(red_text(Button::new("Red")))) + Stack::rows("Green".and(red_text("Red"))) .with(&TextColor, Color::GREEN) .run() } diff --git a/examples/switcher.rs b/examples/switcher.rs index ea43ea6ac..d34a7bc29 100644 --- a/examples/switcher.rs +++ b/examples/switcher.rs @@ -1,6 +1,6 @@ -use gooey::value::Dynamic; +use gooey::value::{Dynamic, StringValue}; use gooey::widget::{MakeWidget, WidgetInstance}; -use gooey::widgets::{Button, Label, Switcher}; +use gooey::widgets::Switcher; use gooey::Run; #[derive(Debug)] @@ -24,9 +24,10 @@ fn main() -> gooey::Result { fn intro(active: Dynamic) -> WidgetInstance { const INTRO: &str = "This example demonstrates the Switcher widget, which uses a mapping function to convert from a generic type to the widget it uses for its contents."; - Label::new(INTRO) + INTRO .and( - Button::new("Switch!") + "Switch!" + .into_button() .on_click(move |_| active.set(ActiveContent::Success)) .centered(), ) @@ -35,11 +36,12 @@ fn intro(active: Dynamic) -> WidgetInstance { } fn success(active: Dynamic) -> WidgetInstance { - Label::new("The value changed to `ActiveContent::Success`!") + "The value changed to `ActiveContent::Success`!" .and( - Button::new("Start Over") + "Start Over" + .into_button() .on_click(move |_| active.set(ActiveContent::Intro)) - // .centered(), + .centered(), ) .into_rows() .make_widget() diff --git a/examples/theme.rs b/examples/theme.rs index 65b809011..6067b5ca7 100644 --- a/examples/theme.rs +++ b/examples/theme.rs @@ -5,9 +5,10 @@ use gooey::styles::components::{TextColor, WidgetBackground}; use gooey::styles::{ ColorScheme, ColorSource, ColorTheme, FixedTheme, SurfaceTheme, Theme, ThemePair, }; -use gooey::value::{Dynamic, MapEach}; +use gooey::value::{Dynamic, MapEach, StringValue}; use gooey::widget::MakeWidget; -use gooey::widgets::{Input, Label, ModeSwitch, Scroll, Slider, Stack, Themed}; +use gooey::widgets::slider::Slidable; +use gooey::widgets::{Slider, Stack}; use gooey::window::ThemeMode; use gooey::Run; use kludgine::Color; @@ -44,38 +45,37 @@ fn main() -> gooey::Result { }, ); - Themed::new( - default_theme.clone(), - Stack::columns( - Scroll::vertical(Stack::rows( - theme_switcher - .and(primary_editor) - .and(secondary_editor) - .and(tertiary_editor) - .and(error_editor) - .and(neutral_editor) - .and(neutral_variant_editor), - )) - .and(fixed_themes( - default_theme.map_each(|theme| theme.primary_fixed), - default_theme.map_each(|theme| theme.secondary_fixed), - default_theme.map_each(|theme| theme.tertiary_fixed), - )) - .and(theme( - default_theme.map_each(|theme| theme.dark), - ThemeMode::Dark, - )) - .and(theme( - default_theme.map_each(|theme| theme.light), - ThemeMode::Light, - )), - ), - ) - .pad() - .expand() - .into_window() - .with_theme_mode(theme_mode) - .run() + let editors = theme_switcher + .and(primary_editor) + .and(secondary_editor) + .and(tertiary_editor) + .and(error_editor) + .and(neutral_editor) + .and(neutral_variant_editor) + .into_rows() + .vertical_scroll(); + + editors + .and(fixed_themes( + default_theme.map_each(|theme| theme.primary_fixed), + default_theme.map_each(|theme| theme.secondary_fixed), + default_theme.map_each(|theme| theme.tertiary_fixed), + )) + .and(theme( + default_theme.map_each(|theme| theme.dark), + ThemeMode::Dark, + )) + .and(theme( + default_theme.map_each(|theme| theme.light), + ThemeMode::Light, + )) + .into_columns() + .themed(default_theme) + .pad() + .expand() + .into_window() + .themed_mode(theme_mode) + .run() } fn dark_mode_slider() -> (Dynamic, impl MakeWidget) { @@ -83,7 +83,7 @@ fn dark_mode_slider() -> (Dynamic, impl MakeWidget) { ( theme_mode.clone(), - Stack::rows(Label::new("Theme Mode").and(Slider::::from_value(theme_mode))), + "Theme Mode".and(theme_mode.slider()).into_rows(), ) } @@ -114,11 +114,11 @@ fn color_editor( ( color, Stack::rows( - Label::new(label) - .and(Slider::::new(hue, 0., 360.)) - .and(Input::new(hue_text)) + label + .and(hue.slider_between(0., 360.)) + .and(hue_text.into_input()) .and(Slider::::from_value(saturation)) - .and(Input::new(saturation_text)), + .and(saturation_text.into_input()), ), ) } @@ -128,69 +128,64 @@ fn fixed_themes( secondary: Dynamic, tertiary: Dynamic, ) -> impl MakeWidget { - Stack::rows( - Label::new("Fixed") - .and(fixed_theme(primary, "Primary")) - .and(fixed_theme(secondary, "Secondary")) - .and(fixed_theme(tertiary, "Tertiary")), - ) - .contain() - .expand() + "Fixed" + .and(fixed_theme(primary, "Primary")) + .and(fixed_theme(secondary, "Secondary")) + .and(fixed_theme(tertiary, "Tertiary")) + .into_rows() + .contain() + .expand() } fn fixed_theme(theme: Dynamic, label: &str) -> impl MakeWidget { let color = theme.map_each(|theme| theme.color); let on_color = theme.map_each(|theme| theme.on_color); - Stack::columns( - swatch(color.clone(), &format!("{label} Fixed"), on_color.clone()) - .and(swatch( - theme.map_each(|theme| theme.dim_color), - &format!("Dim {label}"), - on_color.clone(), - )) - .and(swatch( - on_color.clone(), - &format!("On {label} Fixed"), - color.clone(), - )) - .and(swatch( - theme.map_each(|theme| theme.on_color_variant), - &format!("Variant On {label} Fixed"), - color, - )), - ) - .contain() - .expand() + + swatch(color.clone(), &format!("{label} Fixed"), on_color.clone()) + .and(swatch( + theme.map_each(|theme| theme.dim_color), + &format!("Dim {label}"), + on_color.clone(), + )) + .and(swatch( + on_color.clone(), + &format!("On {label} Fixed"), + color.clone(), + )) + .and(swatch( + theme.map_each(|theme| theme.on_color_variant), + &format!("Variant On {label} Fixed"), + color, + )) + .into_columns() + .contain() + .expand() } fn theme(theme: Dynamic, mode: ThemeMode) -> impl MakeWidget { - ModeSwitch::new( - mode, - Stack::rows( - Label::new(match mode { - ThemeMode::Light => "Light", - ThemeMode::Dark => "Dark", - }) - .and( - Stack::columns( - color_theme(theme.map_each(|theme| theme.primary), "Primary") - .and(color_theme( - theme.map_each(|theme| theme.secondary), - "Secondary", - )) - .and(color_theme( - theme.map_each(|theme| theme.tertiary), - "Tertiary", - )) - .and(color_theme(theme.map_each(|theme| theme.error), "Error")), - ) - .contain() - .expand(), - ) - .and(surface_theme(theme.map_each(|theme| theme.surface))), - ) - .contain(), + match mode { + ThemeMode::Light => "Light", + ThemeMode::Dark => "Dark", + } + .and( + color_theme(theme.map_each(|theme| theme.primary), "Primary") + .and(color_theme( + theme.map_each(|theme| theme.secondary), + "Secondary", + )) + .and(color_theme( + theme.map_each(|theme| theme.tertiary), + "Tertiary", + )) + .and(color_theme(theme.map_each(|theme| theme.error), "Error")) + .into_columns() + .contain() + .expand(), ) + .and(surface_theme(theme.map_each(|theme| theme.surface))) + .into_rows() + .contain() + .themed_mode(mode) .expand() } @@ -310,7 +305,7 @@ fn color_theme(theme: Dynamic, label: &str) -> impl MakeWidget { } fn swatch(background: Dynamic, label: &str, text: Dynamic) -> impl MakeWidget { - Label::new(label) + label .with(&TextColor, text) .with(&WidgetBackground, background) .fit_horizontally() diff --git a/gooey-macros/src/lib.rs b/gooey-macros/src/lib.rs index 051be8cf9..5780e6036 100644 --- a/gooey-macros/src/lib.rs +++ b/gooey-macros/src/lib.rs @@ -1,6 +1,6 @@ use manyhow::{manyhow, Result}; -use quote_use::quote_use as quote; use proc_macro2::TokenStream; +use quote_use::quote_use as quote; mod animation; #[manyhow(proc_macro_derive(LinearInterpolate))] diff --git a/src/value.rs b/src/value.rs index 710a84b4c..cf8f48401 100644 --- a/src/value.rs +++ b/src/value.rs @@ -16,6 +16,7 @@ use crate::animation::{DynamicTransition, LinearInterpolate}; use crate::context::{WidgetContext, WindowHandle}; use crate::utils::{IgnorePoison, WithClone}; use crate::widget::WidgetId; +use crate::widgets::{Button, Input}; /// An instance of a value that provides APIs to observe and react to its /// contents. @@ -1251,3 +1252,18 @@ macro_rules! impl_tuple_map_each { } impl_all_tuples!(impl_tuple_map_each); + +/// A type that can be converted into a [`Value`]. +pub trait StringValue: IntoValue + Sized { + /// Returns this string as a text input widget. + fn into_input(self) -> Input { + Input::new(self.into_value()) + } + + /// Returns this string as a clickable button. + fn into_button(self) -> Button { + Button::new(self.into_value()) + } +} + +impl StringValue for T where T: IntoValue {} diff --git a/src/widget.rs b/src/widget.rs index bd1c4ff67..577b64fe6 100644 --- a/src/widget.rs +++ b/src/widget.rs @@ -24,7 +24,7 @@ use crate::styles::{ use crate::tree::Tree; use crate::utils::IgnorePoison; use crate::value::{IntoValue, Value}; -use crate::widgets::{Align, Container, Expand, Resize, Scroll, Stack, Style}; +use crate::widgets::{Align, Container, Expand, Resize, Scroll, Stack, Style, Themed, ThemedMode}; use crate::window::{RunningWindow, ThemeMode, Window, WindowBehavior}; use crate::{ConstraintLimit, Run}; @@ -727,6 +727,16 @@ pub trait MakeWidget: Sized { fn pad_by(self, padding: impl IntoValue>) -> Container { self.contain().transparent().pad_by(padding) } + + /// Applies `theme` to `self` and its children. + fn themed(self, theme: impl IntoValue) -> Themed { + Themed::new(theme, self) + } + + /// Applies `mode` to `self` and its children. + fn themed_mode(self, mode: impl IntoValue) -> ThemedMode { + ThemedMode::new(mode, self) + } } /// A type that can create a [`WidgetInstance`] with a preallocated diff --git a/src/widgets.rs b/src/widgets.rs index d090af25c..b43bc7734 100644 --- a/src/widgets.rs +++ b/src/widgets.rs @@ -10,7 +10,7 @@ pub mod label; mod mode_switch; mod resize; pub mod scroll; -mod slider; +pub mod slider; mod space; pub mod stack; mod style; @@ -25,7 +25,7 @@ pub use container::Container; pub use expand::Expand; pub use input::Input; pub use label::Label; -pub use mode_switch::ModeSwitch; +pub use mode_switch::ThemedMode; pub use resize::Resize; pub use scroll::Scroll; pub use slider::Slider; diff --git a/src/widgets/mode_switch.rs b/src/widgets/mode_switch.rs index 238b56e98..2f2f61e5b 100644 --- a/src/widgets/mode_switch.rs +++ b/src/widgets/mode_switch.rs @@ -5,12 +5,12 @@ use crate::window::ThemeMode; /// A widget that applies a set of [`ThemeMode`] to all contained widgets. #[derive(Debug)] -pub struct ModeSwitch { +pub struct ThemedMode { mode: Value, child: WidgetRef, } -impl ModeSwitch { +impl ThemedMode { /// Returns a new widget that applies `mode` to all of its children. pub fn new(mode: impl IntoValue, child: impl MakeWidget) -> Self { Self { @@ -20,7 +20,7 @@ impl ModeSwitch { } } -impl WrapperWidget for ModeSwitch { +impl WrapperWidget for ThemedMode { fn child_mut(&mut self) -> &mut WidgetRef { &mut self.child } diff --git a/src/widgets/slider.rs b/src/widgets/slider.rs index 6dd210c43..7fabf9c92 100644 --- a/src/widgets/slider.rs +++ b/src/widgets/slider.rs @@ -1,3 +1,4 @@ +//! A widget that allows a user to "slide" between values. use std::fmt::Debug; use std::panic::UnwindSafe; @@ -327,3 +328,44 @@ define_components! { InactiveTrackColor(Color, "inactive_track_color", |context| context.get(&OpaqueWidgetColor)) } } + +/// A value that can be used in a [`Slider`] widget. +pub trait Slidable: IntoDynamic + Sized +where + T: Clone + + Debug + + PartialOrd + + LinearInterpolate + + PercentBetween + + UnwindSafe + + Send + + 'static, +{ + /// Returns a new slider over the full [range](Ranged) of the type. + fn slider(self) -> Slider + where + T: Ranged, + { + Slider::from_value(self.into_dynamic()) + } + + /// Returns a new slider using the value of `self`. The slider will be + /// limited to values between `min` and `max`. + fn slider_between(self, min: impl IntoValue, max: impl IntoValue) -> Slider { + Slider::new(self.into_dynamic(), min, max) + } +} + +impl Slidable for T +where + T: IntoDynamic, + U: Clone + + Debug + + PartialOrd + + LinearInterpolate + + PercentBetween + + UnwindSafe + + Send + + 'static, +{ +} diff --git a/src/window.rs b/src/window.rs index 76c9b5c98..64f4aa1ab 100644 --- a/src/window.rs +++ b/src/window.rs @@ -139,7 +139,7 @@ impl Window { /// /// `focused` will be initialized with an initial state /// of `false`. - pub fn with_focused(mut self, focused: impl IntoDynamic) -> Self { + pub fn focused(mut self, focused: impl IntoDynamic) -> Self { let focused = focused.into_dynamic(); focused.update(false); self.focused = Some(focused); @@ -154,7 +154,7 @@ impl Window { /// visible, this value will contain `true`. /// /// `occluded` will be initialized with an initial state of `false`. - pub fn with_occluded(mut self, occluded: impl IntoDynamic) -> Self { + pub fn occluded(mut self, occluded: impl IntoDynamic) -> Self { let occluded = occluded.into_dynamic(); occluded.update(false); self.occluded = Some(occluded); @@ -174,10 +174,16 @@ impl Window { /// Setting the [`Dynamic`]'s value will also update the window with the new /// mode until a mode change is detected, upon which the new mode will be /// stored. - pub fn with_theme_mode(mut self, theme_mode: impl IntoValue) -> Self { + pub fn themed_mode(mut self, theme_mode: impl IntoValue) -> Self { self.theme_mode = Some(theme_mode.into_value()); self } + + /// Applies `theme` to the widgets in this window. + pub fn themed(mut self, theme: impl IntoValue) -> Self { + self.theme = theme.into_value(); + self + } } impl Window