From 6ad6cca32d5f409a386a188ae96441a131a13a91 Mon Sep 17 00:00:00 2001 From: Jonathan Johnson Date: Tue, 9 Jan 2024 13:24:15 -0800 Subject: [PATCH] Children renamed to WidgetList Plus more work on the user's guide, which inspired the rename. --- .crate-docs.md | 3 + .rustme/docs.md | 3 + CHANGELOG.md | 4 +- README.md | 3 + examples/contacts.rs | 4 +- examples/wrap.rs | 6 +- guide/book.toml | 1 + .../examples/composition-makewidget.rs | 50 ++++ .../examples/composition-widget.rs | 119 ++++++++ .../examples/composition-wrapperwidget.rs | 126 +++++++++ guide/guide-examples/src/lib.rs | 4 +- guide/src/SUMMARY.md | 16 +- guide/src/about/composition.md | 260 ++++++++++++++++++ guide/src/about/philosophies.md | 13 +- guide/src/examples/composition_makewidget.png | Bin 0 -> 9073 bytes guide/src/examples/composition_widget.png | Bin 0 -> 9095 bytes .../examples/composition_wrapperwidget.png | Bin 0 -> 9095 bytes guide/src/intro.md | 2 +- guide/src/widgets.md | 21 ++ .../widgets/{utility => controls}/space.md | 0 guide/src/widgets/layout.md | 4 - guide/src/widgets/layout/align.md | 2 +- guide/src/widgets/layout/layers.md | 4 +- guide/src/widgets/layout/stack.md | 12 +- guide/src/widgets/layout/wrap.md | 4 +- guide/src/widgets/multi-layout.md | 10 + guide/src/widgets/single-layout.md | 17 ++ src/app.rs | 1 + src/debug.rs | 4 +- src/value.rs | 4 +- src/widget.rs | 47 ++-- src/widgets/layers.rs | 6 +- src/widgets/stack.rs | 16 +- src/widgets/wrap.rs | 6 +- src/window.rs | 6 +- 35 files changed, 705 insertions(+), 73 deletions(-) create mode 100644 guide/guide-examples/examples/composition-makewidget.rs create mode 100644 guide/guide-examples/examples/composition-widget.rs create mode 100644 guide/guide-examples/examples/composition-wrapperwidget.rs create mode 100644 guide/src/about/composition.md create mode 100644 guide/src/examples/composition_makewidget.png create mode 100644 guide/src/examples/composition_widget.png create mode 100644 guide/src/examples/composition_wrapperwidget.png create mode 100644 guide/src/widgets.md rename guide/src/widgets/{utility => controls}/space.md (100%) delete mode 100644 guide/src/widgets/layout.md create mode 100644 guide/src/widgets/multi-layout.md create mode 100644 guide/src/widgets/single-layout.md diff --git a/.crate-docs.md b/.crate-docs.md index 9310783..3f890e6 100644 --- a/.crate-docs.md +++ b/.crate-docs.md @@ -1,3 +1,6 @@ + + + ![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/) diff --git a/.rustme/docs.md b/.rustme/docs.md index b8fb05b..35db32f 100644 --- a/.rustme/docs.md +++ b/.rustme/docs.md @@ -1,3 +1,6 @@ + + + ![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$) diff --git a/CHANGELOG.md b/CHANGELOG.md index cd4fd6d..626f581 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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` instead of `IntoValue`. -- `Dynamic::wrap` has been renamed to `into_wrap` for consistency. +- `Dynamic::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 diff --git a/README.md b/README.md index e30ca24..6c77949 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,8 @@ # Cushy + + + ![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/) diff --git a/examples/contacts.rs b/examples/contacts.rs index 1fc8e61..0d797d4 100644 --- a/examples/contacts.rs +++ b/examples/contacts.rs @@ -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::() + .collect::() } }); diff --git a/examples/wrap.rs b/examples/wrap.rs index 3908b96..f1f4146 100644 --- a/examples/wrap.rs +++ b/examples/wrap.rs @@ -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::(); + .collect::(); let align = Dynamic::::default(); let vertical_align = Dynamic::::default(); @@ -50,7 +50,7 @@ fn main() -> cushy::Result { .h3() .and( words - .wrap() + .into_wrap() .align(align) .vertical_align(vertical_align) .expand_horizontally() diff --git a/guide/book.toml b/guide/book.toml index 5c28854..b34a054 100644 --- a/guide/book.toml +++ b/guide/book.toml @@ -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" diff --git a/guide/guide-examples/examples/composition-makewidget.rs b/guide/guide-examples/examples/composition-makewidget.rs new file mode 100644 index 0000000..a3e06ab --- /dev/null +++ b/guide/guide-examples/examples/composition-makewidget.rs @@ -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, + field: WidgetInstance, + } + + impl FormField { + pub fn new(label: impl IntoValue, 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::::default() + .into_input() + .placeholder("Field"), + ) + // ANCHOR_END: makewidget +} + +fn main() { + guide_examples::book_example!(composition_makewidget).untested_still_frame(); +} + +#[test] +fn runs() { + main(); +} diff --git a/guide/guide-examples/examples/composition-widget.rs b/guide/guide-examples/examples/composition-widget.rs new file mode 100644 index 0000000..f87b633 --- /dev/null +++ b/guide/guide-examples/examples/composition-widget.rs @@ -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, + field: WidgetRef, + } + + impl FormField { + pub fn new(label: impl IntoValue, field: impl MakeWidget) -> Self { + Self { + label: label.into_value(), + field: WidgetRef::new(field), + } + } + } + + impl FormField { + fn measured_label( + &self, + context: &mut GraphicsContext<'_, '_, '_, '_>, + ) -> MeasuredText { + 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 { + 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, + context: &mut LayoutContext<'_, '_, '_, '_>, + ) -> Size { + 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::::default() + .into_input() + .placeholder("Field"), + ) +} + +fn main() { + guide_examples::book_example!(composition_widget).untested_still_frame(); +} + +#[test] +fn runs() { + main(); +} diff --git a/guide/guide-examples/examples/composition-wrapperwidget.rs b/guide/guide-examples/examples/composition-wrapperwidget.rs new file mode 100644 index 0000000..f552f28 --- /dev/null +++ b/guide/guide-examples/examples/composition-wrapperwidget.rs @@ -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, + field: WidgetRef, + } + + impl FormField { + pub fn new(label: impl IntoValue, 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 { + 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 { + 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, + context: &mut LayoutContext<'_, '_, '_, '_>, + ) -> Size { + 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, + available_space: Size, + 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::::default() + .into_input() + .placeholder("Field"), + ) +} + +fn main() { + guide_examples::book_example!(composition_wrapperwidget).untested_still_frame(); +} + +#[test] +fn runs() { + main(); +} diff --git a/guide/guide-examples/src/lib.rs b/guide/guide-examples/src/lib.rs index 0e9e8fc..4853fbf 100644 --- a/guide/guide-examples/src/lib.rs +++ b/guide/guide-examples/src/lib.rs @@ -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"), } } diff --git a/guide/src/SUMMARY.md b/guide/src/SUMMARY.md index 630ba06..4935e04 100644 --- a/guide/src/SUMMARY.md +++ b/guide/src/SUMMARY.md @@ -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) diff --git a/guide/src/about/composition.md b/guide/src/about/composition.md new file mode 100644 index 0000000..4e12b60 --- /dev/null +++ b/guide/src/about/composition.md @@ -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`][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]: +[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> diff --git a/guide/src/about/philosophies.md b/guide/src/about/philosophies.md index 2627e15..dd54f02 100644 --- a/guide/src/about/philosophies.md +++ b/guide/src/about/philosophies.md @@ -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]: +[winit]: diff --git a/guide/src/examples/composition_makewidget.png b/guide/src/examples/composition_makewidget.png new file mode 100644 index 0000000000000000000000000000000000000000..d0537916493c81c9da9887301d2b9b49511fbf05 GIT binary patch literal 9073 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 literal 0 HcmV?d00001 diff --git a/guide/src/examples/composition_widget.png b/guide/src/examples/composition_widget.png new file mode 100644 index 0000000000000000000000000000000000000000..da4c9e2726693a402881f3db3ac51597b600235a GIT binary patch literal 9095 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 jrFVAt8;ZpZ!`Qlc$EJdfqSXHYh{79_ literal 0 HcmV?d00001 diff --git a/guide/src/examples/composition_wrapperwidget.png b/guide/src/examples/composition_wrapperwidget.png new file mode 100644 index 0000000000000000000000000000000000000000..da4c9e2726693a402881f3db3ac51597b600235a GIT binary patch literal 9095 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 jrFVAt8;ZpZ!`Qlc$EJdfqSXHYh{79_ literal 0 HcmV?d00001 diff --git a/guide/src/intro.md b/guide/src/intro.md index 7bffbd7..5609979 100644 --- a/guide/src/intro.md +++ b/guide/src/intro.md @@ -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 diff --git a/guide/src/widgets.md b/guide/src/widgets.md new file mode 100644 index 0000000..f2f0ecf --- /dev/null +++ b/guide/src/widgets.md @@ -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> diff --git a/guide/src/widgets/utility/space.md b/guide/src/widgets/controls/space.md similarity index 100% rename from guide/src/widgets/utility/space.md rename to guide/src/widgets/controls/space.md diff --git a/guide/src/widgets/layout.md b/guide/src/widgets/layout.md deleted file mode 100644 index 3c175a6..0000000 --- a/guide/src/widgets/layout.md +++ /dev/null @@ -1,4 +0,0 @@ -# Layout - -Widgets in this section have a primary purpose of performing some layout -functionality on one or more child widgets. diff --git a/guide/src/widgets/layout/align.md b/guide/src/widgets/layout/align.md index 0c85d00..7117aa2 100644 --- a/guide/src/widgets/layout/align.md +++ b/guide/src/widgets/layout/align.md @@ -91,4 +91,4 @@ Any widget can be aligned to the bottom using [flexauto]: <{{ docs }}/styles/enum.FlexibleDimension.html#variant.Auto> [fit-vert]: [fit-horiz]: -[space]: ../utility/space.md +[space]: ../controls/space.md diff --git a/guide/src/widgets/layout/layers.md b/guide/src/widgets/layout/layers.md index 27c616a..7a33fb4 100644 --- a/guide/src/widgets/layout/layers.md +++ b/guide/src/widgets/layout/layers.md @@ -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> diff --git a/guide/src/widgets/layout/stack.md b/guide/src/widgets/layout/stack.md index c1a905f..7b026ca 100644 --- a/guide/src/widgets/layout/stack.md +++ b/guide/src/widgets/layout/stack.md @@ -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> diff --git a/guide/src/widgets/layout/wrap.md b/guide/src/widgets/layout/wrap.md index 9e227b5..cde2cbe 100644 --- a/guide/src/widgets/layout/wrap.md +++ b/guide/src/widgets/layout/wrap.md @@ -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> diff --git a/guide/src/widgets/multi-layout.md b/guide/src/widgets/multi-layout.md new file mode 100644 index 0000000..342de16 --- /dev/null +++ b/guide/src/widgets/multi-layout.md @@ -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. diff --git a/guide/src/widgets/single-layout.md b/guide/src/widgets/single-layout.md new file mode 100644 index 0000000..5294e5b --- /dev/null +++ b/guide/src/widgets/single-layout.md @@ -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. diff --git a/src/app.rs b/src/app.rs index ab5c063..2b59727 100644 --- a/src/app.rs +++ b/src/app.rs @@ -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 } diff --git a/src/debug.rs b/src/debug.rs index a8896d8..5cc6c94 100644 --- a/src/debug.rs +++ b/src/debug.rs @@ -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::() + .collect::() }); let parent = parent.map(Dynamic::downgrade); diff --git a/src/value.rs b/src/value.rs index edd22dd..c8118b6 100644 --- a/src/value.rs +++ b/src/value.rs @@ -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 for Children { +impl GetWidget for WidgetList { fn get<'a>(&'a self, key: &usize) -> Option<&'a WidgetInstance> { (**self).get(*key) } diff --git a/src/widget.rs b/src/widget.rs index 42e152c..089cfd2 100644 --- a/src/widget.rs +++ b/src/widget.rs @@ -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, } -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 { +impl Dynamic { /// Returns `self` as a vertical [`Stack`] of rows. #[must_use] pub fn into_rows(self) -> Stack { @@ -2126,7 +2137,7 @@ impl Dynamic { } } -impl FromIterator for Children +impl FromIterator 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`. +/// `Value`. #[derive(Debug)] pub struct MountedChildren { generation: Option, @@ -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, context: &mut EventContext<'_>) { + pub fn synchronize_with( + &mut self, + children: &Value, + 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; diff --git a/src/widgets/layers.rs b/src/widgets/layers.rs index 800b772..a6051a4 100644 --- a/src/widgets/layers.rs +++ b/src/widgets/layers.rs @@ -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, + pub children: Value, mounted: MountedChildren, } impl Layers { /// Returns a new instance that lays out `children` as layers. - pub fn new(children: impl IntoValue) -> Self { + pub fn new(children: impl IntoValue) -> Self { Self { children: children.into_value(), mounted: MountedChildren::default(), diff --git a/src/widgets/stack.rs b/src/widgets/stack.rs index 3da1907..321bad3 100644 --- a/src/widgets/stack.rs +++ b/src/widgets/stack.rs @@ -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, + pub children: Value, /// The amount of space to place between each widget. pub gutter: Value, 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) -> Self { + pub fn new(orientation: Orientation, widgets: impl IntoValue) -> 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) -> Self { + pub fn columns(widgets: impl IntoValue) -> Self { Self::new(Orientation::Column, widgets) } /// Returns a new instance that displays `widgets` in a series of rows. - pub fn rows(widgets: impl IntoValue) -> Self { + pub fn rows(widgets: impl IntoValue) -> 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(); diff --git a/src/widgets/wrap.rs b/src/widgets/wrap.rs index dce6103..639cbc1 100644 --- a/src/widgets/wrap.rs +++ b/src/widgets/wrap.rs @@ -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, + pub children: Value, /// The horizontal alignment for widgets on the same row. pub align: Value, /// 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) -> Self { + pub fn new(children: impl IntoValue) -> Self { Self { children: children.into_value(), align: Value::default(), diff --git a/src/window.rs b/src/window.rs index a8c580c..527ee46 100644 --- a/src/window.rs +++ b/src/window.rs @@ -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,