Skip to content

Commit

Permalink
Rewrite more doc
Browse files Browse the repository at this point in the history
  • Loading branch information
PoignardAzur committed Oct 17, 2024
1 parent 43de2cf commit 564b440
Show file tree
Hide file tree
Showing 5 changed files with 271 additions and 66 deletions.
195 changes: 166 additions & 29 deletions masonry/src/doc/01_creating_app.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,73 @@
**TODO - Add screenshots - see [#501](https://github.com/linebender/xilem/issues/501)**

This tutorial covers the to-do-list example shown in the README, and uses it as a support to explain the basic Masonry architecture.
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.

## Building a Masonry app
The app we'll create is identical to the to-do-list example shown in the README.

The first thing our code does after imports is to define a `Driver` which implements the `AppDriver` trait:
## The Widget tree

Let's start with the `main()` function.

```rust
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`) 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
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), 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
use masonry::app_driver::{AppDriver, DriverCtx};
use masonry::{Action, WidgetId};
use masonry::widget::{Label};

struct Driver {
next_task: String,
}
Expand All @@ -16,8 +77,8 @@ impl AppDriver for Driver {
match action {
Action::ButtonPressed(_) => {
let mut root: WidgetMut<RootWidget<Portal<Flex>>> = ctx.get_root();
let mut root = root.get_element();
let mut flex = root.child_mut();
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) => {
Expand All @@ -29,20 +90,73 @@ impl AppDriver for Driver {
}
```

The AppDriver implementation has access to the root app state.
Its methods are called whenever an "action" is emitted by the app.
Actions are user interactions with semantic meaning, like "click a Button" or "change the text in a Textbox".
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 `_widget_id` 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()` 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
// ...

let driver = Driver {
next_task: String::new(),
};

// ...

masonry::event_loop_runner::run(
// ...
main_widget,
driver,
)
.unwrap();
```

## Bringing it all together

In our case, we change our `next_task` text when text is entered in a textbox, and we add a new line to the list when a button is clicked.
The last step is to create our Winit window and start our main loop.

Because our button has a single textbox and a single button, there is no possible ambiguity as to which widget emitted the event, so we can ignore the `_widget_id` argument.
```rust
use masonry::dpi::LogicalSize;
use winit::window::Window;

Next is the main function:
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
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(
Expand All @@ -52,47 +166,70 @@ fn main() {
)
.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};

First we create our initial widget hierarchy.
`Portal` is a scrollable area, `Flex` is a container laid out with the flexbox algorithm, `Textbox` and `Button` are self-explanatory.
struct Driver {
next_task: String,
}

```rust
// ...
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_size = LogicalSize::new(400.0, 400.0);
let window_attributes = Window::default_attributes()
.with_title("To-do list")
.with_resizable(true)
.with_min_inner_size(window_size);
.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,
RootWidget::new(main_widget),
Driver {
next_task: String::new(),
},
main_widget,
driver,
)
.unwrap();
}
```

Finally, we create our Winit window and start our main loop.
All the Masonry examples follow this structure:

Not that we separately pass the widget tree, the `AppDriver` and an `EventLoopBuilder` to the `run` function.
- An initial widget tree.
- A struct implementing `AppDriver` to handle user interactions.
- A Winit window and event loop.

Once we call that function, the event loop runs until the user closes the program.
Some examples also define custom Widgets, but you can build an interactive app with Masonry's base widget set.
Doing so isn't *quite* recommended: most users will probably want to use a higher layer on top of Masonry.


## The Masonry architecture
## Higher layers

The above example isn't representative of how we expect Masonry to be used.

The code creates a Masonry app directly, and implements its own `AppDriver`.
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.
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.

Expand Down
84 changes: 47 additions & 37 deletions masonry/src/doc/02_implementing_widget.md
Original file line number Diff line number Diff line change
@@ -1,62 +1,72 @@
**TODO - This is copy-pasted from ARCHITECTURE.md, needs to be edited.**
(TODO - I'm rewriting this doc, shouldn't mention passes at all)

### WidgetMut
If you're building your own GUI framework on top of Masonry, or even a GUI app with specific needs, you'll probably want to invent your own widgets.

In Masonry, widgets can't be mutated directly. All mutations go through a `WidgetMut` wrapper. So, to change a label's text, you might call `WidgetMut<Label>::set_text()`. This helps Masonry make sure that internal metadata is propagated after every widget change.
This documentation explains how.

Generally speaking, to create a WidgetMut, you need a reference to the parent context that will be updated when the WidgetMut is dropped. That can be the WidgetMut of a parent, an EventCtx / LifecycleCtx, or the WindowRoot. In general, container widgets will have methods such that you can get a WidgetMut of a child from the WidgetMut of a parent.

WidgetMut gives direct mutable access to the widget tree. This can be used by GUI frameworks in their update method, and it can be used in tests to make specific local modifications and test their result.


**TODO - This is copy-pasted from the pass spec RFC, needs to be edited.**

## Editing the widget tree

Widgets can be added and removed during event and rewrite passes *except* inside layout and register_children methods.

Not doing so is a logic error and may trigger debug assertions.

If you do want to add or remove a child during layout, you can always defer it with the `mutate_later` context method.


## Widget methods and context types
## The Widget trait

Widgets are types which implement the `masonry::Widget` trait.

This trait includes a set of methods that must be implemented to hook into the different passes listed above:
This trait includes a set of methods that must be implemented to hook into Masonry's internals:

```rust
// Exact signatures may differ
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 register_children(&mut self, ctx: &mut RegisterCtx);
fn update(&mut self, ctx: &mut UpdateCtx, event: &UpdateEvent);
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) -> Size;
fn compose(&mut self, ctx: &mut ComposeCtx);

fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut Scene);
fn accessibility(&mut self, ctx: &mut AccessCtx);
fn accessibility(&mut self, ctx: &mut AccessCtx, node: &mut NodeBuilder);

// ...
}
```

These methods all take a given context type as a parameter.
Methods aside, `WidgetMut` references can provide a `MutateCtx` context.
`WidgetRef` references can provide a `QueryCtx` context, which is used in some read-only methods.
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 type 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 focus).
- `layout` is called during Masonry's layout pass. It takes size constraints and returns the widget's desired size.
- `paint`, `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 pass.

Most context types include these methods for requesting future passes:

- `request_render()`
- `request_paint_only()`
- `request_accessibility_update()`
- `request_layout()`
- `request_compose()`
- `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 `&mut self`.
- Through a `WidgetMut` wrapper. So, to change the label's text, you will call `WidgetMut<Label>::set_text()`. This helps Masonry make sure that internal metadata is propagated after every widget change.

TODO - edit_widget

Those context types have many methods, some shared, some unique to a given pass.
There are too many to document here, but we can lay out some general principles:
TODO - mutate_later

- 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.

## Example widget: ColorRectangle
Loading

0 comments on commit 564b440

Please sign in to comment.