Children renamed to WidgetList

Plus more work on the user's guide, which inspired the rename.
This commit is contained in:
Jonathan Johnson 2024-01-09 13:24:15 -08:00
parent b5bd69136a
commit 6ad6cca32d
No known key found for this signature in database
GPG key ID: A66D6A34D6620579
35 changed files with 705 additions and 73 deletions

View file

@ -1,3 +1,6 @@
<!-- This file is generated by `rustme`. Ensure you're editing the source in the .rustme/ directory --!>
<!-- markdownlint-disable first-line-h1 -->
![Cushy is considered alpha and unsupported](https://img.shields.io/badge/status-alpha-orange)
[![crate version](https://img.shields.io/crates/v/cushy.svg)](https://crates.io/crates/cushy)
[![Documentation for `main`](https://img.shields.io/badge/docs-main-informational)](https://cushy.rs/main/docs/cushy/)

View file

@ -1,3 +1,6 @@
<!-- This file is generated by `rustme`. Ensure you're editing the source in the .rustme/ directory --!>
<!-- markdownlint-disable first-line-h1 -->
![Cushy is considered alpha and unsupported](https://img.shields.io/badge/status-alpha-orange)
[![crate version](https://img.shields.io/crates/v/cushy.svg)](https://crates.io/crates/cushy)
[![Documentation for `$ref-name$`](https://img.shields.io/badge/docs-$ref-name$-informational)]($docs$)

View file

@ -42,12 +42,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `Label` has been refactored to accept any `Display` type. As a result of this,
`Label::text` is now named `display` and `Label::new()` now accepts an
`IntoReadOnly<T>` instead of `IntoValue<String>`.
- `Dynamic<Children>::wrap` has been renamed to `into_wrap` for consistency.
- `Dynamic<WidgetList>::wrap` and `WidgetList::wrap` have been renamed to
`into_wrap` for consistency.
- Cushy now has its own `KeyEvent` type, as winit's has private fields. This
prevented simulating input in a `VirtualWindow`.
- `FlexibleDimension::ZERO` has been removed, and now `FlexibleDimension`
implements `Zero` which defines an associated constant of the same name and
purpose.
- `Children` has been renamed to `WidgetList`.
### Fixed

View file

@ -1,5 +1,8 @@
# Cushy
<!-- This file is generated by `rustme`. Ensure you're editing the source in the .rustme/ directory --!>
<!-- markdownlint-disable first-line-h1 -->
![Cushy is considered alpha and unsupported](https://img.shields.io/badge/status-alpha-orange)
[![crate version](https://img.shields.io/crates/v/cushy.svg)](https://crates.io/crates/cushy)
[![Documentation for `main`](https://img.shields.io/badge/docs-main-informational)](https://cushy.rs/main/docs/cushy/)

View file

@ -1,7 +1,7 @@
use std::collections::HashMap;
use cushy::value::{Dynamic, MapEach, Source};
use cushy::widget::{Children, MakeWidget};
use cushy::widget::{MakeWidget, WidgetList};
use cushy::widgets::input::InputValue;
use cushy::Run;
@ -50,7 +50,7 @@ fn main() -> cushy::Result {
.new_select(Some(id), format!("{first} {last}").align_left())
.make_widget()
})
.collect::<Children>()
.collect::<WidgetList>()
}
});

View file

@ -1,6 +1,6 @@
use cushy::styles::components::{LineHeight, TextSize};
use cushy::value::Dynamic;
use cushy::widget::{Children, MakeWidget};
use cushy::widget::{MakeWidget, WidgetList};
use cushy::widgets::wrap::{VerticalAlign, WrapAlign};
use cushy::Run;
use figures::units::Lp;
@ -16,7 +16,7 @@ fn main() -> cushy::Result {
let text_size = Lp::points(rng.gen_range(14..48));
word.with(&TextSize, text_size).with(&LineHeight, text_size)
})
.collect::<Children>();
.collect::<WidgetList>();
let align = Dynamic::<WrapAlign>::default();
let vertical_align = Dynamic::<VerticalAlign>::default();
@ -50,7 +50,7 @@ fn main() -> cushy::Result {
.h3()
.and(
words
.wrap()
.into_wrap()
.align(align)
.vertical_align(vertical_align)
.expand_horizontally()

View file

@ -11,3 +11,4 @@ extra-watch-dirs = ["./guide-examples/"]
[preprocessor.variables.variables]
docs = "https://cushy.rs/main/docs/cushy"
src = "https://github.com/khonsulabs/cushy/tree/main"

View file

@ -0,0 +1,50 @@
use cushy::value::{Dynamic, IntoValue, Value};
use cushy::widgets::input::InputValue;
fn composition_makewidget() -> impl cushy::widget::MakeWidget {
// ANCHOR: definition
use cushy::widget::{MakeWidget, WidgetInstance};
struct FormField {
label: Value<String>,
field: WidgetInstance,
}
impl FormField {
pub fn new(label: impl IntoValue<String>, field: impl MakeWidget) -> Self {
Self {
label: label.into_value(),
field: field.make_widget(),
}
}
}
// ANCHOR_END: definition
// ANCHOR: makewidget
impl MakeWidget for FormField {
fn make_widget(self) -> WidgetInstance {
self.label
.align_left()
.and(self.field)
.into_rows()
.make_widget()
}
}
FormField::new(
"Label",
Dynamic::<String>::default()
.into_input()
.placeholder("Field"),
)
// ANCHOR_END: makewidget
}
fn main() {
guide_examples::book_example!(composition_makewidget).untested_still_frame();
}
#[test]
fn runs() {
main();
}

View file

@ -0,0 +1,119 @@
use cushy::context::{GraphicsContext, LayoutContext, Trackable};
use cushy::figures::units::{Px, UPx};
use cushy::figures::{IntoSigned, IntoUnsigned, Point, Rect, ScreenScale, Size};
use cushy::kludgine::text::{MeasuredText, TextOrigin};
use cushy::styles::components::IntrinsicPadding;
use cushy::value::{Dynamic, IntoValue, Value};
use cushy::widget::Widget;
use cushy::widgets::input::InputValue;
use cushy::ConstraintLimit;
fn composition_widget() -> impl cushy::widget::MakeWidget {
use cushy::widget::{MakeWidget, WidgetRef};
#[derive(Debug)]
struct FormField {
label: Value<String>,
field: WidgetRef,
}
impl FormField {
pub fn new(label: impl IntoValue<String>, field: impl MakeWidget) -> Self {
Self {
label: label.into_value(),
field: WidgetRef::new(field),
}
}
}
impl FormField {
fn measured_label(
&self,
context: &mut GraphicsContext<'_, '_, '_, '_>,
) -> MeasuredText<Px> {
self.label.invalidate_when_changed(context);
self.label.map(|label| context.gfx.measure_text(label))
}
fn label_and_padding_size(
&self,
context: &mut GraphicsContext<'_, '_, '_, '_>,
) -> Size<UPx> {
let label_size = self.measured_label(context).size.into_unsigned();
let padding = context.get(&IntrinsicPadding).into_upx(context.gfx.scale());
Size::new(label_size.width, label_size.height + padding)
}
}
// ANCHOR: widget-a
impl Widget for FormField {
fn layout(
&mut self,
available_space: Size<ConstraintLimit>,
context: &mut LayoutContext<'_, '_, '_, '_>,
) -> Size<UPx> {
let label_and_padding = self.label_and_padding_size(context);
let field_available_space = Size::new(
available_space.width,
available_space.height - label_and_padding.height,
);
let field = self.field.mounted(context);
let field_size = context.for_other(&field).layout(field_available_space);
let full_size = Size::new(
available_space
.width
.min()
.max(field_size.width)
.max(label_and_padding.width),
field_size.height + label_and_padding.height,
);
context.set_child_layout(
&field,
Rect::new(
Point::new(UPx::ZERO, label_and_padding.height),
Size::new(full_size.width, field_size.height),
)
.into_signed(),
);
full_size
}
// ANCHOR_END: widget-a
// ANCHOR: widget-b
fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_>) {
let label = self.measured_label(context);
context.gfx.draw_measured_text(&label, TextOrigin::TopLeft);
let field = self.field.mounted(context);
context.for_other(&field).redraw();
}
// ANCHOR_END: widget-b
// ANCHOR: widget-c
fn unmounted(&mut self, context: &mut cushy::context::EventContext<'_>) {
self.field.unmount_in(context);
}
}
// ANCHOR_END: widget-c
FormField::new(
"Label",
Dynamic::<String>::default()
.into_input()
.placeholder("Field"),
)
}
fn main() {
guide_examples::book_example!(composition_widget).untested_still_frame();
}
#[test]
fn runs() {
main();
}

View file

@ -0,0 +1,126 @@
use cushy::context::{GraphicsContext, LayoutContext, Trackable};
use cushy::figures::units::{Px, UPx};
use cushy::figures::{IntoSigned, IntoUnsigned, Point, Rect, ScreenScale, Size};
use cushy::kludgine::text::{MeasuredText, TextOrigin};
use cushy::styles::components::IntrinsicPadding;
use cushy::value::{Dynamic, IntoValue, Value};
use cushy::widget::{WrappedLayout, WrapperWidget};
use cushy::widgets::input::InputValue;
use cushy::ConstraintLimit;
fn composition_wrapperwidget() -> impl cushy::widget::MakeWidget {
// ANCHOR: definition
use cushy::widget::{MakeWidget, WidgetRef};
#[derive(Debug)]
struct FormField {
label: Value<String>,
field: WidgetRef,
}
impl FormField {
pub fn new(label: impl IntoValue<String>, field: impl MakeWidget) -> Self {
Self {
label: label.into_value(),
field: WidgetRef::new(field),
}
}
}
// ANCHOR_END: definition
// ANCHOR: helpers
impl FormField {
fn measured_label(
&self,
context: &mut GraphicsContext<'_, '_, '_, '_>,
) -> MeasuredText<Px> {
self.label.invalidate_when_changed(context);
self.label.map(|label| context.gfx.measure_text(label))
}
fn label_and_padding_size(
&self,
context: &mut GraphicsContext<'_, '_, '_, '_>,
) -> Size<UPx> {
let label_size = self.measured_label(context).size.into_unsigned();
let padding = context.get(&IntrinsicPadding).into_upx(context.gfx.scale());
Size::new(label_size.width, label_size.height + padding)
}
}
// ANCHOR_END: helpers
// ANCHOR: wrapperwidget-a
impl WrapperWidget for FormField {
fn child_mut(&mut self) -> &mut WidgetRef {
&mut self.field
}
// ANCHOR_END: wrapperwidget-a
// ANCHOR: wrapperwidget-b
fn adjust_child_constraints(
&mut self,
available_space: Size<ConstraintLimit>,
context: &mut LayoutContext<'_, '_, '_, '_>,
) -> Size<ConstraintLimit> {
let label_and_padding = self.label_and_padding_size(context);
Size::new(
available_space.width,
available_space.height - label_and_padding.height,
)
}
// ANCHOR_END: wrapperwidget-b
// ANCHOR: wrapperwidget-c
fn position_child(
&mut self,
child_size: Size<Px>,
available_space: Size<ConstraintLimit>,
context: &mut LayoutContext<'_, '_, '_, '_>,
) -> WrappedLayout {
let label_and_padding = self.label_and_padding_size(context).into_signed();
let full_size = Size::new(
available_space
.width
.min()
.into_signed()
.max(child_size.width)
.max(label_and_padding.width),
child_size.height + label_and_padding.height,
);
WrappedLayout {
child: Rect::new(
Point::new(Px::ZERO, label_and_padding.height),
Size::new(full_size.width, child_size.height),
),
size: full_size.into_unsigned(),
}
}
// ANCHOR_END: wrapperwidget-c
// ANCHOR: wrapperwidget-d
fn redraw_foreground(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_>) {
let label = self.measured_label(context);
context.gfx.draw_measured_text(&label, TextOrigin::TopLeft);
}
}
// ANCHOR_END: wrapperwidget-d
FormField::new(
"Label",
Dynamic::<String>::default()
.into_input()
.placeholder("Field"),
)
}
fn main() {
guide_examples::book_example!(composition_wrapperwidget).untested_still_frame();
}
#[test]
fn runs() {
main();
}

View file

@ -14,11 +14,9 @@ pub struct BookExampleBuilder {
impl BookExampleBuilder {
pub fn finish(self) -> BookExample {
let mut recorder = self.recorder.finish().expect("error creating recorder");
recorder.window.set_focused(true);
BookExample {
name: self.name,
recorder,
recorder: self.recorder.finish().expect("error creating recorder"),
}
}

View file

@ -8,17 +8,19 @@
- [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)
- [Composing User Interfaces](./about/composition.md)
- [Widgets](./widgets.md)
- [Multi-Widget Layout](./widgets/multi-layout.md)
- [Grid](./widgets/layout/grid.md)
- [Layers](./widgets/layout/layers.md)
- [Stack](./widgets/layout/stack.md)
- [Wrap](./widgets/layout/wrap.md)
- [Single-widget Layout](./widgets/single-layout.md)
- [Align](./widgets/layout/align.md)
- [Collapse](./widgets/layout/collapse.md)
- [Container](./widgets/layout/container.md)
- [Expand](./widgets/layout/expand.md)
- [Grid](./widgets/layout/grid.md)
- [Layers](./widgets/layout/layers.md)
- [Resize](./widgets/layout/resize.md)
- [Stack](./widgets/layout/stack.md)
- [Wrap](./widgets/layout/wrap.md)
- [Controls](./widgets/controls.md)
- [Button]()
- [Canvas]()
@ -32,6 +34,7 @@
- [Scroll]()
- [Select]()
- [Slider]()
- [Space](./widgets/controls/space.md)
- [Switcher]()
- [Image]()
- [TileMap]()
@ -41,4 +44,3 @@
- [Style]()
- [Themed]()
- [ThemedMode]()
- [Space](./widgets/utility/space.md)

View file

@ -0,0 +1,260 @@
# Composing User Interfaces
Designing user interfaces in Cushy can feel different than in other frameworks.
Part of what makes Cushy unique is its [reactive data model](./reactive.md). The
other significant architectural design was to focus on composition.
The content area of each [window][window] in Cushy is a single
[widget](./widgets.md). A window cannot have more than one widget at its root.
So, how is a user interface with multiple widgets built? Through composition.
Cushy has a category of widgets dedicated to composing multiple widgets into a
single widget: [multi-widget layout widgets](../widgets/multi-layout.md). For
example, the [`Stack`](../widgets/layout/stack.md) widget positions its children
horizontally as a set of columns or vertically as a set of rows, while the
[`Layers`](../widgets/layout/layers.md) widget positions its children on top of
each other in the Z direction.
The power to this approach is that adding new layout strategies is as simple as
implementing a new [`Widget`][widget] that implements
[`Widget::layout`][widget-layout], and that new widget can be used anywhere in
Cushy that any other widget can be used.
## Creating Composable Widgets
At the core of widget composition are the [`MakeWidget`][makewidget] and
[`MakeWidgetWithTag`][makewidgettag] traits. The goal of both of these traits is
simple: transform `self` into a [`WidgetInstance`][widgetinstance].
A `WidgetInstance` is a type-erased, reference-counted instance of a
[`Widget`][widget] trait implementation. This type enables widgets to refer to
other widgets without knowing their underlying types. For example, the label of
a [`Button`][button] can be any widget, not just a string, because it accepts a
`MakeWidget` implementor in its constructor.
Let's see the various ways that Cushy offers to create the same component: a
labeled form field.
### Example: A `FormField` widget
Let's design a way to present a widget resembling this reusable structure:
```text
Label
Field
```
The structure definition for this widget might look like this:
```rust,no_run,no_playground
{{#include ../../guide-examples/examples/composition-makewidget.rs:definition}}
```
While it would arguably be better to accept `label` as another `WidgetInstance`,
by focusing on composing a single widget, this example can also include a
utility trait: [`WrapperWidget`][wrapperwidget].
#### Approach A: Using the `MakeWidget` trait
The simplest approach to making new widgets is to avoid implementing them at
all! In this case, we can reuse the existing [`Stack`][stack] and
[`Align`][align] widgets to position the label and field. So, instead of
creating a `Widget` implementation, if we implement `MakeWidget`, we can compose
our interface using existing widgets:
```rust,no_run,no_playground
{{#include ../../guide-examples/examples/composition-makewidget.rs:makewidget}}
```
The example `FormField` when rendered looks like this:
![MakeWidget Example Output](../examples/composition_makewidget.png)
#### Approach B: Using the `WrapperWidget` trait
The [`WrapperWidget`][wrapperwidget] trait is an alternate trait from `Widget`
that makes it less error-prone to implement a widget that wraps a single other
child widget. The only required function is
[`WrapperWidget::child_mut`][child-mut], which returns a `&mut WidgetRef`.
Previously, we were using [`WidgetInstance`][widgetinstance] to store our field.
When a `WidgetInstance` is mounted inside of another widget in a window, a
[`MountedWidget`][mountedwidget] is returned. A [`WidgetRef`][widgetref] is a
type that manages mounting and unmounting a widget automatically through its API
usage. It also keeps track of each window's `MountedWidget`.
Updating the type to use `WidgetRef` is fairly straightforward, and does not
impact the type's public API:
```rust,no_run,no_playground
{{#include ../../guide-examples/examples/composition-wrapperwidget.rs:definition}}
```
Instead of calling `field.make_widget()`, we now use `WidgetRef::new(field)`.
Now, let's look at the `WrapperWidget` implementation:
```rust,no_run,no_playground
{{#include ../../guide-examples/examples/composition-wrapperwidget.rs:wrapperwidget-a}}
```
As mentioned before, this `child_mut` is the only required function. All other
functions provide default behaviors that ensure that the child is mounted,
positioned, and rendered for us. Running this example without the rest of the
implementation would show only the field without our label.
Before we can start drawing and positioning the field based on the label's size,
let's define a couple of helper functions that we can use to implement the
`WrapperWidget` functions needed:
```rust,no_run,no_playground
{{#include ../../guide-examples/examples/composition-wrapperwidget.rs:helpers}}
```
The first function uses our graphics context to return Kludgine's
[`MeasuredText<Px>`][measuredtext] type. The second function looks up the
current size to use for padding, and adds it to the height of the measured text.
> **Advanced Tip**: `MeasuredText` not only contains the dimensions of the text
> we asked it to measure, it also has all the information necessary to draw all
> the glyphs necessary in the future. It would be more efficient to cache this
> data structure in a [`WindowLocal`][windowlocal] and only re-cache the text
> layout when needed.
The [`Widget::layout()`][layout] implementation for `WrapperWidget`s splits the
layout process into multiple steps. The goal is to allow wrapper widget authors
to be able to customize as little or as much of the process as needed. The first
function we are going to use is
[`adjust_child_constraints()`][adjust-constraints]:
```rust,no_run,no_playground
{{#include ../../guide-examples/examples/composition-wrapperwidget.rs:wrapperwidget-b}}
```
`adjust_child_constraints()` is responsible for adjusting the incoming
`available_space` constraints by whatever space we need to surround the child.
In our case, we need to subtract the label and padding height from the available
height. By reducing the available space, we ensure that even if a field is
wrapped in an [`Expand`][expand] widget, we will have already allocated space
for the label to be visible with an appropriate amount of padding.
The next step in the layout process is to position the child:
```rust,no_run,no_playground
{{#include ../../guide-examples/examples/composition-wrapperwidget.rs:wrapperwidget-c}}
```
The above implementation calculates the full size's width by taking the maximum
of the [minimum available][constraintlimit-min] width, the child's width, and
the label's width. It calcaultes the full size's height by adding the child's
height and the label and padding height.
The result of this function is a structure that contains the child's rectangle
and the total size this widget is requesting. The child's rectangle is placed
below the label and padding, and its width is set to `full_size.width`. This is
to mimic the behavior our original choice of placing the widgets in a stack. In
our example, this is what stretches the text input field to be the full width of
the field.
The final step is to draw the label:
```rust,no_run,no_playground
{{#include ../../guide-examples/examples/composition-wrapperwidget.rs:wrapperwidget-d}}
```
Because the [`Widget::redraw`][redraw] implementation takes care of drawing the
field for us, all this function needed to do was draw the label.
This is the result of using our new implementation:
![WrapperWidget Example Output](../examples/composition_wrapperwidget.png)
It looks the exact same as the previous image. That was our goal! Rest assured
that this image [was generated][book-example-wrapper] using the `WrapperWidget`
implementation shown.
#### Approach C: Using the `Widget` trait
Implementing [`Widget`][widget] is very similar to implementing
[`WrapperWidget`][wrapperwidget], except that we are in full control of layout
and rendering. Since our `WrapperWidget` implementation needed to fully adjust
the layout, our `layout()` function is basically just the combination of the two
layout functions from before:
```rust,no_run,no_playground
{{#include ../../guide-examples/examples/composition-widget.rs:widget-a}}
```
The major difference in this function is that we are manually calling `layout()`
on our field widget. This is done by creating a context for our field
(`context.for_other()`), and calling the layout function on that context. After
we receive the field's size, we must call
[`set_child_layout()`][set-child-layout] to finish laying out the field.
Finally, the result of the `layout()` function is the full size that the field
needs. With layout done, rendering is the next step:
```rust,no_run,no_playground
{{#include ../../guide-examples/examples/composition-widget.rs:widget-b}}
```
This function isn't much more complicated than the `WrapperWidget`'s
implementation. The extra two lines again use `context.for_other()` to create a
context for the `field` widget, but this time we call the field's `redraw()`
function instead.
Finally, we have one last bit of housekeeping that `WrapperWidget` did
automatically: unmounting the field when the form field itself is unmounted.
This is important to minimize memory usage by ensuring that if the widget is
shared between multiple windows, each window's state is cleaned up indepdent of
the widget itself:
```rust,no_run,no_playground
{{#include ../../guide-examples/examples/composition-widget.rs:widget-c}}
```
And with this, we can look at an identical picture that shows [this
implementation][book-example-widget] works the same as the previous
implementations:
![Widget Example Output](../examples/composition_widget.png)
## Conclusion
Cushy tries to provide the tools needed to avoid implementing your own widgets
by utilizing composition. It also has many tools for creating reusable,
composible widgets. If you find yourself uncertain of what path to use when
creating a reusable component, try using a [`MakeWidget`][makewidget]
implementation initially.
If using `MakeWidget` is cumbersome or doesn't expose as much functionality as
needed, you may still find that you can utilize `MakeWidget` along with a
simpler custom widget than implementing a more complex, multi-part widget. This
approach is how the built-in [`Checkbox`][checkbox], [`Radio`][radio] and
[`Select`][select] widgets are all built using a [`Button`][button].
[window]: <{{ docs }}/window/struct.Window.html>
[widget]: <{{ docs }}/widget/trait.Widget.html>
[layout]: <{{ docs }}/widget/trait.Widget.html#method.layout>
[redraw]: <{{ docs }}/widget/trait.Widget.html#method.redraw>
[mountedwidget]: <{{ docs }}/widget/struct.MountedWidget.html>
[widgetref]: <{{ docs }}/widget/struct.WidgetRef.html>
[wrapperwidget]: <{{ docs }}/widget/trait.WrapperWidget.html>
[child-mut]: <{{ docs }}/widget/trait.WrapperWidget.html#tymethod.child_mut>
[adjust-constraints]: <{{ docs }}/widget/trait.WrapperWidget.html#method.adjust_child_constraints>
[makewidget]: <{{ docs }}/widget/trait.MakeWidget.html>
[makewidgettag]: <{{ docs }}/widget/trait.MakeWidgetWithTag.html>
[widget-layout]: <{{ docs }}/widget/trait.Widget.html#method.layout>
[widgetinstance]: <{{ docs }}/widget/struct.WidgetInstance.html>
[button]: ../widgets/controls/button.md
[radio]: ../widgets/controls/radio.md
[select]: ../widgets/controls/select.md
[checkbox]: ../widgets/controls/checkbox.md
[stack]: ../widgets/layout/stack.md
[align]: ../widgets/layout/align.md
[expand]: ../widgets/layout/expand.md
[book-example-wrapper]: <{{ src }}/guide/guide-examples/examples/composition-wrapperwidget.rs>
[book-example-widget]: <{{ src }}/guide/guide-examples/examples/composition-wrapperwidget.rs>
[measuredtext]: <https://docs.rs/kludgine/latest/kludgine/text/struct.MeasuredText.html>
[windowlocal]: <{{ docs }}/window/struct.WindowLocal.html>
[constraintlimit-min]: <{{ docs }}/enum.ConstraintLimit.html#method.min>
[set-child-layout]: <{{ docs }}/context/struct.LayoutContext.html#method.set_child_layout>

View file

@ -1,7 +1,7 @@
# Cushy's Philosophies
There are a lot of GUI libraries with wildly varying approaches to how UIs are
displayed. Here's the philosophies that drive Cushy's design:
displayed. Here are the philosophies that drive Cushy's design:
- Cushy retains information between redraws so that many events can be handled
without redrawing the user interface.
@ -25,9 +25,12 @@ displayed. Here's the philosophies that drive Cushy's design:
From an implementation standpoint, Cushy has these goals:
- For graphics, provide a wgpu-centric library that exposes a rendering API
inspired by wgpu's Encapsulating Graphics Work article.
- For windowing, embrace winit and route input events to the correct widgets.
This allows widgets to support any features that winit can support.
- For graphics, provide a [wgpu][wgpu]-centric library that exposes a rendering
API inspired by wgpu's Encapsulating Graphics Work article.
- For windowing, embrace [winit][winit] and route input events to the correct
widgets. This allows widgets to support any features that winit can support.
- Cushy should be able to idle at close to 0% CPU. Cushy should not redraw
unless needed.
[wgpu]: <https://github.com/gfx-rs/wgpu>
[winit]: <https://github.com/rust-windowing/winit>

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

View file

@ -1,4 +1,4 @@
# Introduction
# Welcome to the Cushy User's Guide
This is a user's guide for [Cushy][cushy], a [Rust][rust] GUI crate. The
[documentation][docs] is a great resource for finding information about specific

21
guide/src/widgets.md Normal file
View file

@ -0,0 +1,21 @@
# Widgets
Types that implement [`Widget`][widget] are the [building
blocks](./about/widgets.md) of Cushy user interfaces. The built-in widgets each
aim to serve a single purpose or solve a single problem. [Through
composition](./about/composition.md), complex user interfaces can be built by
combining these single-purpose widgets.
This section is organized into four categories of widgets:
- [Multi-widget Layout Widgets](./multi-layout.md): Widgets that are designed to
layout multiple widgets as a single widget.
- [Single-widget Layout Widgets](./single-layout.md): Widgets that are designed
to influence a single widget's layout.
- [Controls](./controls.md): Widgets that are visible to the user to present
and/or interact with data.
- [Utility Widgets](./utility.md): Widgets that have no direct layout or visual
presentation. This type of widget usually associates extra data that may
impact how child widgets are presented.
[widget]: <{{ docs }}/widget/trait.Widget.html>

View file

@ -1,4 +0,0 @@
# Layout
Widgets in this section have a primary purpose of performing some layout
functionality on one or more child widgets.

View file

@ -91,4 +91,4 @@ Any widget can be aligned to the bottom using
[flexauto]: <{{ docs }}/styles/enum.FlexibleDimension.html#variant.Auto>
[fit-vert]: <https://cushy.rs/main/docs/cushy/widget/trait.MakeWidget.html#method.fit_vertically>
[fit-horiz]: <https://cushy.rs/main/docs/cushy/widget/trait.MakeWidget.html#method.fit_horizontally>
[space]: ../utility/space.md
[space]: ../controls/space.md

View file

@ -1,10 +1,10 @@
# Layers
The [`Layers`][layers] widget lays its [`Children`][children] widgets on top of
The [`Layers`][layers] widget lays its [`WidgetList`][children] widgets on top of
each other in the Z orientation.
When computing its size, it uses the largest width and height from all of its
children.
[Layers]: <{{ docs }}/widgets/layers/struct.Layers.html>
[children]: <{{ docs }}/widget/struct.Children.html>
[children]: <{{ docs }}/widget/struct.WidgetList.html>

View file

@ -1,18 +1,18 @@
# Stack
The [`Stack`][stack] widget lays a set of [`Children`][children] as either a set
The [`Stack`][stack] widget lays a set of [`WidgetList`][children] as either a set
of columns or rows. It is a convenient way to construct a 1D
[`Grid`](./grid.md). It can be constructed using either:
- [`Stack::rows`][rows]/[`Children::into_rows()`][into_rows]
- [`Stack::columns`][columns]/[`Children::into_columns()`][into_columns]
- [`Stack::rows`][rows]/[`WidgetList::into_rows()`][into_rows]
- [`Stack::columns`][columns]/[`WidgetList::into_columns()`][into_columns]
The stack widget places spacing between each element called a [gutter][gutter].
[stack]: <{{ docs }}/widgets/stack/struct.Stack.html>
[children]: <{{ docs }}/widget/struct.Children.html>
[children]: <{{ docs }}/widget/struct.WidgetList.html>
[rows]: <{{ docs }}/widgets/stack/struct.Stack.html#method.rows>
[columns]: <{{ docs }}/widgets/stack/struct.Stack.html#method.columns>
[into_columns]: <{{ docs }}/widget/struct.Children.html#method.into_columns>
[into_rows]: <{{ docs }}/widget/struct.Children.html#method.into_rows>
[into_columns]: <{{ docs }}/widget/struct.WidgetList.html#method.into_columns>
[into_rows]: <{{ docs }}/widget/struct.WidgetList.html#method.into_rows>
[gutter]: <{{ docs }}/widgets/stack/struct.Stack.html#method.gutter>

View file

@ -1,6 +1,6 @@
# Wrap
The [`Wrap`][wrap] widget lays its [`Children`][children] widgets out in a
The [`Wrap`][wrap] widget lays its [`WidgetList`][children] widgets out in a
fashion that mimics text layout.
It works by measuring each child with [`SizeToFit`][size-to-fit] and laying out
@ -26,7 +26,7 @@ Once the widgets have been grouped into rows, the [alignment][align] and
[wrap]: <{{ docs }}/widgets/wrap/struct.Wrap.html>
[wrapalign]: <{{ docs }}/widgets/wrap/enum.WrapAlign.html>
[children]: <{{ docs }}/widget/struct.Children.html>
[children]: <{{ docs }}/widget/struct.WidgetList.html>
[size-to-fit]: <{{ docs }}/enum.ConstraintLimit.html#variant.SizeToFit>
[spacing]: <{{ docs }}/widgets/wrap/struct.Wrap.html#method.spacing>
[align]: <{{ docs }}/widgets/wrap/struct.Wrap.html#method.align>

View file

@ -0,0 +1,10 @@
# Multi-Widget Layout
Widgets in this section have a primary purpose of performing some layout
functionality on a collection of widgets. These currently are:
- [Grid](./layout/grid.md): A 2D grid where each cell is a widget.
- [Layers](./layout/layers.md): A Z-direction stack of widgets.
- [Stack](./layout/stack.md): A 1D grid of either rows or columns.
- [Wrap](./layout/wrap.md): Widgets are laid out horizontally, wrapping into
multiple rows if needed.

View file

@ -0,0 +1,17 @@
# Single-widget Layout
Widgets in this section have a primary purpose of performing some layout
functionality on a single child widget. To use one of these layout widgets with
multiple widgets, first lay out the widgets using one of the [multi-widget
layout widgets](./multi-layout.md).
The widgets in this category are:
- [Align](./layout/align.md): Aligns its child relative to its parent's edges.
- [Collapse](./layout/collapse.md): Shows/hides its child.
- [Container](./layout/container.md): Visually contains a child. Useful for
grouping related content visually.
- [Expand](./layout/expand.md): Resizes its child to occupy as much space as
available.
- [Resize](./layout/resize.md): Restricts its child's size to be within a range
of widths and/or heights.

View file

@ -15,6 +15,7 @@ pub struct PendingApp {
impl PendingApp {
/// The shared resources this application utilizes.
#[must_use]
pub const fn cushy(&self) -> &Cushy {
&self.cushy
}

View file

@ -5,7 +5,7 @@ use std::fmt::Debug;
use alot::OrderedLots;
use crate::value::{Dynamic, DynamicReader, ForEach, Source, WeakDynamic};
use crate::widget::{Children, MakeWidget, WidgetInstance};
use crate::widget::{MakeWidget, WidgetInstance, WidgetList};
use crate::widgets::grid::{Grid, GridWidgets};
use crate::window::Window;
use crate::{Open, PendingApp};
@ -203,7 +203,7 @@ impl DebugSection {
children
.iter()
.map(|section| section.map_ref(|section| section.widget.clone()))
.collect::<Children>()
.collect::<WidgetList>()
});
let parent = parent.map(Dynamic::downgrade);

View file

@ -21,7 +21,7 @@ use crate::animation::{AnimationHandle, DynamicTransition, IntoAnimate, LinearIn
use crate::context::{self, Trackable, WidgetContext};
use crate::utils::{run_in_bg, IgnorePoison, WithClone};
use crate::widget::{
Children, MakeWidget, MakeWidgetWithTag, OnceCallback, WidgetId, WidgetInstance,
MakeWidget, MakeWidgetWithTag, OnceCallback, WidgetId, WidgetInstance, WidgetList,
};
use crate::widgets::{Label, Radio, Select, Space, Switcher};
use crate::window::WindowHandle;
@ -2442,7 +2442,7 @@ where
}
}
impl GetWidget<usize> for Children {
impl GetWidget<usize> for WidgetList {
fn get<'a>(&'a self, key: &usize) -> Option<&'a WidgetInstance> {
(**self).get(*key)
}

View file

@ -1117,8 +1117,8 @@ pub trait MakeWidget: Sized {
}
/// Returns a collection of widgets using `self` and `other`.
fn and(self, other: impl MakeWidget) -> Children {
let mut children = Children::new();
fn and(self, other: impl MakeWidget) -> WidgetList {
let mut children = WidgetList::new();
children.push(self);
children.push(other);
children
@ -1922,14 +1922,25 @@ impl WidgetGuard<'_> {
}
}
/// A list of [`Widget`]s.
/// A list of [`Widget`]s without a layout strategy.
///
/// To use a `WidgetList` in a user interface, a choice must be made for how
/// each child should be positioned. The built-in widgets that can layout a
/// `WidgetList` are:
///
/// - As rows: [`Stack::rows`] / [`Self::into_rows`]
/// - As columns: [`Stack::columns`] / [`Self::into_columns`]
/// - Positioned on top of each other in the Z orientation: [`Layers::new`] /
/// [`Self::into_layers`]
/// - Layout horizontally, wrapping into multiple rows as needed: [`Wrap::new`]
/// / [`Self::into_wrap`].
#[derive(Default, Eq, PartialEq)]
#[must_use]
pub struct Children {
pub struct WidgetList {
ordered: Vec<WidgetInstance>,
}
impl Children {
impl WidgetList {
/// Returns an empty list.
pub const fn new() -> Self {
Self {
@ -2021,7 +2032,7 @@ impl Children {
/// Returns a [`Wrap`] that lays the children out horizontally, wrapping
/// into additional rows as needed.
#[must_use]
pub fn wrap(self) -> Wrap {
pub fn into_wrap(self) -> Wrap {
Wrap::new(self)
}
@ -2066,13 +2077,13 @@ impl Children {
}
}
impl Debug for Children {
impl Debug for WidgetList {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
Debug::fmt(&self.ordered, f)
}
}
impl Dynamic<Children> {
impl Dynamic<WidgetList> {
/// Returns `self` as a vertical [`Stack`] of rows.
#[must_use]
pub fn into_rows(self) -> Stack {
@ -2126,7 +2137,7 @@ impl Dynamic<Children> {
}
}
impl<W> FromIterator<W> for Children
impl<W> FromIterator<W> for WidgetList
where
W: MakeWidget,
{
@ -2137,7 +2148,7 @@ where
}
}
impl Deref for Children {
impl Deref for WidgetList {
type Target = [WidgetInstance];
fn deref(&self) -> &Self::Target {
@ -2145,13 +2156,13 @@ impl Deref for Children {
}
}
impl DerefMut for Children {
impl DerefMut for WidgetList {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.ordered
}
}
impl<'a> IntoIterator for &'a Children {
impl<'a> IntoIterator for &'a WidgetList {
type IntoIter = slice::Iter<'a, WidgetInstance>;
type Item = &'a WidgetInstance;
@ -2160,7 +2171,7 @@ impl<'a> IntoIterator for &'a Children {
}
}
/// A change to perform during [`Children::synchronize_with`].
/// A change to perform during [`WidgetList::synchronize_with`].
pub enum ChildrenSyncChange {
/// Insert a new widget at the given index.
Insert(usize, WidgetInstance),
@ -2174,7 +2185,7 @@ pub enum ChildrenSyncChange {
///
/// This collection is a helper aimed at making it easier to build widgets that
/// contain multiple children widgets. It is used in conjunction with a
/// `Value<Children>`.
/// `Value<WidgetList>`.
#[derive(Debug)]
pub struct MountedChildren<T = MountedWidget> {
generation: Option<Generation>,
@ -2186,10 +2197,14 @@ where
T: MountableChild,
{
/// Mounts and unmounts all children needed to be in sync with `children`.
pub fn synchronize_with(&mut self, children: &Value<Children>, context: &mut EventContext<'_>) {
pub fn synchronize_with(
&mut self,
children: &Value<WidgetList>,
context: &mut EventContext<'_>,
) {
let current_generation = children.generation();
if current_generation.map_or_else(
|| children.map(Children::len) != self.children.len(),
|| children.map(WidgetList::len) != self.children.len(),
|gen| Some(gen) != self.generation,
) {
self.generation = current_generation;

View file

@ -16,7 +16,7 @@ use crate::context::{AsEventContext, EventContext, GraphicsContext, LayoutContex
use crate::utils::IgnorePoison;
use crate::value::{Destination, Dynamic, DynamicGuard, IntoValue, Source, Value};
use crate::widget::{
Callback, Children, MakeWidget, MountedChildren, MountedWidget, Widget, WidgetId, WidgetRef,
Callback, MakeWidget, MountedChildren, MountedWidget, Widget, WidgetId, WidgetList, WidgetRef,
WrapperWidget,
};
use crate::widgets::container::ContainerShadow;
@ -26,13 +26,13 @@ use crate::ConstraintLimit;
#[derive(Debug)]
pub struct Layers {
/// The children that are laid out as layers with index 0 being the lowest (bottom).
pub children: Value<Children>,
pub children: Value<WidgetList>,
mounted: MountedChildren,
}
impl Layers {
/// Returns a new instance that lays out `children` as layers.
pub fn new(children: impl IntoValue<Children>) -> Self {
pub fn new(children: impl IntoValue<WidgetList>) -> Self {
Self {
children: children.into_value(),
mounted: MountedChildren::default(),

View file

@ -1,4 +1,4 @@
//! A widget that combines a collection of [`Children`] widgets into one.
//! A widget that combines a collection of [`WidgetList`] widgets into one.
use figures::units::UPx;
use figures::{IntoSigned, Rect, Round, ScreenScale, Size};
@ -7,18 +7,18 @@ use crate::context::{AsEventContext, EventContext, GraphicsContext, LayoutContex
use crate::styles::components::IntrinsicPadding;
use crate::styles::FlexibleDimension;
use crate::value::{Generation, IntoValue, Value};
use crate::widget::{Children, ChildrenSyncChange, MountedWidget, Widget, WidgetRef};
use crate::widget::{ChildrenSyncChange, MountedWidget, Widget, WidgetList, WidgetRef};
use crate::widgets::grid::{GridDimension, GridLayout, Orientation};
use crate::widgets::{Expand, Resize};
use crate::ConstraintLimit;
/// A widget that displays a collection of [`Children`] widgets in a
/// A widget that displays a collection of [`WidgetList`] widgets in a
/// [orientation](Orientation).
#[derive(Debug)]
pub struct Stack {
orientation: Orientation,
/// The children widgets that belong to this array.
pub children: Value<Children>,
pub children: Value<WidgetList>,
/// The amount of space to place between each widget.
pub gutter: Value<FlexibleDimension>,
layout: GridLayout,
@ -28,7 +28,7 @@ pub struct Stack {
impl Stack {
/// Returns a new widget with the given orientation and widgets.
pub fn new(orientation: Orientation, widgets: impl IntoValue<Children>) -> Self {
pub fn new(orientation: Orientation, widgets: impl IntoValue<WidgetList>) -> Self {
Self {
orientation,
children: widgets.into_value(),
@ -40,12 +40,12 @@ impl Stack {
}
/// Returns a new instance that displays `widgets` in a series of columns.
pub fn columns(widgets: impl IntoValue<Children>) -> Self {
pub fn columns(widgets: impl IntoValue<WidgetList>) -> Self {
Self::new(Orientation::Column, widgets)
}
/// Returns a new instance that displays `widgets` in a series of rows.
pub fn rows(widgets: impl IntoValue<Children>) -> Self {
pub fn rows(widgets: impl IntoValue<WidgetList>) -> Self {
Self::new(Orientation::Row, widgets)
}
@ -60,7 +60,7 @@ impl Stack {
let current_generation = self.children.generation();
self.children.invalidate_when_changed(context);
if current_generation.map_or_else(
|| self.children.map(Children::len) != self.layout.len(),
|| self.children.map(WidgetList::len) != self.layout.len(),
|gen| Some(gen) != self.layout_generation,
) {
self.layout_generation = self.children.generation();

View file

@ -9,7 +9,7 @@ use crate::context::{AsEventContext, GraphicsContext, LayoutContext, Trackable};
use crate::styles::components::{IntrinsicPadding, LayoutOrder};
use crate::styles::{FlexibleDimension, HorizontalOrder};
use crate::value::{IntoValue, Value};
use crate::widget::{Children, MountedChildren, Widget};
use crate::widget::{MountedChildren, Widget, WidgetList};
use crate::ConstraintLimit;
/// A widget that lays its children out horizontally, wrapping into multiple
@ -20,7 +20,7 @@ use crate::ConstraintLimit;
#[derive(Debug)]
pub struct Wrap {
/// The children to wrap.
pub children: Value<Children>,
pub children: Value<WidgetList>,
/// The horizontal alignment for widgets on the same row.
pub align: Value<WrapAlign>,
/// The vertical alignment for widgets on the same row.
@ -34,7 +34,7 @@ pub struct Wrap {
impl Wrap {
/// Returns a new widget that wraps `children`.
#[must_use]
pub fn new(children: impl IntoValue<Children>) -> Self {
pub fn new(children: impl IntoValue<WidgetList>) -> Self {
Self {
children: children.into_value(),
align: Value::default(),

View file

@ -1,4 +1,5 @@
//! Types for displaying a [`Widget`] inside of a desktop window.
//! Types for displaying a [`Widget`](crate::widget::Widget) inside of a desktop
//! window.
use std::cell::RefCell;
use std::collections::hash_map;
@ -2499,7 +2500,8 @@ impl CushyWindowBuilder {
#[must_use]
pub fn finish_virtual(self, device: &wgpu::Device, queue: &wgpu::Queue) -> VirtualWindow {
let mut state = VirtualState::new();
let cushy = self.finish(&mut state, device, queue);
let mut cushy = self.finish(&mut state, device, queue);
cushy.set_focused(true);
VirtualWindow {
cushy,