Helpers galore

This commit is contained in:
Jonathan Johnson 2023-11-14 09:31:56 -08:00
parent 494fa680cb
commit 4c7c3be5ba
No known key found for this signature in database
GPG key ID: A66D6A34D6620579
23 changed files with 336 additions and 174 deletions

View file

@ -16,16 +16,22 @@ reactive data models work, consider this example that displays a button that
increments its own label: increments its own label:
```rust,ignore ```rust,ignore
// Create a dynamic usize. fn main() -> gooey::Result {
let count = Dynamic::new(0_usize); // 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 with a label that is produced by mapping the contents // Create a new button whose text is our dynamic string.
// of `count`. count_label
Button::new(count.map_each(ToString::to_string)) .into_button()
// Set the `on_click` callback to a closure that increments the counter. // Set the `on_click` callback to a closure that increments the counter.
.on_click(count.with_clone(|count| move |_| count.set(count.get() + 1))) .on_click(count.with_clone(|count| move |_| count.set(count.get() + 1)))
// Run the button as an an application. // Position the button in the center
.run() .centered()
// Run the application
.run()
}
``` ```
[widget]: crate::widget::Widget [widget]: crate::widget::Widget
@ -33,7 +39,7 @@ Button::new(count.map_each(ToString::to_string))
[wgpu]: https://github.com/gfx-rs/wgpu [wgpu]: https://github.com/gfx-rs/wgpu
[winit]: https://github.com/rust-windowing/winit [winit]: https://github.com/rust-windowing/winit
[widgets]: mod@crate::widgets [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 ## Open-source Licenses

View file

@ -16,7 +16,7 @@ reactive data models work, consider this example that displays a button that
increments its own label: increments its own label:
```rust,ignore ```rust,ignore
$../examples/button.rs:readme$ $../examples/basic-button.rs:readme$
``` ```
[widget]: $widget$ [widget]: $widget$
@ -24,4 +24,4 @@ $../examples/button.rs:readme$
[wgpu]: https://github.com/gfx-rs/wgpu [wgpu]: https://github.com/gfx-rs/wgpu
[winit]: https://github.com/rust-windowing/winit [winit]: https://github.com/rust-windowing/winit
[widgets]: $widgets$ [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

View file

@ -18,16 +18,22 @@ reactive data models work, consider this example that displays a button that
increments its own label: increments its own label:
```rust,ignore ```rust,ignore
// Create a dynamic usize. fn main() -> gooey::Result {
let count = Dynamic::new(0_usize); // 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 with a label that is produced by mapping the contents // Create a new button whose text is our dynamic string.
// of `count`. count_label
Button::new(count.map_each(ToString::to_string)) .into_button()
// Set the `on_click` callback to a closure that increments the counter. // Set the `on_click` callback to a closure that increments the counter.
.on_click(count.with_clone(|count| move |_| count.set(count.get() + 1))) .on_click(count.with_clone(|count| move |_| count.set(count.get() + 1)))
// Run the button as an an application. // Position the button in the center
.run() .centered()
// Run the application
.run()
}
``` ```
[widget]: https://gooey.rs/main/gooey/widget/trait.Widget.html [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 [wgpu]: https://github.com/gfx-rs/wgpu
[winit]: https://github.com/rust-windowing/winit [winit]: https://github.com/rust-windowing/winit
[widgets]: https://gooey.rs/main/gooey/widgets/index.html [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 ## Open-source Licenses

View file

@ -1,9 +1,8 @@
use std::time::Duration; use std::time::Duration;
use gooey::animation::{AnimationHandle, AnimationTarget, IntoAnimate, Spawn}; use gooey::animation::{AnimationHandle, AnimationTarget, IntoAnimate, Spawn};
use gooey::value::Dynamic; use gooey::value::{Dynamic, StringValue};
use gooey::widget::MakeWidget; use gooey::widget::MakeWidget;
use gooey::widgets::{Button, Label, Stack};
use gooey::{Run, WithClone}; use gooey::{Run, WithClone};
fn main() -> gooey::Result { fn main() -> gooey::Result {
@ -18,13 +17,17 @@ fn main() -> gooey::Result {
.on_complete(|| println!("Gooey animations are neat!")) .on_complete(|| println!("Gooey animations are neat!"))
.launch(); .launch();
Stack::columns( "To 0"
Button::new("To 0") .into_button()
.on_click(animate_to(&animation, &value, 0)) .on_click(animate_to(&animation, &value, 0))
.and(Label::new(label)) .and(label)
.and(Button::new("To 100").on_click(animate_to(&animation, &value, 100))), .and(
) "To 100"
.run() .into_button()
.on_click(animate_to(&animation, &value, 100)),
)
.into_columns()
.run()
} }
fn animate_to( fn animate_to(

22
examples/basic-button.rs Normal file
View file

@ -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

View file

@ -1,6 +1,5 @@
use gooey::value::Dynamic; use gooey::value::{Dynamic, StringValue};
use gooey::widget::{MakeWidget, WidgetInstance}; use gooey::widget::{MakeWidget, WidgetInstance};
use gooey::widgets::{Button, Label};
use gooey::window::ThemeMode; use gooey::window::ThemeMode;
use gooey::Run; use gooey::Run;
@ -10,7 +9,7 @@ fn main() -> gooey::Result {
.centered() .centered()
.expand() .expand()
.into_window() .into_window()
.with_theme_mode(theme_mode) .themed_mode(theme_mode)
.run() .run()
} }
@ -18,20 +17,21 @@ fn set_of_containers(repeat: usize, theme_mode: Dynamic<ThemeMode>) -> WidgetIns
let inner = if let Some(remaining_iters) = repeat.checked_sub(1) { let inner = if let Some(remaining_iters) = repeat.checked_sub(1) {
set_of_containers(remaining_iters, theme_mode) set_of_containers(remaining_iters, theme_mode)
} else { } else {
Button::new("Toggle Theme Mode") "Toggle Theme Mode"
.into_button()
.on_click(move |_| { .on_click(move |_| {
theme_mode.map_mut(|mode| mode.toggle()); theme_mode.map_mut(|mode| mode.toggle());
}) })
.make_widget() .make_widget()
}; };
Label::new("Lowest") "Lowest"
.and( .and(
Label::new("Low") "Low"
.and( .and(
Label::new("Mid") "Mid"
.and( .and(
Label::new("High") "High"
.and(Label::new("Highest").and(inner).into_rows().contain()) .and("Highest".and(inner).into_rows().contain())
.into_rows() .into_rows()
.contain(), .contain(),
) )

View file

@ -1,8 +1,7 @@
use std::string::ToString; use std::string::ToString;
use gooey::value::Dynamic; use gooey::value::{Dynamic, StringValue};
use gooey::widget::MakeWidget; use gooey::widget::MakeWidget;
use gooey::widgets::{Button, Label};
use gooey::Run; use gooey::Run;
use kludgine::figures::units::Lp; use kludgine::figures::units::Lp;
@ -10,14 +9,14 @@ fn main() -> gooey::Result {
let counter = Dynamic::new(0i32); let counter = Dynamic::new(0i32);
let label = counter.map_each(ToString::to_string); let label = counter.map_each(ToString::to_string);
Label::new(label) label
.width(Lp::points(100)) .width(Lp::points(100))
.and(Button::new("+").on_click(counter.with_clone(|counter| { .and("+".into_button().on_click(counter.with_clone(|counter| {
move |_| { move |_| {
*counter.lock() += 1; *counter.lock() += 1;
} }
}))) })))
.and(Button::new("-").on_click(counter.with_clone(|counter| { .and("-".into_button().on_click(counter.with_clone(|counter| {
move |_| { move |_| {
*counter.lock() -= 1; *counter.lock() -= 1;
} }

View file

@ -1,6 +1,6 @@
use gooey::value::Dynamic; use gooey::value::{Dynamic, StringValue};
use gooey::widget::{MakeWidget, HANDLED, IGNORED}; use gooey::widget::{MakeWidget, HANDLED, IGNORED};
use gooey::widgets::{Input, Label, Space}; use gooey::widgets::Space;
use gooey::Run; use gooey::Run;
use kludgine::app::winit::event::ElementState; use kludgine::app::winit::event::ElementState;
use kludgine::app::winit::keyboard::Key; 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_log = Dynamic::new("Chat log goes here.\n".repeat(100));
let chat_message = Dynamic::new(String::new()); let chat_message = Dynamic::new(String::new());
Label::new(chat_log.clone()) chat_log
.clone()
.vertical_scroll() .vertical_scroll()
.expand() .expand()
.and(Space::colored(Color::RED).expand_weighted(2)) .and(Space::colored(Color::RED).expand_weighted(2))
.into_columns() .into_columns()
.expand() .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) { match (input.state, input.logical_key) {
(ElementState::Pressed, Key::Enter) => { (ElementState::Pressed, Key::Enter) => {
let new_message = chat_message.map_mut(std::mem::take); let new_message = chat_message.map_mut(std::mem::take);

View file

@ -1,7 +1,7 @@
use gooey::value::StringValue;
use gooey::widget::MakeWidget; use gooey::widget::MakeWidget;
use gooey::widgets::Input;
use gooey::Run; use gooey::Run;
fn main() -> gooey::Result { fn main() -> gooey::Result {
Input::new("Hello").expand().run() "Hello".into_input().expand().run()
} }

View file

@ -1,8 +1,8 @@
use std::process::exit; use std::process::exit;
use gooey::value::{Dynamic, MapEach}; use gooey::value::{Dynamic, MapEach, StringValue};
use gooey::widget::MakeWidget; use gooey::widget::MakeWidget;
use gooey::widgets::{Button, Expand, Input, Label}; use gooey::widgets::Expand;
use gooey::Run; use gooey::Run;
use kludgine::figures::units::Lp; use kludgine::figures::units::Lp;
@ -14,18 +14,19 @@ fn main() -> gooey::Result {
(&username, &password).map_each(|(username, password)| validate(username, password)); (&username, &password).map_each(|(username, password)| validate(username, password));
// TODO this should be a grid layout to ensure proper visual alignment. // TODO this should be a grid layout to ensure proper visual alignment.
let username_row = Label::new("Username") let username_row = "Username"
.and(Input::new(username.clone()).expand()) .and(username.clone().into_input().expand())
.into_columns(); .into_columns();
let password_row = Label::new("Password") let password_row = "Password"
.and( .and(
// TODO secure input // TODO secure input
Input::new(password.clone()).expand(), password.clone().into_input().expand(),
) )
.into_columns(); .into_columns();
let buttons = Button::new("Cancel") let buttons = "Cancel"
.into_button()
.on_click(|_| { .on_click(|_| {
eprintln!("Login cancelled"); eprintln!("Login cancelled");
exit(0) exit(0)
@ -33,7 +34,8 @@ fn main() -> gooey::Result {
.into_escape() .into_escape()
.and(Expand::empty()) .and(Expand::empty())
.and( .and(
Button::new("Log In") "Log In"
.into_button()
.enabled(valid) .enabled(valid)
.on_click(move |_| { .on_click(move |_| {
println!("Welcome, {}", username.get()); println!("Welcome, {}", username.get());

View file

@ -1,9 +1,8 @@
use gooey::widget::MakeWidget; use gooey::widget::MakeWidget;
use gooey::widgets::Label;
use gooey::Run; use gooey::Run;
fn main() -> gooey::Result { fn main() -> gooey::Result {
Label::new(include_str!("../src/widgets/scroll.rs")) include_str!("../src/widgets/scroll.rs")
.scroll() .scroll()
.expand() .expand()
.run() .run()

View file

@ -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()
}

View file

@ -1,12 +1,12 @@
use gooey::styles::components::TextColor; use gooey::styles::components::TextColor;
use gooey::widget::MakeWidget; use gooey::widget::MakeWidget;
use gooey::widgets::stack::Stack; use gooey::widgets::stack::Stack;
use gooey::widgets::{Button, Style}; use gooey::widgets::Style;
use gooey::Run; use gooey::Run;
use kludgine::Color; use kludgine::Color;
fn main() -> gooey::Result { 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) .with(&TextColor, Color::GREEN)
.run() .run()
} }

View file

@ -1,6 +1,6 @@
use gooey::value::Dynamic; use gooey::value::{Dynamic, StringValue};
use gooey::widget::{MakeWidget, WidgetInstance}; use gooey::widget::{MakeWidget, WidgetInstance};
use gooey::widgets::{Button, Label, Switcher}; use gooey::widgets::Switcher;
use gooey::Run; use gooey::Run;
#[derive(Debug)] #[derive(Debug)]
@ -24,9 +24,10 @@ fn main() -> gooey::Result {
fn intro(active: Dynamic<ActiveContent>) -> WidgetInstance { fn intro(active: Dynamic<ActiveContent>) -> WidgetInstance {
const INTRO: &str = "This example demonstrates the Switcher<T> widget, which uses a mapping function to convert from a generic type to the widget it uses for its contents."; const INTRO: &str = "This example demonstrates the Switcher<T> 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( .and(
Button::new("Switch!") "Switch!"
.into_button()
.on_click(move |_| active.set(ActiveContent::Success)) .on_click(move |_| active.set(ActiveContent::Success))
.centered(), .centered(),
) )
@ -35,11 +36,12 @@ fn intro(active: Dynamic<ActiveContent>) -> WidgetInstance {
} }
fn success(active: Dynamic<ActiveContent>) -> WidgetInstance { fn success(active: Dynamic<ActiveContent>) -> WidgetInstance {
Label::new("The value changed to `ActiveContent::Success`!") "The value changed to `ActiveContent::Success`!"
.and( .and(
Button::new("Start Over") "Start Over"
.into_button()
.on_click(move |_| active.set(ActiveContent::Intro)) .on_click(move |_| active.set(ActiveContent::Intro))
// .centered(), .centered(),
) )
.into_rows() .into_rows()
.make_widget() .make_widget()

View file

@ -5,9 +5,10 @@ use gooey::styles::components::{TextColor, WidgetBackground};
use gooey::styles::{ use gooey::styles::{
ColorScheme, ColorSource, ColorTheme, FixedTheme, SurfaceTheme, Theme, ThemePair, ColorScheme, ColorSource, ColorTheme, FixedTheme, SurfaceTheme, Theme, ThemePair,
}; };
use gooey::value::{Dynamic, MapEach}; use gooey::value::{Dynamic, MapEach, StringValue};
use gooey::widget::MakeWidget; 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::window::ThemeMode;
use gooey::Run; use gooey::Run;
use kludgine::Color; use kludgine::Color;
@ -44,38 +45,37 @@ fn main() -> gooey::Result {
}, },
); );
Themed::new( let editors = theme_switcher
default_theme.clone(), .and(primary_editor)
Stack::columns( .and(secondary_editor)
Scroll::vertical(Stack::rows( .and(tertiary_editor)
theme_switcher .and(error_editor)
.and(primary_editor) .and(neutral_editor)
.and(secondary_editor) .and(neutral_variant_editor)
.and(tertiary_editor) .into_rows()
.and(error_editor) .vertical_scroll();
.and(neutral_editor)
.and(neutral_variant_editor), editors
)) .and(fixed_themes(
.and(fixed_themes( default_theme.map_each(|theme| theme.primary_fixed),
default_theme.map_each(|theme| theme.primary_fixed), default_theme.map_each(|theme| theme.secondary_fixed),
default_theme.map_each(|theme| theme.secondary_fixed), default_theme.map_each(|theme| theme.tertiary_fixed),
default_theme.map_each(|theme| theme.tertiary_fixed), ))
)) .and(theme(
.and(theme( default_theme.map_each(|theme| theme.dark),
default_theme.map_each(|theme| theme.dark), ThemeMode::Dark,
ThemeMode::Dark, ))
)) .and(theme(
.and(theme( default_theme.map_each(|theme| theme.light),
default_theme.map_each(|theme| theme.light), ThemeMode::Light,
ThemeMode::Light, ))
)), .into_columns()
), .themed(default_theme)
) .pad()
.pad() .expand()
.expand() .into_window()
.into_window() .themed_mode(theme_mode)
.with_theme_mode(theme_mode) .run()
.run()
} }
fn dark_mode_slider() -> (Dynamic<ThemeMode>, impl MakeWidget) { fn dark_mode_slider() -> (Dynamic<ThemeMode>, impl MakeWidget) {
@ -83,7 +83,7 @@ fn dark_mode_slider() -> (Dynamic<ThemeMode>, impl MakeWidget) {
( (
theme_mode.clone(), theme_mode.clone(),
Stack::rows(Label::new("Theme Mode").and(Slider::<ThemeMode>::from_value(theme_mode))), "Theme Mode".and(theme_mode.slider()).into_rows(),
) )
} }
@ -114,11 +114,11 @@ fn color_editor(
( (
color, color,
Stack::rows( Stack::rows(
Label::new(label) label
.and(Slider::<f32>::new(hue, 0., 360.)) .and(hue.slider_between(0., 360.))
.and(Input::new(hue_text)) .and(hue_text.into_input())
.and(Slider::<ZeroToOne>::from_value(saturation)) .and(Slider::<ZeroToOne>::from_value(saturation))
.and(Input::new(saturation_text)), .and(saturation_text.into_input()),
), ),
) )
} }
@ -128,69 +128,64 @@ fn fixed_themes(
secondary: Dynamic<FixedTheme>, secondary: Dynamic<FixedTheme>,
tertiary: Dynamic<FixedTheme>, tertiary: Dynamic<FixedTheme>,
) -> impl MakeWidget { ) -> impl MakeWidget {
Stack::rows( "Fixed"
Label::new("Fixed") .and(fixed_theme(primary, "Primary"))
.and(fixed_theme(primary, "Primary")) .and(fixed_theme(secondary, "Secondary"))
.and(fixed_theme(secondary, "Secondary")) .and(fixed_theme(tertiary, "Tertiary"))
.and(fixed_theme(tertiary, "Tertiary")), .into_rows()
) .contain()
.contain() .expand()
.expand()
} }
fn fixed_theme(theme: Dynamic<FixedTheme>, label: &str) -> impl MakeWidget { fn fixed_theme(theme: Dynamic<FixedTheme>, label: &str) -> impl MakeWidget {
let color = theme.map_each(|theme| theme.color); let color = theme.map_each(|theme| theme.color);
let on_color = theme.map_each(|theme| theme.on_color); let on_color = theme.map_each(|theme| theme.on_color);
Stack::columns(
swatch(color.clone(), &format!("{label} Fixed"), on_color.clone()) swatch(color.clone(), &format!("{label} Fixed"), on_color.clone())
.and(swatch( .and(swatch(
theme.map_each(|theme| theme.dim_color), theme.map_each(|theme| theme.dim_color),
&format!("Dim {label}"), &format!("Dim {label}"),
on_color.clone(), on_color.clone(),
)) ))
.and(swatch( .and(swatch(
on_color.clone(), on_color.clone(),
&format!("On {label} Fixed"), &format!("On {label} Fixed"),
color.clone(), color.clone(),
)) ))
.and(swatch( .and(swatch(
theme.map_each(|theme| theme.on_color_variant), theme.map_each(|theme| theme.on_color_variant),
&format!("Variant On {label} Fixed"), &format!("Variant On {label} Fixed"),
color, color,
)), ))
) .into_columns()
.contain() .contain()
.expand() .expand()
} }
fn theme(theme: Dynamic<Theme>, mode: ThemeMode) -> impl MakeWidget { fn theme(theme: Dynamic<Theme>, mode: ThemeMode) -> impl MakeWidget {
ModeSwitch::new( match mode {
mode, ThemeMode::Light => "Light",
Stack::rows( ThemeMode::Dark => "Dark",
Label::new(match mode { }
ThemeMode::Light => "Light", .and(
ThemeMode::Dark => "Dark", color_theme(theme.map_each(|theme| theme.primary), "Primary")
}) .and(color_theme(
.and( theme.map_each(|theme| theme.secondary),
Stack::columns( "Secondary",
color_theme(theme.map_each(|theme| theme.primary), "Primary") ))
.and(color_theme( .and(color_theme(
theme.map_each(|theme| theme.secondary), theme.map_each(|theme| theme.tertiary),
"Secondary", "Tertiary",
)) ))
.and(color_theme( .and(color_theme(theme.map_each(|theme| theme.error), "Error"))
theme.map_each(|theme| theme.tertiary), .into_columns()
"Tertiary", .contain()
)) .expand(),
.and(color_theme(theme.map_each(|theme| theme.error), "Error")),
)
.contain()
.expand(),
)
.and(surface_theme(theme.map_each(|theme| theme.surface))),
)
.contain(),
) )
.and(surface_theme(theme.map_each(|theme| theme.surface)))
.into_rows()
.contain()
.themed_mode(mode)
.expand() .expand()
} }
@ -310,7 +305,7 @@ fn color_theme(theme: Dynamic<ColorTheme>, label: &str) -> impl MakeWidget {
} }
fn swatch(background: Dynamic<Color>, label: &str, text: Dynamic<Color>) -> impl MakeWidget { fn swatch(background: Dynamic<Color>, label: &str, text: Dynamic<Color>) -> impl MakeWidget {
Label::new(label) label
.with(&TextColor, text) .with(&TextColor, text)
.with(&WidgetBackground, background) .with(&WidgetBackground, background)
.fit_horizontally() .fit_horizontally()

View file

@ -1,6 +1,6 @@
use manyhow::{manyhow, Result}; use manyhow::{manyhow, Result};
use quote_use::quote_use as quote;
use proc_macro2::TokenStream; use proc_macro2::TokenStream;
use quote_use::quote_use as quote;
mod animation; mod animation;
#[manyhow(proc_macro_derive(LinearInterpolate))] #[manyhow(proc_macro_derive(LinearInterpolate))]

View file

@ -16,6 +16,7 @@ use crate::animation::{DynamicTransition, LinearInterpolate};
use crate::context::{WidgetContext, WindowHandle}; use crate::context::{WidgetContext, WindowHandle};
use crate::utils::{IgnorePoison, WithClone}; use crate::utils::{IgnorePoison, WithClone};
use crate::widget::WidgetId; use crate::widget::WidgetId;
use crate::widgets::{Button, Input};
/// An instance of a value that provides APIs to observe and react to its /// An instance of a value that provides APIs to observe and react to its
/// contents. /// contents.
@ -1251,3 +1252,18 @@ macro_rules! impl_tuple_map_each {
} }
impl_all_tuples!(impl_tuple_map_each); impl_all_tuples!(impl_tuple_map_each);
/// A type that can be converted into a [`Value<String>`].
pub trait StringValue: IntoValue<String> + 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<T> StringValue for T where T: IntoValue<String> {}

View file

@ -24,7 +24,7 @@ use crate::styles::{
use crate::tree::Tree; use crate::tree::Tree;
use crate::utils::IgnorePoison; use crate::utils::IgnorePoison;
use crate::value::{IntoValue, Value}; 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::window::{RunningWindow, ThemeMode, Window, WindowBehavior};
use crate::{ConstraintLimit, Run}; use crate::{ConstraintLimit, Run};
@ -727,6 +727,16 @@ pub trait MakeWidget: Sized {
fn pad_by(self, padding: impl IntoValue<Edges<Dimension>>) -> Container { fn pad_by(self, padding: impl IntoValue<Edges<Dimension>>) -> Container {
self.contain().transparent().pad_by(padding) self.contain().transparent().pad_by(padding)
} }
/// Applies `theme` to `self` and its children.
fn themed(self, theme: impl IntoValue<ThemePair>) -> Themed {
Themed::new(theme, self)
}
/// Applies `mode` to `self` and its children.
fn themed_mode(self, mode: impl IntoValue<ThemeMode>) -> ThemedMode {
ThemedMode::new(mode, self)
}
} }
/// A type that can create a [`WidgetInstance`] with a preallocated /// A type that can create a [`WidgetInstance`] with a preallocated

View file

@ -10,7 +10,7 @@ pub mod label;
mod mode_switch; mod mode_switch;
mod resize; mod resize;
pub mod scroll; pub mod scroll;
mod slider; pub mod slider;
mod space; mod space;
pub mod stack; pub mod stack;
mod style; mod style;
@ -25,7 +25,7 @@ pub use container::Container;
pub use expand::Expand; pub use expand::Expand;
pub use input::Input; pub use input::Input;
pub use label::Label; pub use label::Label;
pub use mode_switch::ModeSwitch; pub use mode_switch::ThemedMode;
pub use resize::Resize; pub use resize::Resize;
pub use scroll::Scroll; pub use scroll::Scroll;
pub use slider::Slider; pub use slider::Slider;

View file

@ -5,12 +5,12 @@ use crate::window::ThemeMode;
/// A widget that applies a set of [`ThemeMode`] to all contained widgets. /// A widget that applies a set of [`ThemeMode`] to all contained widgets.
#[derive(Debug)] #[derive(Debug)]
pub struct ModeSwitch { pub struct ThemedMode {
mode: Value<ThemeMode>, mode: Value<ThemeMode>,
child: WidgetRef, child: WidgetRef,
} }
impl ModeSwitch { impl ThemedMode {
/// Returns a new widget that applies `mode` to all of its children. /// Returns a new widget that applies `mode` to all of its children.
pub fn new(mode: impl IntoValue<ThemeMode>, child: impl MakeWidget) -> Self { pub fn new(mode: impl IntoValue<ThemeMode>, child: impl MakeWidget) -> Self {
Self { Self {
@ -20,7 +20,7 @@ impl ModeSwitch {
} }
} }
impl WrapperWidget for ModeSwitch { impl WrapperWidget for ThemedMode {
fn child_mut(&mut self) -> &mut WidgetRef { fn child_mut(&mut self) -> &mut WidgetRef {
&mut self.child &mut self.child
} }

View file

@ -1,3 +1,4 @@
//! A widget that allows a user to "slide" between values.
use std::fmt::Debug; use std::fmt::Debug;
use std::panic::UnwindSafe; use std::panic::UnwindSafe;
@ -327,3 +328,44 @@ define_components! {
InactiveTrackColor(Color, "inactive_track_color", |context| context.get(&OpaqueWidgetColor)) InactiveTrackColor(Color, "inactive_track_color", |context| context.get(&OpaqueWidgetColor))
} }
} }
/// A value that can be used in a [`Slider`] widget.
pub trait Slidable<T>: IntoDynamic<T> + 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<T>
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<T>, max: impl IntoValue<T>) -> Slider<T> {
Slider::new(self.into_dynamic(), min, max)
}
}
impl<U, T> Slidable<U> for T
where
T: IntoDynamic<U>,
U: Clone
+ Debug
+ PartialOrd
+ LinearInterpolate
+ PercentBetween
+ UnwindSafe
+ Send
+ 'static,
{
}

View file

@ -139,7 +139,7 @@ impl Window<WidgetInstance> {
/// ///
/// `focused` will be initialized with an initial state /// `focused` will be initialized with an initial state
/// of `false`. /// of `false`.
pub fn with_focused(mut self, focused: impl IntoDynamic<bool>) -> Self { pub fn focused(mut self, focused: impl IntoDynamic<bool>) -> Self {
let focused = focused.into_dynamic(); let focused = focused.into_dynamic();
focused.update(false); focused.update(false);
self.focused = Some(focused); self.focused = Some(focused);
@ -154,7 +154,7 @@ impl Window<WidgetInstance> {
/// visible, this value will contain `true`. /// visible, this value will contain `true`.
/// ///
/// `occluded` will be initialized with an initial state of `false`. /// `occluded` will be initialized with an initial state of `false`.
pub fn with_occluded(mut self, occluded: impl IntoDynamic<bool>) -> Self { pub fn occluded(mut self, occluded: impl IntoDynamic<bool>) -> Self {
let occluded = occluded.into_dynamic(); let occluded = occluded.into_dynamic();
occluded.update(false); occluded.update(false);
self.occluded = Some(occluded); self.occluded = Some(occluded);
@ -174,10 +174,16 @@ impl Window<WidgetInstance> {
/// Setting the [`Dynamic`]'s value will also update the window with the new /// 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 /// mode until a mode change is detected, upon which the new mode will be
/// stored. /// stored.
pub fn with_theme_mode(mut self, theme_mode: impl IntoValue<ThemeMode>) -> Self { pub fn themed_mode(mut self, theme_mode: impl IntoValue<ThemeMode>) -> Self {
self.theme_mode = Some(theme_mode.into_value()); self.theme_mode = Some(theme_mode.into_value());
self self
} }
/// Applies `theme` to the widgets in this window.
pub fn themed(mut self, theme: impl IntoValue<ThemePair>) -> Self {
self.theme = theme.into_value();
self
}
} }
impl<Behavior> Window<Behavior> impl<Behavior> Window<Behavior>