Skip to content

Commit 87662ba

Browse files
committed
Update ARCHITECTURE.md file
Note: some of the doc depends on #832 and #848 which haven't been merged yet.
1 parent 8342d8f commit 87662ba

File tree

1 file changed

+56
-60
lines changed

1 file changed

+56
-60
lines changed

masonry/ARCHITECTURE.md

+56-60
Original file line numberDiff line numberDiff line change
@@ -1,78 +1,67 @@
11
# ARCHITECTURE
22

3-
**Note - The crate was migrated from the `PoignardAzur/masonry` repository and ported to work with winit. A lot of stuff was changed during that port and all the documentation wasn't updated. Some of this might be outdated.**
4-
53
Masonry is a framework that aims to provide the foundation for Rust GUI libraries.
64

7-
Developers trying to write immediate-mode GUIs, Elm-architecture GUIs, functional reactive GUIs, etc, can import Masonry and get a platform to create windows (using Glazier as a backend) each with a tree of widgets. Each widget has to implement the Widget trait that Masonry provides.
8-
9-
This crate was originally a fork of Druid that emerged from discussions I had with Raph Levien and Colin Rofls about what it would look like to turn Druid into a foundational library. This means the code looks very similar to Druid's code and has mostly the same dependencies and primitives.
5+
Developers trying to write immediate-mode GUIs, Elm-architecture GUIs, functional reactive GUIs, etc, can import Masonry and get a platform to create windows (using Winit as a backend) each with a tree of widgets. Each widget has to implement the Widget trait that Masonry provides.
106

117

128
## High-level goals
139

1410
Masonry has some opinionated design goals:
1511

1612
- **Be dumb.** As a general rule, Masonry doesn't do "algorithms". It has no reconciliation logic, no behind-the-scenes dataflow, no clever optimizations, etc. It tries to be efficient, but that efficiency comes from well-designed interfaces and well-placed abstraction boundaries. High-level logic should be implemented in downstream crates.
17-
- **No mutability tricks.** Masonry tries to use as little unsafe code and cells/mutexes as possible. It's designed to work within Rust's ownership system, not to bypass it.
13+
- **No mutability tricks.** Masonry uses no unsafe code and as few cells/mutexes as possible. It's designed to work within Rust's ownership system, not to bypass it. While it relies on the `tree_arena` crate in this repository which *does* perform some mutability tricks, the resulting usage patterns are still very rust-like.
1814
- **Facilitate testing.** Masonry implements a `TestHarness` type that helps users write unit tests, including tests with simulated user interactions and screenshot tests. In general, every feature should be designed with easy-to-write high-reliability tests in mind.
1915
- **Facilitate debugging.** GUI app bugs are often easy to fix, but extremely painful to track down. GUI framework bugs are worse. Masonry should facilitate reproducing bugs and pinpointing which bit of code they come from.
2016
- **Provide reflection.** Masonry should help developers surface some of the inherent structure in GUI programs. It should provide tools out-of-the-box to get information about the widget tree, performance indicators, etc. It should also provide accessibility data out-of-the-box.
2117

2218

2319
## Code layout
2420

25-
### `src/testing/`
21+
### `src/core/`
2622

27-
Contains the TestHarness type, various helper widgets for writing tests, and the snapshot testing code.
23+
Most widget-related code, including the Widget trait, its context types, event types, and the WidgetRef, WidgetMut, and WidgetPod types.
24+
25+
#### `src/core/widget_state.rs`
2826

29-
### `src/text2/`
27+
Contains the WidgetState type, around which a lot of internal code is based.
3028

31-
Contains text-handling code, for both displaying and editing text. Has been overhauled during the port to winit, but still in rough shape. Here be dragons.
29+
WidgetState is one of the most important internal types in Masonry.
30+
Understanding Masonry pass code will likely be easier if you read WidgetState documentation first.
3231

33-
### `src/text/`
32+
### `src/app/`
3433

35-
Dead code, should probably be cleaned up.
34+
Code for creating a Masonry app, including:
3635

37-
### `src/widget/`
36+
- `event_loop_runner.rs` - glue code between Masonry and winit.
37+
- `render_root.rs` - Masonry's composition root. See **General architecture** section.
3838

39-
Contains widget-related items, including the Widget trait, and the WidgetRef, WidgetMut, and WidgetPod types.
39+
### `src/passes/`
4040

41-
Also includes a list of basic widgets, each defined in a single file.
41+
Masonry's passes are computations that run on the entire widget tree (iff invalidation flags are set) once per frame.
4242

43-
### `src/widget/widget_state.rs`
43+
`event.rs` and `update.rs` include a bunch of related passes. Every other file only includes one pass. `mod.rs` has a utility functions shared between multiple passes.
4444

45-
Contains the WidgetState type, around which a lot of internal code is based.
45+
### `src/doc/`
4646

47-
WidgetState is one of the most important internal types in Masonry.
48-
Understanding Masonry passes will likely be easier if you read WidgetState documentation first.
47+
Documentation for the entire crate. In other projects, this would be an `mdbook` doc, but we choose to directly inline the doc, so that `cargo test` runs on it.
4948

49+
### `src/testing/`
5050

51-
### `src/render_root.rs`
51+
Contains the TestHarness type, various helper widgets for writing tests, and the snapshot testing code.
5252

53-
The composition root of the framework. See **General architecture** section.
53+
### `src/widgets/`
5454

55-
### `src/debug_logger.rs`, `src/debug_values.rs`
55+
A list of basic widgets, each defined in a single file.
5656

57-
WIP logger to get record of widget passes. See <https://github.com/linebender/xilem/issues/370>.
5857

5958
## Module organization principles
6059

6160
(Some of these principles aren't actually applied in the codebase yet. See <https://github.com/linebender/xilem/issues/367>.)
6261

6362
### Module structure
6463

65-
Virtually every module should be private. The only public modules should be inline module blocks that gather public-facing re-exports.
66-
67-
Most items should be exported from the root module, with no other public-facing export. This makes documentation more readable; readers don't need to click on multiple modules to find the item they're looking for.
68-
69-
There should be only three public modules:
70-
71-
- `widgets`
72-
- `commands`
73-
- `test_widgets`
74-
75-
Module files should not be `foobar/mod.rs`. Instead, they should be `_foobar.rs` (thus in the parent folder); the full name is for readability, the leading underscore is so these names appear first in file hierarchies.
64+
The public module hierarchy should be relatively flat. This makes documentation more readable; readers don't need to click on multiple modules to find the item they're looking for, and maintainers don't need to open multiple layers of folders to find a file.
7665

7766
### Imports
7867

@@ -91,48 +80,56 @@ Masonry should have no prelude. Examples and documentation should deliberately h
9180

9281
The composition roots of Masonry are:
9382

94-
- **RenderRoot**
95-
- The **AppDriver** trait.
83+
- **RenderRoot**, which owns the widget tree.
84+
- The **AppDriver** trait, which owns the business logic.
9685
- The **run_with** function in `event_loop_runner.rs`.
9786

98-
TODO - Explain in more detail.
99-
10087
The high-level control flow of a Masonry app's render loop is usually:
10188

10289
- The platform library (windows, macos, x11, etc) runs some callbacks written in Winit in response to user interactions, timers, or other events.
103-
- The callbacks call MasonryWinHandler methods.
104-
- Each method calls a single AppRoot method.
105-
- That method calls some AppRootInner method.
106-
- AppRootInner does a bunch of bookkeeping and calls WindowRoot methods.
107-
- WindowRoot does a bunch of bookkeeping and calls the root WidgetPod's on_event/lifecycle/layout/paint methods.
90+
- Winit calls some RenderRoot method.
91+
- RenderRoot runs a series of passes (events, updates, paint, accessibility, etc).
92+
- Throughout those passes, RenderRootSignal values are pushed to a queue which winit can query.
93+
- winit may change its internals or schedule another frame based on those signals.
10894

10995

11096
### Widget hierarchy and passes
11197

112-
The **Widget** trait is defined in `src/widget/widget.rs`. Most of the widget-related bookkeeping is done by the **WidgetPod** type defined in `src/widget/widget_pod.rs`.
98+
The **Widget** trait is defined in `src/widget/widget.rs`. Most of the widget-related bookkeeping is done in the passes defined in `src/passes`.
99+
100+
A **WidgetPod** is the main way to store a child for container widgets. In general, the widget hierarchy looks like a tree of container widgets, with each container owning a WidgetPod or a Vec of WidgetPod or something similar.
113101

114-
A WidgetPod is the main way to store a child for container widgets. In general, the widget hierarchy looks like a tree of container widgets, with each container owning a WidgetPod or a Vec of WidgetPod or something similar. When a pass (on_event/lifecycle/layout/paint) is run on a window, the matching method is called on the root WidgetPod, which recurses to its widget, which calls the same method on each of its children.
102+
When RenderRoot runs a pass (on_xxx_event/update_xxx/paint), it usually iterates over the widget tree in depth-first pre-order, and calls the matching method is called on the root WidgetPod, which recurses to its widget, which calls the same method on each of its children.
115103

116-
Currently, container Widgets are encouraged to call pass methods on each of their children, even when the pass only concerns a single child (eg a click event where only one child is under the mouse); the filtering, if any, is done in WidgetPod.
104+
There is one exception to this pattern: the layout pass requires widgets to "manually" recurse to their children.
117105

118106
The current passes are:
119107

120-
- **on_xxx_event:** Handles UX-related events, eg user interactions, timers, and IME updates. Widgets can declare these events as "handled" which has a bunch of semantic implications. Right now this pass is split between "cursor events" (eg mouse stuff) and "text events" (eg IME, keyboard stuff).
121-
- **on_status_change:** TODO.
122-
- **lifecycle:** Handles internal events, eg when the widget is marked as "disabled".
123-
- **layout:** Given size constraints, return the widget's size. Container widgets first call their children's layout method, then set the position and layout date of each child.
124-
- **paint** Paint the widget and its children.
108+
- **mutate:** Runs a list of callbacks with mutable access to the widget tree. These callbacks can either be passed by the event loop, or pushed to a queue by widgets during the other passes.
109+
- **on_xxx_event:** Handles UX-related events, e.g. clicks, text entered, IME updates and accessibility input. Widgets can declare these events as "handled" which has a bunch of semantic implications.
110+
- **anim:** Do updates related to an animation frame.
111+
- **update:** Handles internal changes to some widgets, e.g. when the widget is marked as "disabled" or Masonry detects that a widget is hovered by a pointer.
112+
- **layout:** Given size constraints, return the widget's size. Container widgets first call `LayoutCtx::run_layout` on their children, then set the position of each child.
113+
- **compose:** Computes the global transform/origin for every widget.
114+
- **paint** Paint every widget.
115+
- **accessibility:** Compute every widget's node in the accessibility tree.
125116

126-
The general pass order is "For each user event, call on_event once, then lifecycle a variable number of times, then schedule a paint. When the platform starts the paint, run layout, then paint".
117+
See [masonry/src/doc/05_pass_system.md] for details.
127118

128119

129120
### WidgetMut
130121

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

133-
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.
124+
In general, there's three ways to get a WidgetMut:
125+
126+
- From a WidgetMut to a parent widget.
127+
- As an argument to the callback passed to `RenderRoot::edit_widget()`.
128+
- As an argument to a callback pushed to the mutate pass.
134129

135-
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.
130+
In most cases, the WidgetMut holds a reference to a WidgetState that will be updated when the WidgetMut is dropped.
131+
132+
WidgetMut gives direct mutable access to the widget tree. This can be used by GUI frameworks in their tree update methods, and it can be used in tests to make specific local modifications and test their result.
136133

137134

138135
### Tests
@@ -147,16 +144,15 @@ Ideally, the harness should provide ways to emulate absolutely every feature tha
147144

148145
Each widget has unit tests in its module and major features have modules with dedicated unit test suites. Ideally, we would like to achieve complete coverage within the crate.
149146

150-
#### Mock timers
151-
152-
For timers in particular, the framework does some special work to simulate the GUI environment.
147+
#### Screenshot tests
153148

154-
The GlobalPassCtx types stores two timer handlers: **timers** and **mock_timer_queue**. The first one connects ids returned by the platform's timer creator to widget ids; the second one stores a list of timer values that have to be manually advanced by calling `TestHarness::move_timers_forward`.
149+
TODO - mention kompari
155150

156-
When a widget calls `request_timer` in a normal running app, a normal timer is requested from the platform. When a widget calls `request_timer` from a simulated app inside a TestHarness, mock_timer_queue is used instead.
151+
TestHarness can render a widget tree, save the result to an image, and compare the image to a stored snapshot. This lets us check that (1) our widgets' paint methods don't panic and (2) changes don't introduce accidental regression in their visual appearance.
157152

158-
All this means you can have timer-based tests without *actually* having to sleep for the duration of the timer.
153+
The screenshots are stored using git LFS, which adds some minor complications but avoids the overhead of committing files directly to Git.
159154

155+
We include some of the screenshots in the documentation; because `docs.rs` doesn't have access to LFS files, we use the `include_screenshot!` to instead link to `https://media.githubusercontent.com` when building doc for `docs.rs`.
160156

161157
## VS Code markers
162158

0 commit comments

Comments
 (0)