diff --git a/guide/guide-examples/examples/hello-world.rs b/guide/guide-examples/examples/hello-world.rs index fd91cc0..e9744d5 100644 --- a/guide/guide-examples/examples/hello-world.rs +++ b/guide/guide-examples/examples/hello-world.rs @@ -8,7 +8,7 @@ fn main() -> cushy::Result { #[test] fn book() { - fn hello_world() -> impl MakeWidget { + fn hello_world() -> impl cushy::widget::MakeWidget { "Hello, World!" } diff --git a/guide/guide-examples/examples/thread-progress.rs b/guide/guide-examples/examples/thread-progress.rs new file mode 100644 index 0000000..ad27315 --- /dev/null +++ b/guide/guide-examples/examples/thread-progress.rs @@ -0,0 +1,34 @@ +use std::time::Duration; + +use cushy::value::{Destination, Dynamic, Source}; +use cushy::widget::MakeWidget; +use cushy::widgets::progress::Progressable; +use guide_examples::book_example; + +fn thread_progress() -> impl MakeWidget { + // ANCHOR: example + let progress = Dynamic::new(0_u8); + std::thread::spawn({ + let progress = progress.clone(); + move || { + while progress.get() < 10 { + std::thread::sleep(Duration::from_millis(100)); + progress.set(progress.get() + 1); + } + } + }); + + progress.progress_bar_to(10) + // ANCHOR_END: example +} + +fn main() { + book_example!(thread_progress).animated(|recorder| { + recorder.wait_for(Duration::from_secs(2)).unwrap(); + }); +} + +#[test] +fn runs() { + main(); +} diff --git a/guide/src/SUMMARY.md b/guide/src/SUMMARY.md index c26ca99..630ba06 100644 --- a/guide/src/SUMMARY.md +++ b/guide/src/SUMMARY.md @@ -6,6 +6,8 @@ - [About Cushy](./about.md) - [Cushy's Philosophies](./about/philosophies.md) + - [Everything is a `Widget`](./about/widgets.md) + - [Reactive Data Model](./about/reactive.md) - [Widgets]() - [Layout Widgets](./widgets/layout.md) - [Align](./widgets/layout/align.md) diff --git a/guide/src/about/philosophies.md b/guide/src/about/philosophies.md index b517b8b..2627e15 100644 --- a/guide/src/about/philosophies.md +++ b/guide/src/about/philosophies.md @@ -5,8 +5,8 @@ displayed. Here's the philosophies that drive Cushy's design: - Cushy retains information between redraws so that many events can be handled without redrawing the user interface. -- Everything is a widget. The "root" of a user interface/window is a widget, and - widgets can contain other widgets. +- [Everything is a widget](./widgets.md). The "root" of a user interface/window + is a widget, and widgets can contain other widgets. - Composition is powerful and easy to reason about. The built-in widget library is aimed at providing a suite of single-purpose widgets that can be composed to create more complex user interfaces. diff --git a/guide/src/about/reactive.md b/guide/src/about/reactive.md new file mode 100644 index 0000000..5c8ca39 --- /dev/null +++ b/guide/src/about/reactive.md @@ -0,0 +1,93 @@ +# Reactive Data Model + +Cushy is designed around a reactive data model. In many UI frameworks, setting +the text of a [`Label`][label] is done by telling the label what it's new text +is. The Cushy way is to create a `Dynamic` containing the value you want to +display, and give a copy to the label and keep a copy for your application to +update as needed. + +When a widget uses a `Dynamic`, it informs Cushy that it needs to be either +redrawn or invalidated when its contents change. In turn, when Cushy detects a +change, it invalidates what is necessary. + +This means that updating a progress bar from a different thread is as simple as +passing a clone of a `Dynamic` that has been provided to a progress bar: + +```rust,no_run,no_playground +{{#include ../../guide-examples/examples/thread-progress.rs:example}} +``` + +The above snippet produces this user interface: + +![Threaded Progress Bar Update](../examples/thread_progress.png) + +This example just shows one simple way that Cushy's reactive model simplifies +development. To learn more, let's dive into the data types and traits that power +everything. + +## How widgets interact with data + +In Cushy, it's conventional for widgets to accept data using one of these +traits: + +| Trait | Target Type | Purpose | +|-------|-------------|---------| +| [`IntoValue`][intovalue] | [`Value`][value] | For possibly constant or dynamic values. Can be either a `T` or a `Dynamic`. | +| [`IntoDynamic`][intodynamic] | [`Dynamic`][dynamic] | For values that are read from and written to. | +| [`IntoReadOnly`][intoreadonly] | [`ReadOnly`][readonly] | For values that are read-only. Can be either a `T` or a `DynamicReader`. | +| [`IntoDynamicReader`][intodynamicreader] | [`DynamicReader`][dynamicreader] | For values that are read-only, but are unexpected to be constant. In general, `IntoValue` should be preferred if a single value makes sense to accept. | + +Let's look at an example of how these traits are utilizes. +[`Label::new()`][label-new] accepts the value it is to display as a +`ReadOnly` where `T: Display + ...`. + +This showcases Cushy's philosophy of embracing the Rust type system. Rather than +forcing `Label` to receive a `String`, it accepts any type that implements +`Display`, This allows it to accept a wide variety of types. + +Beyond basic values, it can also be given a special type that the `Label` can +react to when updated: a `Dynamic` or a `DynamicReader`. + +## What is a `Dynamic`? + +A [`Dynamic`][dynamic] is a reference-counted, threadsafe, async-friendly +location in memory that can invoke a series of callbacks when its contents +change. Let's revisit the example from the [intro](../intro.md): + +```rust,no_run,no_playground +{{#include ../../guide-examples/examples/intro.rs:example}} +``` + +![Hello Ferris Example](../examples/intro.png) + +Both the [`Input`][input] and the [`Label`][label] widgets have been given +instances of `Dynamic`s, but they are two different dynamics. The text +input field was given the dynamic we want to be edited. We react to the changes +through the `name.map_each(...)` callback. + +## What is a `DynamicReader`? + +A [`DynamicReader`][dynamicreader] provides read-only access to a +`Dynamic`, and also can: + +- [block][block] the current thread until the underlying `Dynamic` is changed. +- [wait][wait] for a change in an async task. +- Detect when the underlying `Dynamic` has had all of its instances dropped. + +`DynamicReader`s can be created using [`Dynamic::into_reader`][into-reader]/[`Dynamic::create_reader`][create-reader]. + +[value]: <{{ docs }}/value/enum.Value.html> +[readonly]: <{{ docs }}/value/enum.ReadOnly.html> +[dynamic]: <{{ docs }}/value/struct.Dynamic.html> +[into-reader]: <{{ docs }}/value/struct.Dynamic.html#method.into_reader> +[create-reader]: <{{ docs }}/value/struct.Dynamic.html#method.create_reader> +[dynamicreader]: <{{ docs }}/value/struct.DynamicReader.html> +[block]: <{{ docs }}/value/struct.DynamicReader.html#method.block_until_updated> +[wait]: <{{ docs }}/value/struct.DynamicReader.html#method.wait_until_updated> +[intovalue]: <{{ docs }}/value/trait.IntoValue.html> +[intodynamic]: <{{ docs }}/value/trait.IntoDynamic.html> +[intoreadonly]: <{{ docs }}/value/trait.IntoReadOnly.html> +[intodynamicreader]: <{{ docs }}/value/trait.IntoDynamicReader.html> +[label-new]: <{{ docs }}/widgets/label/struct.Label.html#method.new> +[label]: <{{ docs }}/widgets/label/struct.Label.html> +[input]: <{{ docs }}/widgets/input/struct.Input.html> diff --git a/guide/src/about/widgets.md b/guide/src/about/widgets.md new file mode 100644 index 0000000..d179b70 --- /dev/null +++ b/guide/src/about/widgets.md @@ -0,0 +1,38 @@ +# Everything is a `Widget` + +A widget is a rectangular area of a screen that implements the +[`Widget`][widget] trait. Widgets are the fundamental building block of Cushy. + +The `Widget` trait can look daunting, as it defines every possible function a +`Widget` might need in a graphical user interface. Thankfully, the details of +how this trait works can be ignored until you're ready to create custom widgets. + +Developing a user interface in Cushy is a two-step process: gather the +information for the interface and present the information in one or more +widgets. + +Cushy makes the process of creating widgets easy through the +[`MakeWidget`][makewidget] trait. Every `Widget` implementor automatically +implements `MakeWidget`, but it can also be implemented by any type to make it +easy to utilize within Cushy. For example, `String` implements `MakeWidget` by +returning a [`Label`][label]. This approach can also be used to convert complex +structures into multi-widget components without needing to create any new +`Widget` implementations. + +`MakeWidget` is also responsible for why `"Hello, World".run()` works. The +[`Run`][run] trait is automatically implemented for all `MakeWidget` +implementations. The implementation simply creates a [`Window`][window] from the +widget and runs it: + +```rust,no_run,no_playground +{{#include ../../../src/widget.rs:run}} +``` + +So now that we know our goal is to create one or more widgets to represent our +data, how do we transform our data and application state into widgets? + +[widget]: <{{ docs }}/widget/trait.Widget.html> +[makewidget]: <{{ docs }}/widget/trait.MakeWidget.html> +[label]: <{{docs}}/widgets/label/struct.Label.html> +[run]: <{{ docs }}/trait.Run.html> +[window]: <{{ docs }}/window/struct.Window.html> diff --git a/guide/src/examples/thread_progress.png b/guide/src/examples/thread_progress.png new file mode 100644 index 0000000..fcb4c34 Binary files /dev/null and b/guide/src/examples/thread_progress.png differ diff --git a/guide/src/widgets/utility/space.md b/guide/src/widgets/utility/space.md index 01d908a..47f45ef 100644 --- a/guide/src/widgets/utility/space.md +++ b/guide/src/widgets/utility/space.md @@ -1,5 +1,8 @@ # Space Widget -The [`Space`][space] +The [`Space`][space] widget is used to fill space. It is not interactive, and +will always occupy the smallest size it can. + +It can render a color or be clear. [space]: <{{ docs }}/widgets/struct.Space.html> diff --git a/src/value.rs b/src/value.rs index acece66..edd22dd 100644 --- a/src/value.rs +++ b/src/value.rs @@ -2030,7 +2030,7 @@ impl PartialEq> for Dynamic { } } -/// A reader that tracks the last generation accessed through this reader. +/// A reader of a [`Dynamic`] that tracks the last generation accessed. pub struct DynamicReader { source: Arc>, read_generation: Mutex, diff --git a/src/widget.rs b/src/widget.rs index d30de58..42e152c 100644 --- a/src/widget.rs +++ b/src/widget.rs @@ -455,14 +455,16 @@ pub trait Widget: Send + Debug + 'static { } } +// ANCHOR: run impl Run for T where T: MakeWidget, { fn run(self) -> crate::Result { - Window::::new(self.make_widget()).run() + Window::for_widget(self).run() } } +// ANCHOR_END: run impl Open for T where diff --git a/src/window.rs b/src/window.rs index 00a1d12..c93d2f2 100644 --- a/src/window.rs +++ b/src/window.rs @@ -56,8 +56,8 @@ use crate::value::{ Destination, Dynamic, DynamicReader, Generation, IntoDynamic, IntoValue, Source, Value, }; use crate::widget::{ - EventHandling, MakeWidget, MountedWidget, OnceCallback, RootBehavior, Widget, WidgetId, - WidgetInstance, HANDLED, IGNORED, + EventHandling, MakeWidget, MountedWidget, OnceCallback, RootBehavior, WidgetId, WidgetInstance, + HANDLED, IGNORED, }; use crate::window::sealed::WindowCommand; use crate::{initialize_tracing, ConstraintLimit}; @@ -496,9 +496,9 @@ impl Window { /// Returns a new instance using `widget` as its contents. pub fn for_widget(widget: W) -> Self where - W: Widget, + W: MakeWidget, { - Self::new(WidgetInstance::new(widget)) + Self::new(widget.make_widget()) } /// Sets `focused` to be the dynamic updated when this window's focus status