mirror of
https://github.com/danbulant/cushy
synced 2026-06-19 22:41:10 +00:00
Helpers galore
This commit is contained in:
parent
494fa680cb
commit
4c7c3be5ba
23 changed files with 336 additions and 174 deletions
|
|
@ -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);
|
||||
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 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()
|
||||
// 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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
26
README.md
26
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);
|
||||
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 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()
|
||||
// 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
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
22
examples/basic-button.rs
Normal file
22
examples/basic-button.rs
Normal 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
|
||||
|
|
@ -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<ThemeMode>) -> 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(),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
53
examples/stack-align-test.rs
Normal file
53
examples/stack-align-test.rs
Normal 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()
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<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.";
|
||||
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<ActiveContent>) -> WidgetInstance {
|
|||
}
|
||||
|
||||
fn success(active: Dynamic<ActiveContent>) -> 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()
|
||||
|
|
|
|||
|
|
@ -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<ThemeMode>, impl MakeWidget) {
|
||||
|
|
@ -83,7 +83,7 @@ fn dark_mode_slider() -> (Dynamic<ThemeMode>, impl MakeWidget) {
|
|||
|
||||
(
|
||||
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,
|
||||
Stack::rows(
|
||||
Label::new(label)
|
||||
.and(Slider::<f32>::new(hue, 0., 360.))
|
||||
.and(Input::new(hue_text))
|
||||
label
|
||||
.and(hue.slider_between(0., 360.))
|
||||
.and(hue_text.into_input())
|
||||
.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>,
|
||||
tertiary: Dynamic<FixedTheme>,
|
||||
) -> 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<FixedTheme>, 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<Theme>, 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<ColorTheme>, label: &str) -> impl MakeWidget {
|
|||
}
|
||||
|
||||
fn swatch(background: Dynamic<Color>, label: &str, text: Dynamic<Color>) -> impl MakeWidget {
|
||||
Label::new(label)
|
||||
label
|
||||
.with(&TextColor, text)
|
||||
.with(&WidgetBackground, background)
|
||||
.fit_horizontally()
|
||||
|
|
|
|||
|
|
@ -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))]
|
||||
|
|
|
|||
16
src/value.rs
16
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<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> {}
|
||||
|
|
|
|||
|
|
@ -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<Edges<Dimension>>) -> Container {
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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<ThemeMode>,
|
||||
child: WidgetRef,
|
||||
}
|
||||
|
||||
impl ModeSwitch {
|
||||
impl ThemedMode {
|
||||
/// Returns a new widget that applies `mode` to all of its children.
|
||||
pub fn new(mode: impl IntoValue<ThemeMode>, 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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<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,
|
||||
{
|
||||
}
|
||||
|
|
|
|||
|
|
@ -139,7 +139,7 @@ impl Window<WidgetInstance> {
|
|||
///
|
||||
/// `focused` will be initialized with an initial state
|
||||
/// 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();
|
||||
focused.update(false);
|
||||
self.focused = Some(focused);
|
||||
|
|
@ -154,7 +154,7 @@ impl Window<WidgetInstance> {
|
|||
/// 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<bool>) -> Self {
|
||||
pub fn occluded(mut self, occluded: impl IntoDynamic<bool>) -> Self {
|
||||
let occluded = occluded.into_dynamic();
|
||||
occluded.update(false);
|
||||
self.occluded = Some(occluded);
|
||||
|
|
@ -174,10 +174,16 @@ impl Window<WidgetInstance> {
|
|||
/// 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<ThemeMode>) -> Self {
|
||||
pub fn themed_mode(mut self, theme_mode: impl IntoValue<ThemeMode>) -> 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<ThemePair>) -> Self {
|
||||
self.theme = theme.into_value();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<Behavior> Window<Behavior>
|
||||
|
|
|
|||
Loading…
Reference in a new issue