mirror of https://github.com/linebender/xilem
Create skeleton of documentation (#632)
This is a very messy, very basic skeleton of what Masonry documentation will eventually look like. Main points are: - Dedicated documentation modules. - Re-using most of the language from the RFCs. Next steps are: - Flesh out the Widget documentation. - Rewrite all those docs in a less placeholder-y way. - Add chapter about the widget arena. - Spread out that pass documentation to the respective pass files. - Rewrite ARCHITECTURE.md. - Add screenshots. Fixes #376 and #389. --------- Co-authored-by: Daniel McNab <36049421+DJMcNab@users.noreply.github.com>
This commit is contained in:
parent
30dba40300
commit
e03dfdd82b
|
@ -39,8 +39,6 @@ use masonry::widget::{Button, Flex, Label, Portal, RootWidget, Textbox, WidgetMu
|
||||||
use masonry::{Action, AppDriver, DriverCtx, WidgetId};
|
use masonry::{Action, AppDriver, DriverCtx, WidgetId};
|
||||||
use winit::window::Window;
|
use winit::window::Window;
|
||||||
|
|
||||||
const VERTICAL_WIDGET_SPACING: f64 = 20.0;
|
|
||||||
|
|
||||||
struct Driver {
|
struct Driver {
|
||||||
next_task: String,
|
next_task: String,
|
||||||
}
|
}
|
||||||
|
@ -63,6 +61,8 @@ impl AppDriver for Driver {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
const VERTICAL_WIDGET_SPACING: f64 = 20.0;
|
||||||
|
|
||||||
let main_widget = Portal::new(
|
let main_widget = Portal::new(
|
||||||
Flex::column()
|
Flex::column()
|
||||||
.with_child(
|
.with_child(
|
||||||
|
@ -91,7 +91,9 @@ fn main() {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Create feature flags
|
For more information, see [the documentation module](https://docs.rs/masonry/latest/masonry/doc/).
|
||||||
|
|
||||||
|
### Crate feature flags
|
||||||
|
|
||||||
The following feature flags are available:
|
The following feature flags are available:
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,263 @@
|
||||||
|
# Building a "To-Do List" app
|
||||||
|
|
||||||
|
<!-- Copyright 2024 the Xilem Authors -->
|
||||||
|
<!-- SPDX-License-Identifier: Apache-2.0 -->
|
||||||
|
|
||||||
|
<div class="rustdoc-hidden">
|
||||||
|
|
||||||
|
> [!TIP]
|
||||||
|
>
|
||||||
|
> This file is intended to be read in rustdoc.
|
||||||
|
> Use `cargo doc --open --package masonry --no-deps`.
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
**TODO - Add screenshots - see [#501](https://github.com/linebender/xilem/issues/501)**
|
||||||
|
|
||||||
|
This tutorial explains how to build a simple Masonry app, step by step.
|
||||||
|
Though it isn't representative of how we expect Masonry to be used, it does cover the basic architecture.
|
||||||
|
|
||||||
|
The app we'll create is identical to the to-do-list example shown in the README.
|
||||||
|
|
||||||
|
## The Widget tree
|
||||||
|
|
||||||
|
Let's start with the `main()` function.
|
||||||
|
|
||||||
|
```rust,ignore
|
||||||
|
fn main() {
|
||||||
|
const VERTICAL_WIDGET_SPACING: f64 = 20.0;
|
||||||
|
|
||||||
|
use masonry::widget::{Button, Flex, Portal, RootWidget, Textbox};
|
||||||
|
|
||||||
|
let main_widget = Portal::new(
|
||||||
|
Flex::column()
|
||||||
|
.with_child(
|
||||||
|
Flex::row()
|
||||||
|
.with_flex_child(Textbox::new(""), 1.0)
|
||||||
|
.with_child(Button::new("Add task")),
|
||||||
|
)
|
||||||
|
.with_spacer(VERTICAL_WIDGET_SPACING),
|
||||||
|
);
|
||||||
|
let main_widget = RootWidget::new(main_widget);
|
||||||
|
|
||||||
|
// ...
|
||||||
|
|
||||||
|
masonry::event_loop_runner::run(
|
||||||
|
// ...
|
||||||
|
main_widget,
|
||||||
|
// ...
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
First we create our initial widget hierarchy.
|
||||||
|
We're trying to build a simple to-do list app, so our root widget is a scrollable area ([`Portal`]) with a vertical list ([`Flex`]), whose first row is a horizontal list (`Flex` again) containing a text field ([`Textbox`]) and an "Add task" button ([`Button`]).
|
||||||
|
|
||||||
|
We wrap it in a [`RootWidget`], whose main purpose is to include a `Window` node in the accessibility tree.
|
||||||
|
|
||||||
|
At the end of the main function, we pass the root widget to the `event_loop_runner::run` function.
|
||||||
|
That function starts the main event loop, which runs until the user closes the window.
|
||||||
|
During the course of the event loop, the widget tree will be displayed, and updated as the user interacts with the app.
|
||||||
|
|
||||||
|
|
||||||
|
## The `Driver`
|
||||||
|
|
||||||
|
To handle user interactions, we need to implement the [`AppDriver`] trait:
|
||||||
|
|
||||||
|
```rust,ignore
|
||||||
|
trait AppDriver {
|
||||||
|
fn on_action(&mut self, ctx: &mut DriverCtx<'_>, widget_id: WidgetId, action: Action);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Every time the user interacts with the app in a meaningful way (clicking a button, entering text, etc), an [`Action`] is emitted, and the `on_action` method is called.
|
||||||
|
|
||||||
|
That method gives our app a [`DriverCtx`] context, which we can use to access the root widget, and a [`WidgetId`] identifying the widget that emitted the action.
|
||||||
|
|
||||||
|
We create a `Driver` struct to store a very simple app's state, and we implement the `AppDriver` trait for it:
|
||||||
|
|
||||||
|
```rust,ignore
|
||||||
|
use masonry::app_driver::{AppDriver, DriverCtx};
|
||||||
|
use masonry::{Action, WidgetId};
|
||||||
|
use masonry::widget::{Label};
|
||||||
|
|
||||||
|
struct Driver {
|
||||||
|
next_task: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppDriver for Driver {
|
||||||
|
fn on_action(&mut self, ctx: &mut DriverCtx<'_>, _widget_id: WidgetId, action: Action) {
|
||||||
|
match action {
|
||||||
|
Action::ButtonPressed(_) => {
|
||||||
|
let mut root: WidgetMut<RootWidget<Portal<Flex>>> = ctx.get_root();
|
||||||
|
let mut portal = root.child_mut();
|
||||||
|
let mut flex = portal.child_mut();
|
||||||
|
flex.add_child(Label::new(self.next_task.clone()));
|
||||||
|
}
|
||||||
|
Action::TextChanged(new_text) => {
|
||||||
|
self.next_task = new_text.clone();
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
In `on_action`, we handle the two possible actions:
|
||||||
|
|
||||||
|
- `TextChanged`: Update the text of the next task.
|
||||||
|
- `ButtonPressed`: Add a task to the list.
|
||||||
|
|
||||||
|
Because our widget tree only has one button and one textbox, there is no possible ambiguity as to which widget emitted the event, so we can ignore the `WidgetId` argument.
|
||||||
|
|
||||||
|
When handling `ButtonPressed`:
|
||||||
|
|
||||||
|
- `ctx.get_root()` returns a `WidgetMut<RootWidget<...>>`.
|
||||||
|
- `root.child_mut()` returns a `WidgetMut<Portal<...>>` for the `Portal`.
|
||||||
|
- `portal.child_mut()` returns a `WidgetMut<Flex>` for the `Flex`.
|
||||||
|
|
||||||
|
A [`WidgetMut`] is a smart reference type which lets us modify the widget tree.
|
||||||
|
It's set up to automatically propagate update flags and update internal state when dropped.
|
||||||
|
|
||||||
|
We use [`WidgetMut::<Flex>::add_child()`][add_child] to add a new `Label` with the text of our new task to our list.
|
||||||
|
|
||||||
|
In our main function, we create a `Driver` and pass it to `event_loop_runner::run`:
|
||||||
|
|
||||||
|
```rust,ignore
|
||||||
|
// ...
|
||||||
|
|
||||||
|
let driver = Driver {
|
||||||
|
next_task: String::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// ...
|
||||||
|
|
||||||
|
masonry::event_loop_runner::run(
|
||||||
|
// ...
|
||||||
|
main_widget,
|
||||||
|
driver,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Bringing it all together
|
||||||
|
|
||||||
|
The last step is to create our Winit window and start our main loop.
|
||||||
|
|
||||||
|
```rust,ignore
|
||||||
|
use masonry::dpi::LogicalSize;
|
||||||
|
use winit::window::Window;
|
||||||
|
|
||||||
|
let window_attributes = Window::default_attributes()
|
||||||
|
.with_title("To-do list")
|
||||||
|
.with_resizable(true)
|
||||||
|
.with_min_inner_size(LogicalSize::new(400.0, 400.0));
|
||||||
|
|
||||||
|
masonry::event_loop_runner::run(
|
||||||
|
masonry::event_loop_runner::EventLoop::with_user_event(),
|
||||||
|
window_attributes,
|
||||||
|
main_widget,
|
||||||
|
driver,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
```
|
||||||
|
|
||||||
|
Our complete program therefore looks like this:
|
||||||
|
|
||||||
|
```rust,ignore
|
||||||
|
fn main() {
|
||||||
|
const VERTICAL_WIDGET_SPACING: f64 = 20.0;
|
||||||
|
|
||||||
|
use masonry::widget::{Button, Flex, Portal, RootWidget, Textbox};
|
||||||
|
|
||||||
|
let main_widget = Portal::new(
|
||||||
|
Flex::column()
|
||||||
|
.with_child(
|
||||||
|
Flex::row()
|
||||||
|
.with_flex_child(Textbox::new(""), 1.0)
|
||||||
|
.with_child(Button::new("Add task")),
|
||||||
|
)
|
||||||
|
.with_spacer(VERTICAL_WIDGET_SPACING),
|
||||||
|
);
|
||||||
|
let main_widget = RootWidget::new(main_widget);
|
||||||
|
|
||||||
|
use masonry::app_driver::{AppDriver, DriverCtx};
|
||||||
|
use masonry::{Action, WidgetId};
|
||||||
|
use masonry::widget::{Label};
|
||||||
|
|
||||||
|
struct Driver {
|
||||||
|
next_task: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppDriver for Driver {
|
||||||
|
fn on_action(&mut self, ctx: &mut DriverCtx<'_>, _widget_id: WidgetId, action: Action) {
|
||||||
|
match action {
|
||||||
|
Action::ButtonPressed(_) => {
|
||||||
|
let mut root: WidgetMut<RootWidget<Portal<Flex>>> = ctx.get_root();
|
||||||
|
let mut portal = root.child_mut();
|
||||||
|
let mut flex = portal.child_mut();
|
||||||
|
flex.add_child(Label::new(self.next_task.clone()));
|
||||||
|
}
|
||||||
|
Action::TextChanged(new_text) => {
|
||||||
|
self.next_task = new_text.clone();
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let driver = Driver {
|
||||||
|
next_task: String::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
use masonry::dpi::LogicalSize;
|
||||||
|
use winit::window::Window;
|
||||||
|
|
||||||
|
let window_attributes = Window::default_attributes()
|
||||||
|
.with_title("To-do list")
|
||||||
|
.with_resizable(true)
|
||||||
|
.with_min_inner_size(LogicalSize::new(400.0, 400.0));
|
||||||
|
|
||||||
|
masonry::event_loop_runner::run(
|
||||||
|
masonry::event_loop_runner::EventLoop::with_user_event(),
|
||||||
|
window_attributes,
|
||||||
|
main_widget,
|
||||||
|
driver,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
All the Masonry examples follow this structure:
|
||||||
|
|
||||||
|
- An initial widget tree.
|
||||||
|
- A struct implementing `AppDriver` to handle user interactions.
|
||||||
|
- A Winit window and event loop.
|
||||||
|
|
||||||
|
Some examples also define custom Widgets, but you can build an interactive app with Masonry's base widget set, though it's not Masonry's intended use.
|
||||||
|
|
||||||
|
|
||||||
|
## Higher layers
|
||||||
|
|
||||||
|
The above example isn't representative of how we expect Masonry to be used.
|
||||||
|
|
||||||
|
In practice, we expect most implementations of `AppDriver` to be GUI frameworks built on top of Masonry and using it to back their own abstractions.
|
||||||
|
|
||||||
|
Currently, the only public framework built with Masonry is Xilem, though we hope others will develop as Masonry matures.
|
||||||
|
|
||||||
|
Most of this documentation is written to help developers trying to build such a framework.
|
||||||
|
|
||||||
|
[`Portal`]: crate::widget::Portal
|
||||||
|
[`Flex`]: crate::widget::Flex
|
||||||
|
[`Textbox`]: crate::widget::Textbox
|
||||||
|
[`Button`]: crate::widget::Button
|
||||||
|
[`RootWidget`]: crate::widget::RootWidget
|
||||||
|
|
||||||
|
[`AppDriver`]: crate::AppDriver
|
||||||
|
[`Action`]: crate::Action
|
||||||
|
[`DriverCtx`]: crate::DriverCtx
|
||||||
|
[`WidgetId`]: crate::WidgetId
|
||||||
|
[`WidgetMut`]: crate::widget::WidgetMut
|
||||||
|
[add_child]: crate::widget::WidgetMut::add_child
|
|
@ -0,0 +1,336 @@
|
||||||
|
# Creating a new Widget
|
||||||
|
|
||||||
|
<!-- Copyright 2024 the Xilem Authors -->
|
||||||
|
<!-- SPDX-License-Identifier: Apache-2.0 -->
|
||||||
|
|
||||||
|
<div class="rustdoc-hidden">
|
||||||
|
|
||||||
|
> [!TIP]
|
||||||
|
>
|
||||||
|
> This file is intended to be read in rustdoc.
|
||||||
|
> Use `cargo doc --open --package masonry --no-deps`.
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
**TODO - Add screenshots - see [#501](https://github.com/linebender/xilem/issues/501)**
|
||||||
|
|
||||||
|
If you're building your own GUI framework on top of Masonry, or even a GUI app with specific needs, you'll want to specify your own widgets.
|
||||||
|
|
||||||
|
This tutorial explains how to create a simple leaf widget.
|
||||||
|
|
||||||
|
|
||||||
|
## The Widget trait
|
||||||
|
|
||||||
|
Widgets are types which implement the [`Widget`] trait.
|
||||||
|
|
||||||
|
This trait includes a set of methods that must be implemented to hook into Masonry's internals:
|
||||||
|
|
||||||
|
```rust,ignore
|
||||||
|
trait Widget {
|
||||||
|
fn on_pointer_event(&mut self, ctx: &mut EventCtx, event: &PointerEvent);
|
||||||
|
fn on_text_event(&mut self, ctx: &mut EventCtx, event: &TextEvent);
|
||||||
|
fn on_access_event(&mut self, ctx: &mut EventCtx, event: &AccessEvent);
|
||||||
|
|
||||||
|
fn on_anim_frame(&mut self, ctx: &mut UpdateCtx, interval: u64);
|
||||||
|
fn update(&mut self, ctx: &mut UpdateCtx, event: &Update);
|
||||||
|
|
||||||
|
fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size;
|
||||||
|
|
||||||
|
fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut Scene);
|
||||||
|
fn accessibility_role(&self) -> Role;
|
||||||
|
fn accessibility(&mut self, ctx: &mut AccessCtx, node: &mut NodeBuilder);
|
||||||
|
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
These methods are called by the framework at various points, with a `FoobarCtx` parameter giving information about the current widget (for example its size, position, or whether it's currently hovered).
|
||||||
|
The information accessible from the context argument depends on the method.
|
||||||
|
|
||||||
|
In the course of a frame, Masonry will run a series of passes over the widget tree, which will call these methods at different points:
|
||||||
|
|
||||||
|
- `on_pointer_event`, `on_text_event` and `on_access_event` are called once after a user-initiated event (like a mouse click or keyboard input).
|
||||||
|
- `on_anim_frame` is called once per frame for animated widgets.
|
||||||
|
- `update` is called many times during a frame, with various events reflecting changes in the widget's state (for instance, it gets or loses text focus).
|
||||||
|
- `layout` is called during Masonry's layout pass. It takes size constraints and returns the widget's desired size.
|
||||||
|
- `paint`, `accessibility_role` and `accessibility` are called roughly every frame for every widget, to allow them to draw to the screen and describe their structure to assistive technologies.
|
||||||
|
|
||||||
|
Most passes will skip most widgets by default.
|
||||||
|
For instance, the paint pass will only call a widget's `paint` method once, and then cache the resulting scene.
|
||||||
|
If your widget's appearance is changed by another method, you need to call `ctx.request_render()` to tell the framework to re-run the paint and accessibility passes.
|
||||||
|
|
||||||
|
Most context types include these methods for requesting future passes:
|
||||||
|
|
||||||
|
- `request_render()`
|
||||||
|
- `request_paint_only()`
|
||||||
|
- `request_accessibility_update()`
|
||||||
|
- `request_layout()`
|
||||||
|
- `request_anim_frame()`
|
||||||
|
|
||||||
|
|
||||||
|
## Widget mutation
|
||||||
|
|
||||||
|
In Masonry, widgets generally can't be mutated directly.
|
||||||
|
That is to say, even if you own a window, and even if that window holds a widget tree with a `Label` instance, you can't get a `&mut Label` directly from that window.
|
||||||
|
|
||||||
|
Instead, there are two ways to mutate `Label`:
|
||||||
|
|
||||||
|
- Inside a Widget method. Most methods (`on_pointer_event`, `update`, `layout`, etc) take a `&mut self` argument.
|
||||||
|
- Through a [`WidgetMut`] wrapper. So, to change your label's text, you will call `WidgetMut::<Label>::set_text()`. This helps Masonry make sure that internal metadata is propagated after every widget change.
|
||||||
|
|
||||||
|
As mentioned in the previous chapter, a `WidgetMut` is a smart reference type to the Widget tree.
|
||||||
|
Most Widgets will implement methods that let their users "project" a WidgetMut from a parent to its child.
|
||||||
|
For example, `WidgetMut<Portal<MyWidget>>` has a `get_child_mut()` method that returns a `WidgetMut<MyWidget>`.
|
||||||
|
|
||||||
|
So far, we've seen one way to get a WidgetMut: the [`DriverCtx::get_root()`] method in `AppDriver` implementations.
|
||||||
|
This methods returns a WidgetMut to the root widget, which you can then project into a WidgetMut reference to its descendants.
|
||||||
|
|
||||||
|
<!-- TODO - Change AppDriver trait to take a `&mut RenderRoot` instead, and rewrite above doc. -->
|
||||||
|
|
||||||
|
<!-- TODO - Mention edit_root_widget, edit_widget. -->
|
||||||
|
|
||||||
|
### Using WidgetMut in your custom Widget code
|
||||||
|
|
||||||
|
The WidgetMut type only has two fields, both public:
|
||||||
|
|
||||||
|
```rust,ignore
|
||||||
|
pub struct WidgetMut<'a, W: Widget> {
|
||||||
|
pub ctx: MutateCtx<'a>,
|
||||||
|
pub widget: &'a mut W,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`W` is your widget type. `MutateCtx` is yet another context type, with methods that let you get information about your widget and report that it changed in some ways.
|
||||||
|
|
||||||
|
If you want your widget to be mutable outside of its pass methods, you should write setter functions taking WidgetMut as a parameter.
|
||||||
|
|
||||||
|
These functions should modify the internal values of your widget, then set flags using `MutateCtx` depending on which values changed.
|
||||||
|
For instance, a `set_padding()` function should probably call `ctx.request_layout()`, whereas a `set_background_color()` function should probably call `ctx.request_render()` or `ctx.request_paint_only()`.
|
||||||
|
|
||||||
|
|
||||||
|
## Example widget: ColorRectangle
|
||||||
|
|
||||||
|
<!-- TODO - Interleave this with above documentation. -->
|
||||||
|
|
||||||
|
Let's implement a very simple widget: `ColorRectangle`.
|
||||||
|
This Widget has a size, a color, and emits a `ButtonPressed` action when the user left-clicks on it (on mouse press; we ignore mouse release).
|
||||||
|
|
||||||
|
First, let's create our struct:
|
||||||
|
|
||||||
|
```rust,ignore
|
||||||
|
use vello::kurbo::Size;
|
||||||
|
use vello::peniko::Color;
|
||||||
|
|
||||||
|
struct ColorRectangle {
|
||||||
|
size: Size,
|
||||||
|
color: Color,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ColorRectangle {
|
||||||
|
fn new(size: Size, color: Color) -> Self {
|
||||||
|
Self { size, color }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This widget doesn't have children and doesn't really need to keep track of any transient state, so its definition is pretty simple.
|
||||||
|
Note that we store a size, and not a position: our widget's position is picked by its parent.
|
||||||
|
|
||||||
|
### Implementing the Widget trait
|
||||||
|
|
||||||
|
First we implement event methods:
|
||||||
|
|
||||||
|
```rust,ignore
|
||||||
|
use masonry::{
|
||||||
|
Widget, EventCtx, PointerEvent, TextEvent, AccessEvent, Action
|
||||||
|
};
|
||||||
|
|
||||||
|
impl Widget for ColorRectangle {
|
||||||
|
fn on_pointer_event(&mut self, ctx: &mut EventCtx, event: &PointerEvent) {
|
||||||
|
match event {
|
||||||
|
PointerEvent::PointerDown(PointerButton::Primary, _) => {
|
||||||
|
ctx.submit_action(Action::ButtonPressed(PointerButton::Primary));
|
||||||
|
}
|
||||||
|
_ => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_text_event(&mut self, _ctx: &mut EventCtx, _event: &TextEvent) {}
|
||||||
|
|
||||||
|
fn on_access_event(&mut self, ctx: &mut EventCtx, event: &AccessEvent) {
|
||||||
|
match event.action {
|
||||||
|
accesskit::Action::Default => {
|
||||||
|
ctx.submit_action(Action::ButtonPressed(PointerButton::Primary));
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Here we've written a simple handler which filters pointer events for left clicks, and submits a [`ButtonPressed`] action.
|
||||||
|
|
||||||
|
We've also implemented the `on_access_event` method, which emulates the click behaviors for people using assistive technologies.
|
||||||
|
|
||||||
|
Next we can leave the `on_anim_frame` and `update` implementations empty:
|
||||||
|
|
||||||
|
```rust,ignore
|
||||||
|
use masonry::{
|
||||||
|
UpdateCtx
|
||||||
|
};
|
||||||
|
|
||||||
|
impl Widget for ColorRectangle {
|
||||||
|
// ...
|
||||||
|
|
||||||
|
fn on_anim_frame(&mut self, _ctx: &mut UpdateCtx, _interval: u64) {}
|
||||||
|
fn update(&mut self, _ctx: &mut UpdateCtx, _event: &Update) {}
|
||||||
|
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Next we implement layout:
|
||||||
|
|
||||||
|
```rust,ignore
|
||||||
|
use masonry::{
|
||||||
|
LayoutCtx, BoxConstraints
|
||||||
|
};
|
||||||
|
|
||||||
|
impl Widget for ColorRectangle {
|
||||||
|
// ...
|
||||||
|
|
||||||
|
fn layout(&mut self, _ctx: &mut LayoutCtx, _bc: &BoxConstraints) -> Size {
|
||||||
|
self.size
|
||||||
|
}
|
||||||
|
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Our size is static, and doesn't depend on size constraints passed by our parent or context information like "the widget is currently hovered", so it can be written as a one-liner.
|
||||||
|
|
||||||
|
Next we write our render methods:
|
||||||
|
|
||||||
|
```rust,ignore
|
||||||
|
use masonry::{
|
||||||
|
PaintCtx, AccessCtx
|
||||||
|
};
|
||||||
|
use vello::Scene;
|
||||||
|
use accesskit::{NodeBuilder, Role};
|
||||||
|
|
||||||
|
impl Widget for ColorRectangle {
|
||||||
|
// ...
|
||||||
|
|
||||||
|
fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut Scene) {
|
||||||
|
let rect = ctx.size().to_rect();
|
||||||
|
scene.fill(
|
||||||
|
Fill::NonZero,
|
||||||
|
Affine::IDENTITY,
|
||||||
|
self.color,
|
||||||
|
Some(Affine::IDENTITY),
|
||||||
|
&rect,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn accessibility_role(&self) -> Role {
|
||||||
|
Role::Button
|
||||||
|
}
|
||||||
|
|
||||||
|
fn accessibility(&mut self, ctx: &mut AccessCtx, node: &mut NodeBuilder) {
|
||||||
|
node.set_default_action_verb(DefaultActionVerb::Click);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
In our `paint` method, we're given a [`vello::Scene`] and paint a rectangle into it.
|
||||||
|
|
||||||
|
The rectangle's position is zero (because coordinates in our scenes are local to our widget), and its size is `ctx.size()`, which is the value returned by `layout()`; though we could also have used `self.size`.
|
||||||
|
|
||||||
|
Next we define our accessibility role.
|
||||||
|
Returning [`Role::Button`] means that screen readers will report our widget as a button, which roughly makes sense since it is clickable.
|
||||||
|
|
||||||
|
<!-- TODO - Add more detail about how you should choose your role. -->
|
||||||
|
|
||||||
|
In `accessibility`, we define a default action of `Click`, which is how we register our widget to be eligible for the `accesskit::Action::Default` event reported above.
|
||||||
|
|
||||||
|
<!-- TODO - Is that actually true? I'm not sure what set_default_action does. -->
|
||||||
|
|
||||||
|
We also write a `make_trace_span()` method, which is useful for debugging with the [tracing](https://docs.rs/tracing/latest/tracing/) framework.
|
||||||
|
|
||||||
|
```rust,ignore
|
||||||
|
use tracing::{trace_span, Span};
|
||||||
|
|
||||||
|
impl Widget for ColorRectangle {
|
||||||
|
// ...
|
||||||
|
|
||||||
|
fn make_trace_span(&self) -> Span {
|
||||||
|
trace_span!("ColorRectangle")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
And last, we stub in some additional methods:
|
||||||
|
|
||||||
|
```rust,ignore
|
||||||
|
use masonry::{
|
||||||
|
RegisterCtx, WidgetId
|
||||||
|
};
|
||||||
|
use smallvec::SmallVec;
|
||||||
|
|
||||||
|
impl Widget for ColorRectangle {
|
||||||
|
// ...
|
||||||
|
|
||||||
|
fn register_children(&mut self, _ctx: &mut RegisterCtx) {}
|
||||||
|
fn children_ids(&self) -> SmallVec<[WidgetId; 16]> {
|
||||||
|
SmallVec::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Don't worry about what they mean for now.
|
||||||
|
|
||||||
|
Finally, we want to define some setters for external users:
|
||||||
|
|
||||||
|
<!-- TODO - Rewrite once we've decided how WidgetMut should be implemented. -->
|
||||||
|
|
||||||
|
```rust,ignore
|
||||||
|
struct ColorRectangle {
|
||||||
|
size: Size,
|
||||||
|
color: Color,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WidgetMut<'_, ColorRectangle> {
|
||||||
|
pub fn set_color(&mut self, color: Color) {
|
||||||
|
self.widget.color = color;
|
||||||
|
self.ctx.request_paint_only();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_size(&mut self, size: Size) {
|
||||||
|
self.widget.size = size;
|
||||||
|
self.ctx.request_layout();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
By making ColorRectangle's fields private, and making it so the only way to mutate them is through a WidgetMut, we make it "watertight".
|
||||||
|
Our users can never find themselves in a situation where they forget to propagate invalidation flags, and end up with confusing bugs.
|
||||||
|
|
||||||
|
|
||||||
|
## Next up
|
||||||
|
|
||||||
|
This document was about how to create a simple leaf widget.
|
||||||
|
|
||||||
|
The next one is about creating a container widgets, and the complications it adds.
|
||||||
|
|
||||||
|
[`Widget`]: crate::Widget
|
||||||
|
[`WidgetMut`]: crate::widget::WidgetMut
|
||||||
|
[`DriverCtx::get_root()`]: crate::DriverCtx::get_root
|
||||||
|
[`ButtonPressed`]: crate::Action::ButtonPressed
|
||||||
|
[`vello::Scene`]: crate::vello::Scene
|
||||||
|
[`Role::Button`]: accesskit::Role::Button
|
|
@ -0,0 +1,270 @@
|
||||||
|
# Creating a container Widget
|
||||||
|
|
||||||
|
<!-- Copyright 2024 the Xilem Authors -->
|
||||||
|
<!-- SPDX-License-Identifier: Apache-2.0 -->
|
||||||
|
|
||||||
|
<div class="rustdoc-hidden">
|
||||||
|
|
||||||
|
> [!TIP]
|
||||||
|
>
|
||||||
|
> This file is intended to be read in rustdoc.
|
||||||
|
> Use `cargo doc --open --package masonry --no-deps`.
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
**TODO - Add screenshots - see [#501](https://github.com/linebender/xilem/issues/501)**
|
||||||
|
|
||||||
|
In the previous section we implemented a simple widget.
|
||||||
|
Our widget was overall pretty simple, the tutorial skipped over a few methods with a "we'll explain later" handwave.
|
||||||
|
|
||||||
|
However, in some cases you want to write a widget which "contains" other widgets.
|
||||||
|
You want these child widgets to receive events and be painted as well, as part of the widget hierarchy.
|
||||||
|
|
||||||
|
To do so, you need to implement a container widget.
|
||||||
|
A container widget is still a type which implements the [`Widget`] trait.
|
||||||
|
It stores handles for its children using a type called [`WidgetPod`], and its `Widget` trait implementation is more complex.
|
||||||
|
|
||||||
|
As an example, let's write a `VerticalStack` widget, which lays out its children in a vertical line:
|
||||||
|
|
||||||
|
```rust,ignore
|
||||||
|
struct VerticalStack {
|
||||||
|
children: Vec<WidgetPod<Box<dyn Widget>>>,
|
||||||
|
gap: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VerticalStack {
|
||||||
|
pub fn new(gap: f64) -> Self {
|
||||||
|
Self {
|
||||||
|
children: Vec::new(),
|
||||||
|
gap,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Container-specific methods
|
||||||
|
|
||||||
|
A container widget needs to pay special attention to these methods:
|
||||||
|
|
||||||
|
```rust,ignore
|
||||||
|
trait Widget {
|
||||||
|
// ...
|
||||||
|
|
||||||
|
fn layout(&mut self, ctx: &mut LayoutCtx) -> Size;
|
||||||
|
fn compose(&mut self, ctx: &mut ComposeCtx);
|
||||||
|
|
||||||
|
fn register_children(&mut self, ctx: &mut RegisterCtx);
|
||||||
|
fn children_ids(&self) -> SmallVec<[WidgetId; 16]>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Let's go over them one by one.
|
||||||
|
|
||||||
|
### `layout`
|
||||||
|
|
||||||
|
Like with a leaf widget, the `layout` method must compute and return the size of the container widget.
|
||||||
|
|
||||||
|
Before that, it must call [`LayoutCtx::run_layout`] then [`LayoutCtx::place_child`] for each of its own children:
|
||||||
|
|
||||||
|
- `LayoutCtx::run_layout` recursively calls `Widget::layout` on the child. It takes a [`BoxConstraints`] argument, which represents how much space the parent "gives" to the child.
|
||||||
|
- `LayoutCtx::place_child` sets the child's position relative to the container.
|
||||||
|
|
||||||
|
Generally, containers first get the size of all their children, then use that information and the parent constraints to both compute their own size and spread the children within the available space.
|
||||||
|
|
||||||
|
The `layout` method *must* iterate over all its children.
|
||||||
|
Not doing so is a logical bug.
|
||||||
|
When debug assertions are on, Masonry will actively try to detect cases where you forget to compute a child's layout and panic if it finds such a case.
|
||||||
|
|
||||||
|
For our `VerticalStack`, we'll lay out our children in a vertical line, with a gap between each child; we give each child an equal share of the available height:
|
||||||
|
|
||||||
|
```rust,ignore
|
||||||
|
use masonry::{
|
||||||
|
LayoutCtx, BoxConstraints
|
||||||
|
};
|
||||||
|
|
||||||
|
impl Widget for VerticalStack {
|
||||||
|
// ...
|
||||||
|
|
||||||
|
fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
|
||||||
|
let total_width = bc.max().width;
|
||||||
|
let total_height = bc.max().height;
|
||||||
|
let total_child_height = total_height - self.gap * (self.children.len() - 1) as f64;
|
||||||
|
let child_height = total_child_height / self.children.len() as f64;
|
||||||
|
|
||||||
|
let mut y_offset = 0.0;
|
||||||
|
for child in &self.children {
|
||||||
|
let child_bc = BoxConstraints::new(Size::new(0., 0.), Size::new(total_width, child_height));
|
||||||
|
|
||||||
|
let child_size = ctx.run_layout(child, &child_bc);
|
||||||
|
ctx.place_child(child, Point::new(0.0, y_offset));
|
||||||
|
|
||||||
|
y_offset += child_size.height + self.gap;
|
||||||
|
}
|
||||||
|
|
||||||
|
let total_height = y_offset - self.gap;
|
||||||
|
Size::new(total_width, total_height)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
There are a few things to note here:
|
||||||
|
|
||||||
|
- We use `bc.max()` to get the total space available to the container.
|
||||||
|
- Our children get maximum constraints based on their share of the available space, and a minimum of 0.
|
||||||
|
- We compute the size of each child, and use the computed height to get the offset for the next child. That height might smaller **or greater** than the child's available height.
|
||||||
|
- We compute the height of the container by summing the heights of all children and adding the gap between them. That total height might be smaller **or greater** than the container's available height.
|
||||||
|
- We return the total size of the container.
|
||||||
|
|
||||||
|
|
||||||
|
### `compose`
|
||||||
|
|
||||||
|
The `compose` method is called during the compose pass, after layout.
|
||||||
|
|
||||||
|
The compose pass runs top-down and assigns transforms to children. Transform-only layout changes (e.g. scrolling) should request compose instead of requesting layout.
|
||||||
|
|
||||||
|
Compose is meant to be a cheaper way to position widgets than layout. Because the compose pass is more limited than layout, it's easier to recompute in many situations.
|
||||||
|
|
||||||
|
For instance, if a widget in a list changes size, its siblings and parents must be re-laid out to account for the change; whereas changing a given widget's transform only affects its children.
|
||||||
|
|
||||||
|
In the case of our `VerticalStack`, we don't implement any transform-only changes, so we don't need to do anything in compose:
|
||||||
|
|
||||||
|
```rust,ignore
|
||||||
|
use masonry::{
|
||||||
|
LayoutCtx, BoxConstraints
|
||||||
|
};
|
||||||
|
|
||||||
|
impl Widget for VerticalStack {
|
||||||
|
// ...
|
||||||
|
|
||||||
|
fn compose(&mut self, _ctx: &mut ComposeCtx) {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `register_children` and `children_ids`
|
||||||
|
|
||||||
|
The `register_children` method must call [`RegisterCtx::register_child`] for each child:
|
||||||
|
|
||||||
|
```rust,ignore
|
||||||
|
use masonry::{
|
||||||
|
Widget, RegisterCtx
|
||||||
|
};
|
||||||
|
|
||||||
|
impl Widget for VerticalStack {
|
||||||
|
// ...
|
||||||
|
|
||||||
|
fn register_children(&mut self, ctx: &mut RegisterCtx) {
|
||||||
|
for child in &self.children {
|
||||||
|
ctx.register_child(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `register_children` method is called to insert new children in your container into the widget tree.
|
||||||
|
|
||||||
|
You can request a call to that method by calling `ctx.children_changed()` on various context types.
|
||||||
|
|
||||||
|
The method *must* iterate over all its children.
|
||||||
|
Not doing so is a logical bug, and may also trigger debug assertions.
|
||||||
|
|
||||||
|
"All its children", in this context, means the children whose ids are returned by the `children_ids` method:
|
||||||
|
|
||||||
|
```rust,ignore
|
||||||
|
impl Widget for VerticalStack {
|
||||||
|
// ...
|
||||||
|
|
||||||
|
fn children_ids(&self) -> SmallVec<[WidgetId; 16]> {
|
||||||
|
self.children.iter().map(|child| child.id()).collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `children_ids` method must return the IDs of all your container's children.
|
||||||
|
That list is considered the "canonical" list of children by Masonry, and must match the children visited during `register_children` and `layout`.
|
||||||
|
It should be stable across calls; anything that mutates the list `children_ids()` returns must also call `ctx.children_changed()`.
|
||||||
|
|
||||||
|
|
||||||
|
## Editing the widget tree
|
||||||
|
|
||||||
|
We've seen how to deal with the children of a container widget once they're already there.
|
||||||
|
|
||||||
|
But how do we add them in the first place?
|
||||||
|
|
||||||
|
Widgets will usually be added or removed through a [`WidgetMut`] wrapper.
|
||||||
|
Let's write WidgetMut methods for our `VerticalStack`:
|
||||||
|
|
||||||
|
```rust,ignore
|
||||||
|
impl WidgetMut<'_, VerticalStack> {
|
||||||
|
pub fn add_child(&mut self, child: WidgetPod<Box<dyn Widget>>) {
|
||||||
|
self.widget.children.push(child);
|
||||||
|
self.ctx.children_changed();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove_child(&mut self, n: usize) {
|
||||||
|
self.widget.children.remove(n);
|
||||||
|
self.ctx.children_changed();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear_children(&mut self) {
|
||||||
|
self.widget.children.clear();
|
||||||
|
self.ctx.children_changed();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
If you want to add or remove a child during other passes, the simplest solution is to use the `mutate_self_later` context method.
|
||||||
|
That mutate takes a callback, and schedules it to be run with a `WidgetMut` wrapper to the current widget.
|
||||||
|
|
||||||
|
|
||||||
|
## Regular `Widget` methods
|
||||||
|
|
||||||
|
Now that we've implemented our container-specific methods, we should also implement the regular `Widget` methods.
|
||||||
|
|
||||||
|
In the case of our `VerticalStack`, all of them can be left empty:
|
||||||
|
|
||||||
|
```rust,ignore
|
||||||
|
impl Widget for VerticalStack {
|
||||||
|
fn on_pointer_event(&mut self, _ctx: &mut EventCtx, _event: &PointerEvent) {}
|
||||||
|
fn on_text_event(&mut self, _ctx: &mut EventCtx, _event: &TextEvent) {}
|
||||||
|
fn on_access_event(&mut self, _ctx: &mut EventCtx, _event: &AccessEvent) {}
|
||||||
|
|
||||||
|
fn on_anim_frame(&mut self, _ctx: &mut UpdateCtx, _interval: u64) {}
|
||||||
|
fn update(&mut self, _ctx: &mut UpdateCtx, _event: &Update) {}
|
||||||
|
|
||||||
|
// ...
|
||||||
|
|
||||||
|
fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut Scene) {}
|
||||||
|
|
||||||
|
fn accessibility_role(&self) -> Role {
|
||||||
|
Role::GenericContainer
|
||||||
|
}
|
||||||
|
|
||||||
|
fn accessibility(&mut self, _ctx: &mut AccessCtx, _node: &mut NodeBuilder) {}
|
||||||
|
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This might surprise you: shouldn't our container widget recurse these methods to its children?
|
||||||
|
Doesn't `VerticalStack::paint` need to call `paint` on its children, for instance?
|
||||||
|
|
||||||
|
It doesn't.
|
||||||
|
|
||||||
|
In Masonry, most passes are automatically propagated to children, without container widgets having to implement code iterating over their children.
|
||||||
|
|
||||||
|
So for instance, if `VerticalStack::children_ids()` returns a list of three children, the paint pass will automatically call `paint` on all three children after `VerticalStack::paint()`.
|
||||||
|
|
||||||
|
So various methods in container widgets should only implement the logic that is specific to the container itself.
|
||||||
|
For instance, a container widget with a background color should implement `paint` to draw the background.
|
||||||
|
|
||||||
|
[`Widget`]: crate::Widget
|
||||||
|
[`WidgetPod`]: crate::WidgetPod
|
||||||
|
[`WidgetMut`]: crate::widget::WidgetMut
|
||||||
|
[`LayoutCtx::place_child`]: crate::LayoutCtx::place_child
|
||||||
|
[`LayoutCtx::run_layout`]: crate::LayoutCtx::run_layout
|
||||||
|
[`BoxConstraints`]: crate::BoxConstraints
|
||||||
|
[`RegisterCtx::register_child`]: crate::RegisterCtx::register_child
|
|
@ -0,0 +1,15 @@
|
||||||
|
# Testing widgets in Masonry
|
||||||
|
|
||||||
|
<!-- Copyright 2024 the Xilem Authors -->
|
||||||
|
<!-- SPDX-License-Identifier: Apache-2.0 -->
|
||||||
|
|
||||||
|
<div class="rustdoc-hidden">
|
||||||
|
|
||||||
|
> [!TIP]
|
||||||
|
>
|
||||||
|
> This file is intended to be read in rustdoc.
|
||||||
|
> Use `cargo doc --open --package masonry --no-deps`.
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
TODO
|
|
@ -0,0 +1,184 @@
|
||||||
|
# Masonry pass system
|
||||||
|
|
||||||
|
<!-- Copyright 2024 the Xilem Authors -->
|
||||||
|
<!-- SPDX-License-Identifier: Apache-2.0 -->
|
||||||
|
|
||||||
|
<div class="rustdoc-hidden">
|
||||||
|
|
||||||
|
> [!TIP]
|
||||||
|
>
|
||||||
|
> This file is intended to be read in rustdoc.
|
||||||
|
> Use `cargo doc --open --package masonry --no-deps`.
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
Masonry has a set of **passes**, which are computations run over a subset of the widget tree during every frame.
|
||||||
|
|
||||||
|
Passes can be split into roughly three categories:
|
||||||
|
|
||||||
|
- **Event passes:** triggered by user interaction.
|
||||||
|
- **Rewrite passes:** run after every event pass, may run multiple times until all invalidation flags are cleared.
|
||||||
|
- **Render passes:** run just before rendering a new frame.
|
||||||
|
|
||||||
|
Note: unless otherwise specified, all passes run over widgets in depth-first preorder.
|
||||||
|
|
||||||
|
|
||||||
|
## Event passes
|
||||||
|
|
||||||
|
When a user interacts with the application in some way, like a mouse click, Masonry runs an **event pass** over the tree.
|
||||||
|
There are three types of event passes:
|
||||||
|
|
||||||
|
- **on_pointer_event:** covers positional events from the mouse and other pointing devices (pen, stylus, touchpad, etc).
|
||||||
|
- **on_text_event:** text input events like keyboard presses, IME, clipboard paste, etc.
|
||||||
|
- **on_access_event:** events from the OS's accessibility API.
|
||||||
|
|
||||||
|
When an event occurs, the application selects the widget targeted by the event.
|
||||||
|
For pointer events, this is either the widget under the pointer or the widget with pointer capture.
|
||||||
|
For text and accessibility events, this is the widget with focus.
|
||||||
|
|
||||||
|
The widget's event handling method (`on_pointer_event`, `on_text_event`, or `on_access_event`) is called.
|
||||||
|
Then, the same method is called for each of the widget's parents, up to the root.
|
||||||
|
This behavior is known in browsers as event bubbling.
|
||||||
|
|
||||||
|
### Animation pass
|
||||||
|
|
||||||
|
The **update_anim** pass runs an animation frame, which occurs at set intervals if the widget tree includes animated widgets.
|
||||||
|
|
||||||
|
It runs in depth-first preorder on all animated widgets in the tree.
|
||||||
|
|
||||||
|
The animation pass may be considered as a special event pass: it's not triggered by user interaction, and it doesn't bubble, but it's also triggered externally and sets off the rewrite passes.
|
||||||
|
|
||||||
|
|
||||||
|
## Rewrite passes
|
||||||
|
|
||||||
|
After an event pass, some flags may have been changed, and some values may have been invalidated and need to be recomputed.
|
||||||
|
To address these invalidations, Masonry runs a set of **rewrite passes** over the tree:
|
||||||
|
|
||||||
|
- **mutate:** Runs callbacks with mutable access to the tree.
|
||||||
|
- **update_widget_tree:** Updates the tree when widgets are added or removed.
|
||||||
|
- **update_disabled:** Updates the disabled status of widgets.
|
||||||
|
- **update_stashed:** Updates the stashed status of widgets.
|
||||||
|
- **update_focus_chain:** Updates the focus chain. (Internal-only, doesn't call widget methods.)
|
||||||
|
- **update_focus:** Updates the focused status of widgets.
|
||||||
|
- **layout:** Computes the layout of the widget tree.
|
||||||
|
- **update_scrolls:** Updates the scroll positions of widgets.
|
||||||
|
- **compose:** Assigns transforms to widgets.
|
||||||
|
- **update_pointer:** Updates the hovered status of widgets and the current cursor icon.
|
||||||
|
|
||||||
|
The layout and compose passes have methods with matching names in the Widget trait.
|
||||||
|
The update_xxx passes call the widgets' update method.
|
||||||
|
|
||||||
|
By default, each of these passes completes immediately, unless pass-dependent invalidation flags are set.
|
||||||
|
Each pass can generally request work for later passes; for instance, the mutate pass can invalidate the layout of a widget, in which case the layout pass will run on that widget and its children and parents.
|
||||||
|
|
||||||
|
Passes may also request work for *previous* passes, in which case all rewrite passes are run again in sequence.
|
||||||
|
For instance, the update_pointer pass may change a widget's size, requiring another layout pass.
|
||||||
|
|
||||||
|
To avoid infinite loops in those cases, the number of reruns has a static limit.
|
||||||
|
If passes are still requested past that limit, they're delayed to a later frame.
|
||||||
|
|
||||||
|
### The mutate pass
|
||||||
|
|
||||||
|
The **mutate** pass runs a list of callbacks with mutable access to the widget tree.
|
||||||
|
These callbacks can be queued with the `mutate_later()` method of various context types.
|
||||||
|
|
||||||
|
"Mutable access" means that those callbacks are given a [`WidgetMut`] to the widget that requested them, something that is otherwise only accessible from the owner of the global [`RenderRoot`] object.
|
||||||
|
|
||||||
|
If a callback is scheduled to run on a widget which is deleted before the callback is run, that callback is silently dropped.
|
||||||
|
|
||||||
|
*Note:* The mutate pass is meant to be *an escape hatch*.
|
||||||
|
It covers widgets which don't quite fit into the pass system and future use-cases that we didn't foresee while developing Masonry.
|
||||||
|
It's more powerful and gives complete access to the tree, but is also slightly more expensive and less idiomatic than doing things in other passes.
|
||||||
|
|
||||||
|
Widgets should try to fit their logic into the other passes, and use `mutate_later()` sparsely.
|
||||||
|
|
||||||
|
### Update passes
|
||||||
|
|
||||||
|
Update passes mostly run internal calculations.
|
||||||
|
They compute if some widget's property has changed, and send it a matching `update` event (see "Status" section below).
|
||||||
|
|
||||||
|
For instance, if a user presses `Tab` and text focus moves to the next widget, Masonry will run the `update_focus` pass, which will automatically changed the "focused" status of the previously-focused widget and the newly-focused widget, and call their `update()` methods with relevant events.
|
||||||
|
|
||||||
|
### Update tree pass
|
||||||
|
|
||||||
|
The `update_widget_tree` pass is a special case.
|
||||||
|
It is called when new widgets are added to the tree, or existing widgets are removed.
|
||||||
|
|
||||||
|
It will call the `register_children()` widget method on container widgets whose children changed, then the `update()` method with the `WidgetAdded` event on new widgets.
|
||||||
|
|
||||||
|
### Layout pass
|
||||||
|
|
||||||
|
The layout pass runs bidirectionally, passing constraints from the top down and getting back sizes and other layout info from the bottom up.
|
||||||
|
|
||||||
|
It is subject to be reworked in the future to be closer to the semantics of web layout engines and the Taffy crate.
|
||||||
|
|
||||||
|
Unlike with other passes, container widgets' `Widget::layout()` method must "manually" recurse by calling [`LayoutCtx::run_layout`] then [`LayoutCtx::place_child`] for each of their children.
|
||||||
|
|
||||||
|
Not doing so is a logical bug, and may trigger debug assertions.
|
||||||
|
|
||||||
|
### Compose pass
|
||||||
|
|
||||||
|
The **compose** pass runs top-down and assigns transforms to children.
|
||||||
|
Transform-only layout changes (e.g. scrolling) should request compose instead of requesting layout.
|
||||||
|
|
||||||
|
Compose is meant to be a cheaper way to position widgets than layout.
|
||||||
|
Because the compose pass is more limited than layout, it's easier to recompute in many situations.
|
||||||
|
|
||||||
|
For instance, if a widget in a list changes size, its siblings and parents must be re-laid out to account for the change; whereas changing a given widget's transform only affects its children.
|
||||||
|
|
||||||
|
Masonry automatically calls the `compose` methods of all widgets in the tree, in depth-first preorder, where child order is determined by their position in the `children_ids()` array.
|
||||||
|
|
||||||
|
|
||||||
|
## Render passes
|
||||||
|
|
||||||
|
Event and rewrite passes can invalidate how the widget tree is presented to the user.
|
||||||
|
|
||||||
|
If that happens, a redraw frame will be requested from the environment (e.g. the Winit event loop).
|
||||||
|
When the environment applies the redraw, it will run the **render passes** as needed:
|
||||||
|
|
||||||
|
- **paint:** The paint pass gets a Vello Scene description from each widget.
|
||||||
|
These scenes are then stitched together in pre-order: first the parent, then its first child, then *its* first child, etc.
|
||||||
|
- **accessibility:** The accessibility pass gets an AccessKit node description from each widget.
|
||||||
|
These nodes together form the accessibility tree.
|
||||||
|
|
||||||
|
Methods for these passes should be written under the assumption that they can be skipped or called multiple times for arbitrary reasons.
|
||||||
|
Therefore, their ability to affect the widget tree is limited.
|
||||||
|
|
||||||
|
Masonry automatically calls these methods for all widgets in the tree in depth-first preorder.
|
||||||
|
|
||||||
|
## External mutation
|
||||||
|
|
||||||
|
Code with mutable access to the `RenderRoot`, like the Xilem app runner, can get mutable access to the root widget and all its children through the `edit_root_widget()` method, which takes a callback and passes it a `WidgetMut` to the root widget.
|
||||||
|
|
||||||
|
This is in effect a MUTATE pass which only processes one callback.
|
||||||
|
|
||||||
|
External mutation is how Xilem applies any changes to the widget tree produced by its reactive step.
|
||||||
|
|
||||||
|
Calling the `edit_root_widget()` method, or any similar direct-mutation method, triggers the entire set of rewrite passes.
|
||||||
|
|
||||||
|
|
||||||
|
## Pass context types
|
||||||
|
|
||||||
|
Some notes about pass context types:
|
||||||
|
|
||||||
|
- Render passes should be pure and can be skipped occasionally, therefore their context types ([`PaintCtx`] and [`AccessCtx`]) can't set invalidation flags or send signals.
|
||||||
|
- The `layout` and `compose` passes lay out all widgets, which are transiently invalid during the passes, therefore [`LayoutCtx`]and [`ComposeCtx`] cannot access the size and position of the `self` widget.
|
||||||
|
They can access the layout of children if they have already been laid out.
|
||||||
|
- For the same reason, `LayoutCtx`and `ComposeCtx` cannot create a `WidgetRef` reference to a child.
|
||||||
|
- [`MutateCtx`], [`EventCtx`] and [`UpdateCtx`] can let you add and remove children.
|
||||||
|
- [`RegisterCtx`] can't do anything except register children.
|
||||||
|
- [`QueryCtx`] provides read-only information about the widget.
|
||||||
|
|
||||||
|
[`LayoutCtx::place_child`]: crate::LayoutCtx::place_child
|
||||||
|
[`LayoutCtx::run_layout`]: crate::LayoutCtx::run_layout
|
||||||
|
[`WidgetMut`]: crate::widget::WidgetMut
|
||||||
|
[`RenderRoot`]: crate::RenderRoot
|
||||||
|
[`PaintCtx`]: crate::PaintCtx
|
||||||
|
[`AccessCtx`]: crate::AccessCtx
|
||||||
|
[`LayoutCtx`]: crate::LayoutCtx
|
||||||
|
[`ComposeCtx`]: crate::ComposeCtx
|
||||||
|
[`MutateCtx`]: crate::MutateCtx
|
||||||
|
[`EventCtx`]: crate::EventCtx
|
||||||
|
[`UpdateCtx`]: crate::UpdateCtx
|
||||||
|
[`RegisterCtx`]: crate::RegisterCtx
|
||||||
|
[`QueryCtx`]: crate::QueryCtx
|
|
@ -0,0 +1,105 @@
|
||||||
|
# Concepts and definitions
|
||||||
|
|
||||||
|
<!-- Copyright 2024 the Xilem Authors -->
|
||||||
|
<!-- SPDX-License-Identifier: Apache-2.0 -->
|
||||||
|
|
||||||
|
<div class="rustdoc-hidden">
|
||||||
|
|
||||||
|
> [!TIP]
|
||||||
|
>
|
||||||
|
> This file is intended to be read in rustdoc.
|
||||||
|
> Use `cargo doc --open --package masonry --no-deps`.
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
This section describes concepts mentioned by name elsewhere in the documentation and gives them a semi-formal definition for reference.
|
||||||
|
|
||||||
|
## Widget status
|
||||||
|
|
||||||
|
The notion of widget status is somewhat vague, but you can think of it as similar to [CSS pseudo-classes](https://developer.mozilla.org/en-US/docs/Web/CSS/Pseudo-classes).
|
||||||
|
|
||||||
|
Widget statuses are "things" managed by Masonry that affect how widgets are presented.
|
||||||
|
Statuses include:
|
||||||
|
|
||||||
|
- Being hovered.
|
||||||
|
- Having pointer capture.
|
||||||
|
- Having active text focus.
|
||||||
|
- Having inactive text focus.
|
||||||
|
- Being disabled.
|
||||||
|
- Being stashed.
|
||||||
|
|
||||||
|
When one of these statuses changes, the `update` method is called on the widget.
|
||||||
|
However, `update` can be called for reasons other than status changes.
|
||||||
|
|
||||||
|
|
||||||
|
## Pointer capture
|
||||||
|
|
||||||
|
When a user starts a pointer click on a widget, the widget can "capture" the pointer.
|
||||||
|
|
||||||
|
Pointer capture has a few implications:
|
||||||
|
|
||||||
|
- When a widget has captured a pointer, all events from that pointer will be sent to the widget, even if the pointer isn't in the widget's hitbox.
|
||||||
|
Conversely, no other widget can get events from the pointer (outside of bubbling).
|
||||||
|
- The "hovered" status of other widgets won't be updated even if the pointer is over them.
|
||||||
|
The hovered status of the capturing widget will be updated, meaning a widget that captured a pointer can still lose the "hovered" status.
|
||||||
|
- The pointer's cursor icon will be updated as if the pointer stayed over the capturing widget.
|
||||||
|
- If the widget loses pointer capture for some reason (e.g. the pointer is disconnected), the Widget will get a [`PointerLeave`] event.
|
||||||
|
|
||||||
|
Masonry should guarantee that pointers can only be captured by one widget at a time.
|
||||||
|
Masonry should force the widget to lose pointer capture when some events occur; not just MouseLeave, but also `Tab` being pressed, the window losing focus, the widget being disabled, etc.
|
||||||
|
|
||||||
|
Examples of use cases for pointer capture include selecting text, dragging a slider, or long-pressing a button.
|
||||||
|
|
||||||
|
|
||||||
|
## Text focus
|
||||||
|
|
||||||
|
Focus marks whether a widget receives text events.
|
||||||
|
|
||||||
|
To give a simple example, when you click a textbox, the textbox gets focus: anything you type on your keyboard will be sent to that textbox.
|
||||||
|
|
||||||
|
Focus can be changed with the tab key, or by clicking on a widget, both which Masonry automatically handles.
|
||||||
|
Widgets can also set custom focus behavior.
|
||||||
|
|
||||||
|
Note that widgets without text-edition capabilities such as buttons and checkboxes can also get focus.
|
||||||
|
For instance, pressing space when a button is focused will trigger that button.
|
||||||
|
|
||||||
|
There are two types of focus: active and inactive focus.
|
||||||
|
Active focus is the default one; inactive focus is when the window your app runs in has lost focus itself.
|
||||||
|
|
||||||
|
In that case, we still mark the widget as focused, but with a different color to signal that e.g. typing on the keyboard won't actually affect it.
|
||||||
|
|
||||||
|
|
||||||
|
## Disabled
|
||||||
|
|
||||||
|
A disabled widget is one which is visibly marked as non-interactive.
|
||||||
|
|
||||||
|
It is usually grayed out, and can't receive pointer or text events.
|
||||||
|
|
||||||
|
|
||||||
|
## Stashed
|
||||||
|
|
||||||
|
A stashed widget is one which is no longer "part of the logical tree", so to speak.
|
||||||
|
|
||||||
|
Stashed widgets can't receive keyboard or pointer events, don't get painted, aren't part of the accessibility tree, but should still keep some state.
|
||||||
|
|
||||||
|
The stereotypical stashed widget would be one inside a hidden tab in a "tab group" widget.
|
||||||
|
|
||||||
|
By contrast, widgets scrolled outside the viewport are **not** stashed: they can still get text events and are part of the accessibility tree.
|
||||||
|
|
||||||
|
|
||||||
|
## Interactivity
|
||||||
|
|
||||||
|
A widget is considered "interactive" if it can still get text and/or pointer events.
|
||||||
|
Stashed and disabled widget are non-interactive.
|
||||||
|
|
||||||
|
|
||||||
|
## Safety rails
|
||||||
|
|
||||||
|
When debug assertions are on, Masonry runs a bunch of checks every frame to make sure widget code doesn't have logical errors.
|
||||||
|
|
||||||
|
These checks are sometimes referred to as "safety rails".
|
||||||
|
|
||||||
|
Safety rails aren't guaranteed to run and may be disabled even in debug mode for performance reasons.
|
||||||
|
They should not be relied upon to check code correctness, but are meant to help you catch implementation errors early on during development.
|
||||||
|
|
||||||
|
[`PointerLeave`]: crate::PointerEvent::PointerLeave
|
|
@ -0,0 +1,43 @@
|
||||||
|
// Copyright 2024 the Xilem Authors
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
//! Documentation-only module
|
||||||
|
//!
|
||||||
|
//! This module includes a series of articles documenting the crate:
|
||||||
|
//!
|
||||||
|
//! - **Building a "To-Do List" app:** Tutorial to get started with Masonry.
|
||||||
|
//! - **Creating a new Widget:** Introduces the Widget trait.
|
||||||
|
//! - **Creating a container Widget:** Expands on the Widget trait for container Widgets.
|
||||||
|
//! - **Testing widgets in Masonry:** TODO.
|
||||||
|
//! - **Masonry pass system:** Deep dive into Masonry internals.
|
||||||
|
//! - **Concepts and definitions:** Glossary of concepts used in Masonry APIs and internals.
|
||||||
|
|
||||||
|
// TODO: Remove this once the issues within masonry are fixed. Tracked in https://github.com/linebender/xilem/issues/449
|
||||||
|
#![warn(rustdoc::broken_intra_doc_links)]
|
||||||
|
|
||||||
|
// These docs all use the .rustdoc-hidden trick described in
|
||||||
|
// https://linebender.org/blog/doc-include/
|
||||||
|
|
||||||
|
#[doc = include_str!("./01_creating_app.md")]
|
||||||
|
/// <style> .rustdoc-hidden { display: none; } </style>
|
||||||
|
pub mod doc_01_creating_app {}
|
||||||
|
|
||||||
|
#[doc = include_str!("./02_implementing_widget.md")]
|
||||||
|
/// <style> .rustdoc-hidden { display: none; } </style>
|
||||||
|
pub mod doc_02_implementing_widget {}
|
||||||
|
|
||||||
|
#[doc = include_str!("./03_implementing_container_widget.md")]
|
||||||
|
/// <style> .rustdoc-hidden { display: none; } </style>
|
||||||
|
pub mod doc_03_implementing_container_widget {}
|
||||||
|
|
||||||
|
#[doc = include_str!("./04_testing_widget.md")]
|
||||||
|
/// <style> .rustdoc-hidden { display: none; } </style>
|
||||||
|
pub mod doc_04_testing_widget {}
|
||||||
|
|
||||||
|
#[doc = include_str!("./05_pass_system.md")]
|
||||||
|
/// <style> .rustdoc-hidden { display: none; } </style>
|
||||||
|
pub mod doc_05_pass_system {}
|
||||||
|
|
||||||
|
#[doc = include_str!("./06_masonry_concepts.md")]
|
||||||
|
/// <style> .rustdoc-hidden { display: none; } </style>
|
||||||
|
pub mod doc_06_masonry_concepts {}
|
|
@ -22,8 +22,6 @@
|
||||||
//! use masonry::{Action, AppDriver, DriverCtx, WidgetId};
|
//! use masonry::{Action, AppDriver, DriverCtx, WidgetId};
|
||||||
//! use winit::window::Window;
|
//! use winit::window::Window;
|
||||||
//!
|
//!
|
||||||
//! const VERTICAL_WIDGET_SPACING: f64 = 20.0;
|
|
||||||
//!
|
|
||||||
//! struct Driver {
|
//! struct Driver {
|
||||||
//! next_task: String,
|
//! next_task: String,
|
||||||
//! }
|
//! }
|
||||||
|
@ -46,6 +44,8 @@
|
||||||
//! }
|
//! }
|
||||||
//!
|
//!
|
||||||
//! fn main() {
|
//! fn main() {
|
||||||
|
//! const VERTICAL_WIDGET_SPACING: f64 = 20.0;
|
||||||
|
//!
|
||||||
//! let main_widget = Portal::new(
|
//! let main_widget = Portal::new(
|
||||||
//! Flex::column()
|
//! Flex::column()
|
||||||
//! .with_child(
|
//! .with_child(
|
||||||
|
@ -75,7 +75,9 @@
|
||||||
//! }
|
//! }
|
||||||
//! ```
|
//! ```
|
||||||
//!
|
//!
|
||||||
//! ## Create feature flags
|
//! For more information, see [the documentation module](crate::doc).
|
||||||
|
//!
|
||||||
|
//! ## Crate feature flags
|
||||||
//!
|
//!
|
||||||
//! The following feature flags are available:
|
//! The following feature flags are available:
|
||||||
//!
|
//!
|
||||||
|
@ -93,6 +95,7 @@
|
||||||
// #![warn(missing_docs)]
|
// #![warn(missing_docs)]
|
||||||
#![warn(unused_imports)]
|
#![warn(unused_imports)]
|
||||||
#![warn(clippy::print_stdout, clippy::print_stderr, clippy::dbg_macro)]
|
#![warn(clippy::print_stdout, clippy::print_stderr, clippy::dbg_macro)]
|
||||||
|
#![allow(clippy::needless_doctest_main)]
|
||||||
#![allow(clippy::should_implement_trait)]
|
#![allow(clippy::should_implement_trait)]
|
||||||
#![allow(clippy::single_match)]
|
#![allow(clippy::single_match)]
|
||||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||||
|
@ -110,6 +113,9 @@ mod debug_logger;
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
mod debug_values;
|
mod debug_values;
|
||||||
|
|
||||||
|
#[cfg(doc)]
|
||||||
|
pub mod doc;
|
||||||
|
|
||||||
mod action;
|
mod action;
|
||||||
mod app_driver;
|
mod app_driver;
|
||||||
mod box_constraints;
|
mod box_constraints;
|
||||||
|
|
|
@ -100,6 +100,8 @@ pub trait Widget: AsAny {
|
||||||
/// the monitor's refresh, causing lag or jerky animations.
|
/// the monitor's refresh, causing lag or jerky animations.
|
||||||
fn on_anim_frame(&mut self, ctx: &mut UpdateCtx, interval: u64) {}
|
fn on_anim_frame(&mut self, ctx: &mut UpdateCtx, interval: u64) {}
|
||||||
|
|
||||||
|
// TODO - Reorder methods to match 02_implementing_widget.md
|
||||||
|
|
||||||
/// Register child widgets with Masonry.
|
/// Register child widgets with Masonry.
|
||||||
///
|
///
|
||||||
/// Leaf widgets can implement this with an empty body.
|
/// Leaf widgets can implement this with an empty body.
|
||||||
|
|
Loading…
Reference in New Issue