Skip to content

Commit

Permalink
Document "Properties" concept
Browse files Browse the repository at this point in the history
  • Loading branch information
PoignardAzur committed Feb 19, 2025
1 parent 31eb79d commit f7c1d94
Show file tree
Hide file tree
Showing 7 changed files with 143 additions and 18 deletions.
2 changes: 2 additions & 0 deletions masonry/src/core/widget_mut.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,10 @@ use crate::kurbo::Affine;
///
/// Once the Receiver trait is stabilized, `WidgetMut` will implement it so that custom
/// widgets in downstream crates can use `WidgetMut` as the receiver for inherent methods.
#[non_exhaustive]
pub struct WidgetMut<'a, W: Widget + ?Sized> {
pub ctx: MutateCtx<'a>,
#[doc(hidden)]
pub properties: PropertiesMut<'a>,
pub widget: &'a mut W,
}
Expand Down
2 changes: 1 addition & 1 deletion masonry/src/doc/02_implementing_widget.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ impl Widget for ColorRectangle {

We handle pointer events and accessibility events the same way: we check the event type, and if it's a left-click, we submit an action.

Submitting an action lets Masonry that a semantically meaningful event has occurred; Masonry will call `AppDriver::on_action()` with the action before the end of the frame.
Submitting an action lets Masonry know that a semantically meaningful event has occurred; Masonry will call `AppDriver::on_action()` with the action before the end of the frame.
This lets higher-level frameworks like Xilem react to UI events, like a button being pressed.

Implementing `on_access_event` lets us emulate click behaviors for people using assistive technologies.
Expand Down
110 changes: 110 additions & 0 deletions masonry/src/doc/04b_widget_properties.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# Reading Widget Properties

<!-- 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)**

Throughout the previous chapters, you may have noticed that most Widget methods take a `props: &PropertiesRef<'_>` or `props: &mut PropertiesMut<'_>` argument.
We haven't used these arguments so far, and you can build a robust widget set without them, but they're helpful for making your widgets more customizable and modular.


## What are Properties?

In Masonry, **Properties** (often abbreviated to **props**) are values of arbitrary static types stored alongside each widget.

In simpler terms, that means you can take any non-ref type (e.g. `struct RubberDuck(Color, String, Buoyancy);`) and associate a value of that type to any widget, including widgets of existing types (`Button`, `Checkbox`, `Textbox`, etc) or your own custom widget (`ColorRectangle`).

Code accessing the property will look like:

```rust,ignore
if let Some(ducky) = props.get::<RubberDuck>() {
let (color, name, buoyancy) = ducky;
// ...
}
```

### When to use Properties?

<!-- TODO - Mention event handling -->
<!-- I expect that properties will be used to share the same pointer event handling code between Button, SizedBox, Textbox, etc... -->

In practice, properties should mostly be used for styling.

Properties should be defined to represent self-contained values that a widget can have, that are expected to make sense for multiple types of widgets, and where code handling those values should be shared between widgets.

Some examples:

- `BackgroundColor`
- BorderColor
- Padding
- TextFont
- TextSize
- TextWeight

**TODO: Most of the properties cited above do *not* exist in Masonry's codebase. They should hopefully be added quickly.**

Properties should *not* be used to represent an individual widget's state. The following should *not* be properties:

- Text contents.
- Cached values.
- A checkbox's status.

<!-- TODO - Mention properties as a unit of code sharing, once we have concrete examples of that. -->


## Using properties in `ColorRectangle`

With that in mind, let's rewrite our `ColorRectangle` widget to use properties:

```rust,ignore
use masonry::properties::BackgroundColor;
impl Widget for ColorRectangle {
// ...
fn paint(&mut self, ctx: &mut PaintCtx, props: &PropertiesRef<'_>, scene: &mut Scene) {
let color = props.get::<BackgroundColor>().unwrap_or(masonry::palette::css::WHITE);
let rect = ctx.size().to_rect();
scene.fill(
Fill::NonZero,
Affine::IDENTITY,
color,
Some(Affine::IDENTITY),
&rect,
);
}
// ...
}
```

## Setting properties in `WidgetMut`

The most idiomatic way to set properties is through `WidgetMut`:

```rust,ignore
let color_rectangle_mut: WidgetMut<ColorRectangle> = ...;
let bg = BackgroundColor { color: masonry::palette::css::BLUE };
color_rectangle_mut.insert_prop(bg);
```

This code will set the given rectangle's `BackgroundColor` (replacing the old one if there was one) to blue.

You can set as many properties as you want.
Properties are an associative map, where types are the keys.

But setting a property to a given value doesn't change anything by default, unless your widget code specifically reads that value and does something with it.

<!-- TODO - Mention "transform" property. -->
7 changes: 7 additions & 0 deletions masonry/src/doc/06_masonry_concepts.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,13 @@ A widget is considered "interactive" if it can still get text and/or pointer eve
Stashed and disabled widget are non-interactive.


## Properties / Props

All widgets have associated data of arbitrary types called "properties".
These properties are mostly used for styling and event handling.

See [Reading Widget Properties](crate::doc::04b_widget_properties) for more info.

## Safety rails

When debug assertions are on, Masonry runs a bunch of checks every frame to make sure widget code doesn't have logical errors.
Expand Down
4 changes: 4 additions & 0 deletions masonry/src/doc/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ pub mod doc_03_implementing_container_widget {}
/// <style> .rustdoc-hidden { display: none; } </style>
pub mod doc_04_testing_widget {}

#[doc = include_str!("./04b_widget_properties.md")]
/// <style> .rustdoc-hidden { display: none; } </style>
pub mod doc_04b_testing_widget {}

#[doc = include_str!("./05_pass_system.md")]
/// <style> .rustdoc-hidden { display: none; } </style>
pub mod doc_05_pass_system {}
Expand Down
9 changes: 5 additions & 4 deletions masonry/src/properties/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,19 @@
reason = "A lot of properties and especially their fields are self-explanatory."
)]

use vello::peniko::Brush;
use vello::peniko::color::{AlphaColor, Srgb};

use crate::core::{MutateCtx, WidgetProperty};

// TODO - Split out into files.

/// The background color of a widget.
pub struct BackgroundBrush {
pub brush: Brush,
#[derive(Clone, Copy, Debug)]
pub struct BackgroundColor {
pub color: AlphaColor<Srgb>,
}

impl WidgetProperty for BackgroundBrush {
impl WidgetProperty for BackgroundColor {
fn changed(ctx: &mut MutateCtx<'_>) {
ctx.request_paint_only();
}
Expand Down
27 changes: 14 additions & 13 deletions masonry/src/widgets/sized_box.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ use crate::core::{
WidgetPod,
};
use crate::kurbo::{Point, Size};
use crate::properties::BackgroundBrush;
use crate::properties::BackgroundColor;
use crate::util::stroke;

// FIXME - Improve all doc in this module ASAP.
Expand Down Expand Up @@ -548,10 +548,11 @@ impl Widget for SizedBox {

// TODO - Handle properties more gracefully.
// This is more of a proof of concept.
let background = self
.background
.as_ref()
.or_else(|| props.get::<BackgroundBrush>().map(|color| &color.brush));
let background = self.background.clone().or_else(|| {
props
.get::<BackgroundColor>()
.map(|background| background.color.into())
});

if let Some(background) = background {
let panel = ctx.size().to_rounded_rect(corner_radius);
Expand All @@ -560,7 +561,7 @@ impl Widget for SizedBox {
scene.fill(
Fill::NonZero,
Affine::IDENTITY,
background,
&background,
Some(Affine::IDENTITY),
&panel,
);
Expand Down Expand Up @@ -765,24 +766,24 @@ mod tests {
let mut harness = TestHarness::create(widget);

harness.edit_root_widget(|mut sized_box| {
let brush = BackgroundBrush {
brush: palette::css::RED.into(),
let brush = BackgroundColor {
color: palette::css::RED.into(),
};
sized_box.insert_prop(brush);
});
assert_render_snapshot!(harness, "background_brush_red");

harness.edit_root_widget(|mut sized_box| {
let brush = BackgroundBrush {
brush: palette::css::GREEN.into(),
let brush = BackgroundColor {
color: palette::css::GREEN.into(),
};
*sized_box.get_prop_mut().unwrap() = brush;
});
assert_render_snapshot!(harness, "background_brush_green");

harness.edit_root_widget(|mut sized_box| {
let brush = BackgroundBrush {
brush: palette::css::BLUE.into(),
let brush = BackgroundColor {
color: palette::css::BLUE.into(),
};
sized_box.prop_entry().and_modify(|entry| {
*entry = brush;
Expand All @@ -791,7 +792,7 @@ mod tests {
assert_render_snapshot!(harness, "background_brush_blue");

harness.edit_root_widget(|mut sized_box| {
sized_box.remove_prop::<BackgroundBrush>();
sized_box.remove_prop::<BackgroundColor>();
});
assert_render_snapshot!(harness, "background_brush_removed");
}
Expand Down

0 comments on commit f7c1d94

Please sign in to comment.