Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rewrite tutorial #813

Merged
merged 4 commits into from
Jan 19, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
245 changes: 167 additions & 78 deletions masonry/src/doc/02_implementing_widget.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,7 @@ 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:
Widgets are types which implement the [`Widget`] trait:

```rust,ignore
trait Widget {
Expand All @@ -44,9 +42,6 @@ trait Widget {
}
```

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).
Expand All @@ -55,61 +50,11 @@ In the course of a frame, Masonry will run a series of passes over the widget tr
- `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`:
## Our example widget: `ColorRectangle`

- 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 [`RenderRoot::edit_root_widget()`] method.
This methods returns a WidgetMut to the root widget, which you can then project into a WidgetMut reference to its descendants.

### 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).
Let's implement a very simple widget named `ColorRectangle`.
This widget has a size, a color, and will notify Masonry when the user left-clicks on it (on mouse press; we'll ignore mouse release).

First, let's create our struct:

Expand All @@ -130,9 +75,10 @@ impl ColorRectangle {
```

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.
Note that we store a size, and not a position: our widget's position is managed by its parent.


### Implementing the Widget trait
### Event methods

First we implement event methods:

Expand Down Expand Up @@ -166,15 +112,23 @@ impl Widget for ColorRectangle {
}
```

Here we've written a simple handler which filters pointer events for left clicks, and submits a [`ButtonPressed`] action.
We handle pointer events and accessibility events the same way: we check the event type, and if it's a left-click, we submit an action.

Submitting an action lets Masonry that a semantically meaningful event has occurred; Masonry will call `AppDriver::on_action()` with the action before the end of the frame.
This lets higher-level frameworks like Xilem react to UI events, like a button being pressed.

Implementing `on_access_event` lets us emulate click behaviors for people using assistive technologies.

We don't handle any text events.

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:
### Animation and update

Since our widget isn't animated and doesn't react to changes in status, we can leave the `on_anim_frame` and `update` implementations empty:

```rust,ignore
use masonry::{
UpdateCtx
UpdateCtx, Update,
};

impl Widget for ColorRectangle {
Expand All @@ -187,6 +141,8 @@ impl Widget for ColorRectangle {
}
```

### Layout

Next we implement layout:

```rust,ignore
Expand All @@ -197,15 +153,20 @@ use masonry::{
impl Widget for ColorRectangle {
// ...

fn layout(&mut self, _ctx: &mut LayoutCtx, _bc: &BoxConstraints) -> Size {
self.size
fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
bc.constrain(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.
Our size is static, and doesn't perform complex operations like line-breaking, or depend on context information like "the widget is currently hovered", so it can be written as a one-liner.

`BoxConstraints` are the minimum and maximum size our parent expects this widget to respect. It's possible to ignore them, but we choose not to.
We return our stored size, clamped between the min and max constraints.

### Render methods

Next we write our render methods:

Expand Down Expand Up @@ -244,7 +205,7 @@ impl Widget for ColorRectangle {

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`.
The rectangle's origin is zero (because coordinates in our scene are local to our widget), and its size is `ctx.size()`, which is the value returned by `layout()`.

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.
Expand All @@ -255,6 +216,8 @@ In `accessibility`, we define a default action of `Click`, which is how we regis

<!-- TODO - Is that actually true? I'm not sure what set_default_action does. -->

### Other methods

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
Expand Down Expand Up @@ -291,25 +254,148 @@ impl Widget for ColorRectangle {

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. -->
## Context arguments

The methods of the [`Widget`] trait take `***Ctx` context arguments, which you can use to get information about the current widget.
For instance, our `paint()` method used [`PaintCtx::size()`] to get the widget's size.
The information accessible from the context argument depends on the method.

### Status flags

All context types have getters to check some status information:

- `is_hovered()`
- `has_pointer_capture()`
- `is_focused()`
- `is_disabled()`
- `is_stashed()`

See the ["Concepts and definitions"](06_masonry_concepts.md#widget-status) documentation for more information on what they mean.

### Requesting passes

Most passes will skip most widgets by default.

For example, the animate pass will only run on widgets running an animation, and the paint pass will only call a widget's `paint` method once, after which it caches 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()`


### Using context in `ColorRectangle`

To show how context types are used in practice, let's add a feature to `ColorRectangle`: the widget will now be painted in white when hovered.

First, we update our paint method:

```rust,ignore
impl Widget for ColorRectangle {
// ...

fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut Scene) {
let rect = ctx.size().to_rect();
let color = if ctx.is_hovered() {
Color::WHITE
} else {
self.color
};
scene.fill(
Fill::NonZero,
Affine::IDENTITY,
color,
Some(Affine::IDENTITY),
&rect,
);
}

// ...
}
```

Next, we need to detect hover changes so the paint method actually gets called.

Let's re-implement the `update` method:

```rust,ignore
impl Widget for ColorRectangle {
// ...

fn update(&mut self, ctx: &mut UpdateCtx, event: &Update) {
match event {
Update::HoveredChanged(_) => {
ctx.request_render();
}
_ => {}
}
}

// ...
}
```

## 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 [`Label::set_text()`], which takes a [`WidgetMut`] argument. 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 [`RenderRoot::edit_root_widget()`] method.
This methods returns a WidgetMut to the root widget, which you can then project into a WidgetMut reference to its descendants.

### 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()`.


### Mutating `ColorRectangle`

Let's define some setters for `ColorRectangle`:

```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();
impl ColorRectangle {
pub fn set_color(this: &mut WidgetMut<'_, Self>, color: Color) {
this.widget.color = color;
this.ctx.request_paint_only();
}

pub fn set_size(&mut self, size: Size) {
self.widget.size = size;
self.ctx.request_layout();
pub fn set_size(this: &mut WidgetMut<'_, Self>, size: Size) {
this.widget.size = size;
this.ctx.request_layout();
}
}
```
Expand All @@ -326,7 +412,10 @@ The next one is about creating a container widgets, and the complications it add

[`Widget`]: crate::Widget
[`WidgetMut`]: crate::widget::WidgetMut
[`PaintCtx::size()`]: crate::PaintCtx::size
[`UpdateCtx::request_paint_only()`]: crate::UpdateCtx::request_paint_only
[`ButtonPressed`]: crate::Action::ButtonPressed
[`vello::Scene`]: crate::vello::Scene
[`Role::Button`]: accesskit::Role::Button
[`RenderRoot::edit_root_widget()`]: crate::RenderRoot::edit_root_widget
[`Label::set_text()`]: crate::widget::Label::set_text
20 changes: 10 additions & 10 deletions masonry/src/doc/03_implementing_container_widget.md
Original file line number Diff line number Diff line change
Expand Up @@ -198,20 +198,20 @@ 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();
impl VerticalStack {
pub fn add_child(this: &mut WidgetMut<'_, Self>, child: WidgetPod<Box<dyn Widget>>) {
this.widget.children.push(child);
this.ctx.children_changed();
}

pub fn remove_child(&mut self, n: usize) {
self.widget.children.remove(n);
self.ctx.children_changed();
pub fn remove_child(this: &mut WidgetMut<'_, Self>, n: usize) {
this.widget.children.remove(n);
this.ctx.children_changed();
}

pub fn clear_children(&mut self) {
self.widget.children.clear();
self.ctx.children_changed();
pub fn clear_children(this: &mut WidgetMut<'_, Self>) {
this.widget.children.clear();
this.ctx.children_changed();
}
}
```
Expand Down
Loading