From e03dfdd82bd8a7043d9f842bc4d986423d090a93 Mon Sep 17 00:00:00 2001 From: Olivier FAURE Date: Tue, 22 Oct 2024 15:49:43 +0200 Subject: [PATCH] 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 <36049421+DJMcNab@users.noreply.github.com> --- masonry/README.md | 8 +- masonry/src/doc/01_creating_app.md | 263 ++++++++++++++ masonry/src/doc/02_implementing_widget.md | 336 ++++++++++++++++++ .../doc/03_implementing_container_widget.md | 270 ++++++++++++++ masonry/src/doc/04_testing_widget.md | 15 + masonry/src/doc/05_pass_system.md | 184 ++++++++++ masonry/src/doc/06_masonry_concepts.md | 105 ++++++ masonry/src/doc/mod.rs | 43 +++ masonry/src/lib.rs | 12 +- masonry/src/widget/widget.rs | 2 + 10 files changed, 1232 insertions(+), 6 deletions(-) create mode 100644 masonry/src/doc/01_creating_app.md create mode 100644 masonry/src/doc/02_implementing_widget.md create mode 100644 masonry/src/doc/03_implementing_container_widget.md create mode 100644 masonry/src/doc/04_testing_widget.md create mode 100644 masonry/src/doc/05_pass_system.md create mode 100644 masonry/src/doc/06_masonry_concepts.md create mode 100644 masonry/src/doc/mod.rs diff --git a/masonry/README.md b/masonry/README.md index 4e9e2b19..c1d2f873 100644 --- a/masonry/README.md +++ b/masonry/README.md @@ -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, } @@ -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( @@ -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: diff --git a/masonry/src/doc/01_creating_app.md b/masonry/src/doc/01_creating_app.md new file mode 100644 index 00000000..b4fff80f --- /dev/null +++ b/masonry/src/doc/01_creating_app.md @@ -0,0 +1,263 @@ +# Building a "To-Do List" app + + + + +
+ +> [!TIP] +> +> This file is intended to be read in rustdoc. +> Use `cargo doc --open --package masonry --no-deps`. + +
+ + +**TODO - Add screenshots - see [#501](https://github.com/linebender/xilem/issues/501)** + +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>> = 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>`. +- `root.child_mut()` returns a `WidgetMut>` for the `Portal`. +- `portal.child_mut()` returns a `WidgetMut` 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::::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>> = 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 diff --git a/masonry/src/doc/02_implementing_widget.md b/masonry/src/doc/02_implementing_widget.md new file mode 100644 index 00000000..47084cd6 --- /dev/null +++ b/masonry/src/doc/02_implementing_widget.md @@ -0,0 +1,336 @@ +# Creating a new Widget + + + + +
+ +> [!TIP] +> +> This file is intended to be read in rustdoc. +> Use `cargo doc --open --package masonry --no-deps`. + +
+ +**TODO - Add screenshots - see [#501](https://github.com/linebender/xilem/issues/501)** + +If you're building your own GUI framework on top of Masonry, or even a GUI app with specific needs, you'll want to specify your own widgets. + +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: + +```rust,ignore +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 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, bc: &BoxConstraints) -> Size; + + fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut Scene); + fn accessibility_role(&self) -> Role; + fn accessibility(&mut self, ctx: &mut AccessCtx, node: &mut NodeBuilder); + + // ... +} +``` + +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). +- `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 text focus). +- `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`: + +- 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::