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

Create skeleton of documentation #632

Merged
merged 11 commits into from
Oct 22, 2024
8 changes: 5 additions & 3 deletions masonry/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,6 @@ use masonry::widget::{Button, Flex, Label, Portal, RootWidget, Textbox, WidgetMu
use masonry::{Action, AppDriver, DriverCtx, WidgetId};
use winit::window::Window;

const VERTICAL_WIDGET_SPACING: f64 = 20.0;

struct Driver {
next_task: String,
}
Expand All @@ -63,6 +61,8 @@ impl AppDriver for Driver {
}

fn main() {
const VERTICAL_WIDGET_SPACING: f64 = 20.0;

let main_widget = Portal::new(
Flex::column()
.with_child(
Expand Down Expand Up @@ -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:

Expand Down
263 changes: 263 additions & 0 deletions masonry/src/doc/01_creating_app.md
PoignardAzur marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,263 @@
# Building a "To-Do List" app

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Applies to all markdown files)

Suggested change
<div class="rustdoc-hidden">
> [!TIP]
>
> This file is intended to be read in rustdoc.
> Use `cargo doc --open --package masonry --no-deps`.
</div>

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This still needs the newlines around the opening/closing tags, because the rendering is broken.

<!-- 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)**
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
**TODO - Add screenshots - see [#501](https://github.com/linebender/xilem/issues/501)**
<!-- TODO - Add screenshots - see [#501](https://github.com/linebender/xilem/issues/501) -->

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I'll leave it as-is. I'd rather have a visible reminder.


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
Loading