Skip to content

Commit e03dfdd

Browse files
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 <[email protected]>
1 parent 30dba40 commit e03dfdd

10 files changed

+1232
-6
lines changed

masonry/README.md

+5-3
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,6 @@ use masonry::widget::{Button, Flex, Label, Portal, RootWidget, Textbox, WidgetMu
3939
use masonry::{Action, AppDriver, DriverCtx, WidgetId};
4040
use winit::window::Window;
4141

42-
const VERTICAL_WIDGET_SPACING: f64 = 20.0;
43-
4442
struct Driver {
4543
next_task: String,
4644
}
@@ -63,6 +61,8 @@ impl AppDriver for Driver {
6361
}
6462

6563
fn main() {
64+
const VERTICAL_WIDGET_SPACING: f64 = 20.0;
65+
6666
let main_widget = Portal::new(
6767
Flex::column()
6868
.with_child(
@@ -91,7 +91,9 @@ fn main() {
9191
}
9292
```
9393

94-
### Create feature flags
94+
For more information, see [the documentation module](https://docs.rs/masonry/latest/masonry/doc/).
95+
96+
### Crate feature flags
9597

9698
The following feature flags are available:
9799

masonry/src/doc/01_creating_app.md

+263
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
# Building a "To-Do List" app
2+
3+
<!-- Copyright 2024 the Xilem Authors -->
4+
<!-- SPDX-License-Identifier: Apache-2.0 -->
5+
6+
<div class="rustdoc-hidden">
7+
8+
> [!TIP]
9+
>
10+
> This file is intended to be read in rustdoc.
11+
> Use `cargo doc --open --package masonry --no-deps`.
12+
13+
</div>
14+
15+
16+
**TODO - Add screenshots - see [#501](https://github.com/linebender/xilem/issues/501)**
17+
18+
This tutorial explains how to build a simple Masonry app, step by step.
19+
Though it isn't representative of how we expect Masonry to be used, it does cover the basic architecture.
20+
21+
The app we'll create is identical to the to-do-list example shown in the README.
22+
23+
## The Widget tree
24+
25+
Let's start with the `main()` function.
26+
27+
```rust,ignore
28+
fn main() {
29+
const VERTICAL_WIDGET_SPACING: f64 = 20.0;
30+
31+
use masonry::widget::{Button, Flex, Portal, RootWidget, Textbox};
32+
33+
let main_widget = Portal::new(
34+
Flex::column()
35+
.with_child(
36+
Flex::row()
37+
.with_flex_child(Textbox::new(""), 1.0)
38+
.with_child(Button::new("Add task")),
39+
)
40+
.with_spacer(VERTICAL_WIDGET_SPACING),
41+
);
42+
let main_widget = RootWidget::new(main_widget);
43+
44+
// ...
45+
46+
masonry::event_loop_runner::run(
47+
// ...
48+
main_widget,
49+
// ...
50+
)
51+
.unwrap();
52+
}
53+
```
54+
55+
First we create our initial widget hierarchy.
56+
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`]).
57+
58+
We wrap it in a [`RootWidget`], whose main purpose is to include a `Window` node in the accessibility tree.
59+
60+
At the end of the main function, we pass the root widget to the `event_loop_runner::run` function.
61+
That function starts the main event loop, which runs until the user closes the window.
62+
During the course of the event loop, the widget tree will be displayed, and updated as the user interacts with the app.
63+
64+
65+
## The `Driver`
66+
67+
To handle user interactions, we need to implement the [`AppDriver`] trait:
68+
69+
```rust,ignore
70+
trait AppDriver {
71+
fn on_action(&mut self, ctx: &mut DriverCtx<'_>, widget_id: WidgetId, action: Action);
72+
}
73+
```
74+
75+
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.
76+
77+
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.
78+
79+
We create a `Driver` struct to store a very simple app's state, and we implement the `AppDriver` trait for it:
80+
81+
```rust,ignore
82+
use masonry::app_driver::{AppDriver, DriverCtx};
83+
use masonry::{Action, WidgetId};
84+
use masonry::widget::{Label};
85+
86+
struct Driver {
87+
next_task: String,
88+
}
89+
90+
impl AppDriver for Driver {
91+
fn on_action(&mut self, ctx: &mut DriverCtx<'_>, _widget_id: WidgetId, action: Action) {
92+
match action {
93+
Action::ButtonPressed(_) => {
94+
let mut root: WidgetMut<RootWidget<Portal<Flex>>> = ctx.get_root();
95+
let mut portal = root.child_mut();
96+
let mut flex = portal.child_mut();
97+
flex.add_child(Label::new(self.next_task.clone()));
98+
}
99+
Action::TextChanged(new_text) => {
100+
self.next_task = new_text.clone();
101+
}
102+
_ => {}
103+
}
104+
}
105+
}
106+
```
107+
108+
In `on_action`, we handle the two possible actions:
109+
110+
- `TextChanged`: Update the text of the next task.
111+
- `ButtonPressed`: Add a task to the list.
112+
113+
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.
114+
115+
When handling `ButtonPressed`:
116+
117+
- `ctx.get_root()` returns a `WidgetMut<RootWidget<...>>`.
118+
- `root.child_mut()` returns a `WidgetMut<Portal<...>>` for the `Portal`.
119+
- `portal.child_mut()` returns a `WidgetMut<Flex>` for the `Flex`.
120+
121+
A [`WidgetMut`] is a smart reference type which lets us modify the widget tree.
122+
It's set up to automatically propagate update flags and update internal state when dropped.
123+
124+
We use [`WidgetMut::<Flex>::add_child()`][add_child] to add a new `Label` with the text of our new task to our list.
125+
126+
In our main function, we create a `Driver` and pass it to `event_loop_runner::run`:
127+
128+
```rust,ignore
129+
// ...
130+
131+
let driver = Driver {
132+
next_task: String::new(),
133+
};
134+
135+
// ...
136+
137+
masonry::event_loop_runner::run(
138+
// ...
139+
main_widget,
140+
driver,
141+
)
142+
.unwrap();
143+
```
144+
145+
## Bringing it all together
146+
147+
The last step is to create our Winit window and start our main loop.
148+
149+
```rust,ignore
150+
use masonry::dpi::LogicalSize;
151+
use winit::window::Window;
152+
153+
let window_attributes = Window::default_attributes()
154+
.with_title("To-do list")
155+
.with_resizable(true)
156+
.with_min_inner_size(LogicalSize::new(400.0, 400.0));
157+
158+
masonry::event_loop_runner::run(
159+
masonry::event_loop_runner::EventLoop::with_user_event(),
160+
window_attributes,
161+
main_widget,
162+
driver,
163+
)
164+
.unwrap();
165+
```
166+
167+
Our complete program therefore looks like this:
168+
169+
```rust,ignore
170+
fn main() {
171+
const VERTICAL_WIDGET_SPACING: f64 = 20.0;
172+
173+
use masonry::widget::{Button, Flex, Portal, RootWidget, Textbox};
174+
175+
let main_widget = Portal::new(
176+
Flex::column()
177+
.with_child(
178+
Flex::row()
179+
.with_flex_child(Textbox::new(""), 1.0)
180+
.with_child(Button::new("Add task")),
181+
)
182+
.with_spacer(VERTICAL_WIDGET_SPACING),
183+
);
184+
let main_widget = RootWidget::new(main_widget);
185+
186+
use masonry::app_driver::{AppDriver, DriverCtx};
187+
use masonry::{Action, WidgetId};
188+
use masonry::widget::{Label};
189+
190+
struct Driver {
191+
next_task: String,
192+
}
193+
194+
impl AppDriver for Driver {
195+
fn on_action(&mut self, ctx: &mut DriverCtx<'_>, _widget_id: WidgetId, action: Action) {
196+
match action {
197+
Action::ButtonPressed(_) => {
198+
let mut root: WidgetMut<RootWidget<Portal<Flex>>> = ctx.get_root();
199+
let mut portal = root.child_mut();
200+
let mut flex = portal.child_mut();
201+
flex.add_child(Label::new(self.next_task.clone()));
202+
}
203+
Action::TextChanged(new_text) => {
204+
self.next_task = new_text.clone();
205+
}
206+
_ => {}
207+
}
208+
}
209+
}
210+
211+
let driver = Driver {
212+
next_task: String::new(),
213+
};
214+
215+
use masonry::dpi::LogicalSize;
216+
use winit::window::Window;
217+
218+
let window_attributes = Window::default_attributes()
219+
.with_title("To-do list")
220+
.with_resizable(true)
221+
.with_min_inner_size(LogicalSize::new(400.0, 400.0));
222+
223+
masonry::event_loop_runner::run(
224+
masonry::event_loop_runner::EventLoop::with_user_event(),
225+
window_attributes,
226+
main_widget,
227+
driver,
228+
)
229+
.unwrap();
230+
}
231+
```
232+
233+
All the Masonry examples follow this structure:
234+
235+
- An initial widget tree.
236+
- A struct implementing `AppDriver` to handle user interactions.
237+
- A Winit window and event loop.
238+
239+
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.
240+
241+
242+
## Higher layers
243+
244+
The above example isn't representative of how we expect Masonry to be used.
245+
246+
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.
247+
248+
Currently, the only public framework built with Masonry is Xilem, though we hope others will develop as Masonry matures.
249+
250+
Most of this documentation is written to help developers trying to build such a framework.
251+
252+
[`Portal`]: crate::widget::Portal
253+
[`Flex`]: crate::widget::Flex
254+
[`Textbox`]: crate::widget::Textbox
255+
[`Button`]: crate::widget::Button
256+
[`RootWidget`]: crate::widget::RootWidget
257+
258+
[`AppDriver`]: crate::AppDriver
259+
[`Action`]: crate::Action
260+
[`DriverCtx`]: crate::DriverCtx
261+
[`WidgetId`]: crate::WidgetId
262+
[`WidgetMut`]: crate::widget::WidgetMut
263+
[add_child]: crate::widget::WidgetMut::add_child

0 commit comments

Comments
 (0)