Skip to content

Commit 05db616

Browse files
masonry: author paint with imaging Painter instead of vello::Scene (#1696)
Switch Masonry’s paint boundary from direct Vello scene authoring to `imaging`’s `Painter` API. Widgets now paint through `Painter<'_>` while the paint pass keeps the Vello-specific sink lifecycle private. This moves Masonry toward the intended split where `imaging` is the authoring API and Vello is the backend. Canvas now retains `imaging::record::Scene`, so canvas drawing also uses `imaging`’s retained representation at the widget boundary. As part of the migration, update the paint call sites to match the newer `imaging` ergonomics: - use direct `Painter` fills/strokes for native shapes - use `draw_image`, transformed clip helpers, and `Painter::replay` - remove redundant default-state builder calls such as `.transform(Affine::IDENTITY)` and `.fill_rule(Fill::NonZero)` - remove the old Masonry `fill`/`stroke` helper abstractions This also fixes a pre-existing radio button bug where the border circle was stroked twice.
1 parent 82d24fa commit 05db616

73 files changed

Lines changed: 877 additions & 581 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Cargo.lock

Lines changed: 24 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ tree_arena = { version = "0.2.0", path = "tree_arena" }
5353
include_doc_path = { version = "0.1.0", path = "include_doc_path", package = "linebender_include_doc_path" }
5454

5555
anymore = "1.0.0"
56+
imaging = { git = "https://github.com/forest-rs/imaging.git", rev = "850af057d4c56cf644f37aaa644c7814a7a03a4b" }
57+
imaging_vello = { git = "https://github.com/forest-rs/imaging.git", rev = "850af057d4c56cf644f37aaa644c7814a7a03a4b" }
5658
vello = { version = "0.8.0", default-features = false, features = ["wgpu"] }
5759
kurbo = "0.13.0"
5860
parley = { version = "0.7.0", features = ["accesskit"] }

masonry/examples/custom_widget.rs

Lines changed: 15 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,14 @@ use masonry::core::{
1414
NoAction, PaintCtx, PointerEvent, PropertiesMut, PropertiesRef, RegisterCtx, TextEvent, Widget,
1515
WidgetId,
1616
};
17+
use masonry::imaging::Painter;
1718
use masonry::kurbo::{Affine, Axis, BezPath, Point, Rect, Size, Stroke};
1819
use masonry::layout::LenReq;
1920
use masonry::parley::style::{FontFamily, FontStack, GenericFamily, StyleProperty};
20-
use masonry::peniko::{Color, Fill, ImageBrush, ImageFormat};
21+
use masonry::peniko::{Color, ImageBrush, ImageFormat};
2122
use masonry::peniko::{ImageAlphaType, ImageData};
2223
use masonry::properties::ObjectFit;
2324
use masonry::theme::default_property_set;
24-
use masonry::vello::Scene;
2525
use masonry::{TextAlign, TextAlignOptions, palette};
2626
use masonry_winit::app::{AppDriver, DriverCtx, NewWindow, WindowId};
2727
use masonry_winit::winit::window::Window;
@@ -105,18 +105,17 @@ impl Widget for CustomWidget {
105105
// The paint method gets called last, after an event flow.
106106
// It goes event -> update -> layout -> paint, and each method can influence the next.
107107
// Basically, anything that changes the appearance of a widget causes a paint.
108-
fn paint(&mut self, ctx: &mut PaintCtx<'_>, _props: &PropertiesRef<'_>, scene: &mut Scene) {
108+
fn paint(
109+
&mut self,
110+
ctx: &mut PaintCtx<'_>,
111+
_props: &PropertiesRef<'_>,
112+
painter: &mut Painter<'_>,
113+
) {
109114
// Clear the whole widget with the color of your choice
110115
// (ctx.content_box_size() returns the size of the content rect we're painting in)
111116
let size = ctx.content_box_size();
112117
let rect = ctx.content_box();
113-
scene.fill(
114-
Fill::NonZero,
115-
Affine::IDENTITY,
116-
palette::css::WHITE,
117-
None,
118-
&rect,
119-
);
118+
painter.fill(rect, palette::css::WHITE).draw();
120119

121120
// Create an arbitrary bezier path
122121
let mut path = BezPath::new();
@@ -125,19 +124,15 @@ impl Widget for CustomWidget {
125124
// Create a color
126125
let stroke_color = Color::from_rgb8(0, 128, 0);
127126
// Stroke the path with thickness 5.0
128-
scene.stroke(
129-
&Stroke::new(5.0),
130-
Affine::IDENTITY,
131-
stroke_color,
132-
None,
133-
&path,
134-
);
127+
painter
128+
.stroke(&path, &Stroke::new(5.0), stroke_color)
129+
.draw();
135130

136131
// Rectangles: the path for practical people
137132
let rect = Rect::from_origin_size((10.0, 10.0), (100.0, 100.0));
138133
// Note the Color:from_rgba8 which includes an alpha channel (7F in this case)
139134
let fill_color = Color::from_rgba8(0x00, 0x00, 0x00, 0x7F);
140-
scene.fill(Fill::NonZero, Affine::IDENTITY, fill_color, None, &rect);
135+
painter.fill(rect, fill_color).draw();
141136

142137
// To render text, we first create a text layout builder and then set the text properties.
143138
let (fcx, lcx) = ctx.text_contexts();
@@ -154,7 +149,7 @@ impl Widget for CustomWidget {
154149

155150
// We can pass a transform matrix to rotate the text we render
156151
masonry::core::render_text(
157-
scene,
152+
painter,
158153
Affine::rotate(std::f64::consts::FRAC_PI_4).then_translate((80.0, 40.0).into()),
159154
&text_layout,
160155
&[fill_color.into()],
@@ -171,7 +166,7 @@ impl Widget for CustomWidget {
171166
height: 256,
172167
});
173168
let transform = ObjectFit::Stretch.affine(size, Size::new(256., 256.));
174-
scene.draw_image(&image_data, transform);
169+
painter.draw_image(&image_data, transform);
175170
}
176171

177172
fn accessibility_role(&self) -> Role {

masonry/examples/layers.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ use masonry::core::{
1212
NoAction, PaintCtx, PointerEvent, PointerUpdate, PropertiesMut, PropertiesRef, PropertySet,
1313
RegisterCtx, StyleProperty, Update, UpdateCtx, Widget, WidgetId, WidgetPod,
1414
};
15+
use masonry::imaging::Painter;
1516
use masonry::kurbo::{Axis, Point, Size, Vec2};
1617
use masonry::layers::Tooltip;
1718
use masonry::layout::{AsUnit, LayoutSize, LenReq, SizeDef};
@@ -20,7 +21,6 @@ use masonry::peniko::Color;
2021
use masonry::properties::{Background, BorderColor, BorderWidth, ContentColor};
2122
use masonry::theme::default_property_set;
2223
use masonry::util::{Duration, Instant};
23-
use masonry::vello::Scene;
2424
use masonry::widgets::{Flex, Label, Selector};
2525
use masonry_winit::app::{AppDriver, DriverCtx, NewWindow, WindowId};
2626
use masonry_winit::winit::window::Window;
@@ -137,7 +137,13 @@ impl Widget for OverlayBox {
137137
ctx.place_child(&mut self.child, Point::ORIGIN);
138138
}
139139

140-
fn paint(&mut self, _ctx: &mut PaintCtx<'_>, _props: &PropertiesRef<'_>, _scene: &mut Scene) {}
140+
fn paint(
141+
&mut self,
142+
_ctx: &mut PaintCtx<'_>,
143+
_props: &PropertiesRef<'_>,
144+
_painter: &mut Painter<'_>,
145+
) {
146+
}
141147

142148
fn accessibility_role(&self) -> Role {
143149
Role::GenericContainer

masonry/src/doc/color_rectangle.rs

Lines changed: 24 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,7 @@ use masonry::layout::LenReq;
2929
// ---
3030
use masonry::accesskit::{Node, Role};
3131
use masonry::core::{AccessCtx, PaintCtx};
32-
use masonry::kurbo::Affine;
33-
use masonry::peniko::Fill;
34-
use masonry::vello::Scene;
32+
use masonry::imaging::Painter;
3533
// ---
3634
use masonry::core::WidgetId;
3735
use tracing::{Span, trace_span};
@@ -152,15 +150,14 @@ impl Widget for ColorRectangle {
152150
// ---
153151

154152
#[cfg(false)] // We show two `paint` implementations; check that both parse.
155-
fn paint(&mut self, ctx: &mut PaintCtx<'_>, _props: &PropertiesRef<'_>, scene: &mut Scene) {
153+
fn paint(
154+
&mut self,
155+
ctx: &mut PaintCtx<'_>,
156+
_props: &PropertiesRef<'_>,
157+
painter: &mut Painter<'_>,
158+
) {
156159
let rect = ctx.size().to_rect();
157-
scene.fill(
158-
Fill::NonZero,
159-
Affine::IDENTITY,
160-
self.color,
161-
Some(Affine::IDENTITY),
162-
&rect,
163-
);
160+
painter.fill(rect, self.color).draw();
164161
}
165162

166163
fn accessibility_role(&self) -> Role {
@@ -194,20 +191,19 @@ impl Widget for ColorRectangle {
194191
// Second implementation from "Creating a new widget" tutorial.
195192
// We use these methods in the trait, so that hovering is detected in our unit tests.
196193

197-
fn paint(&mut self, ctx: &mut PaintCtx<'_>, _props: &PropertiesRef<'_>, scene: &mut Scene) {
194+
fn paint(
195+
&mut self,
196+
ctx: &mut PaintCtx<'_>,
197+
_props: &PropertiesRef<'_>,
198+
painter: &mut Painter<'_>,
199+
) {
198200
let rect = ctx.content_box();
199201
let color = if ctx.is_hovered() {
200202
Color::WHITE
201203
} else {
202204
self.color
203205
};
204-
scene.fill(
205-
Fill::NonZero,
206-
Affine::IDENTITY,
207-
color,
208-
Some(Affine::IDENTITY),
209-
&rect,
210-
);
206+
painter.fill(rect, color).draw();
211207
}
212208

213209
fn update(&mut self, ctx: &mut UpdateCtx<'_>, _props: &mut PropertiesMut<'_>, event: &Update) {
@@ -225,16 +221,17 @@ impl Widget for ColorRectangle {
225221
// Implementation from "Reading widget properties" tutorial.
226222
#[expect(dead_code, reason = "example code")]
227223
impl ColorRectangle {
228-
fn paint(&mut self, ctx: &mut PaintCtx<'_>, props: &PropertiesRef<'_>, scene: &mut Scene) {
224+
fn paint(
225+
&mut self,
226+
ctx: &mut PaintCtx<'_>,
227+
props: &PropertiesRef<'_>,
228+
painter: &mut Painter<'_>,
229+
) {
229230
let background = props.get::<Background>();
230231
let rect = ctx.content_box();
231-
scene.fill(
232-
Fill::NonZero,
233-
Affine::IDENTITY,
234-
&background.get_peniko_brush_for_rect(rect),
235-
Some(Affine::IDENTITY),
236-
&rect,
237-
);
232+
painter
233+
.fill(rect, &background.get_peniko_brush_for_rect(rect))
234+
.draw();
238235
}
239236
}
240237

masonry/src/doc/implementing_container_widget.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -278,7 +278,7 @@ use masonry::core::{
278278
AccessCtx, AccessEvent, EventCtx, NoAction, PaintCtx, PointerEvent, PropertiesRef, TextEvent,
279279
Update, UpdateCtx,
280280
};
281-
use masonry::vello::Scene;
281+
use masonry::imaging::Painter;
282282
283283
impl Widget for VerticalStack {
284284
type Action = NoAction;
@@ -292,7 +292,13 @@ impl Widget for VerticalStack {
292292
293293
// ...
294294
295-
fn paint(&mut self, _ctx: &mut PaintCtx<'_>, _props: &PropertiesRef<'_>, _scene: &mut Scene) {}
295+
fn paint(
296+
&mut self,
297+
_ctx: &mut PaintCtx<'_>,
298+
_props: &PropertiesRef<'_>,
299+
_painter: &mut Painter<'_>,
300+
) {
301+
}
296302
297303
fn accessibility_role(&self) -> Role {
298304
Role::GenericContainer

masonry/src/doc/implementing_widget.md

Lines changed: 18 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ trait Widget {
3737
fn measure(&mut self, ctx: &mut MeasureCtx<'_>, props: &PropertiesRef<'_>, axis: Axis, len_req: LenReq, cross_length: Option<f64>) -> f64;
3838
fn layout(&mut self, ctx: &mut LayoutCtx<'_>, props: &PropertiesRef<'_>, size: Size);
3939
40-
fn paint(&mut self, ctx: &mut PaintCtx<'_>, props: &PropertiesRef<'_>, scene: &mut Scene);
40+
fn paint(&mut self, ctx: &mut PaintCtx<'_>, props: &PropertiesRef<'_>, painter: &mut Painter<'_>);
4141
fn accessibility_role(&self) -> Role;
4242
fn accessibility(&mut self, ctx: &mut AccessCtx<'_>, props: &PropertiesRef<'_>, node: &mut Node);
4343
@@ -217,23 +217,20 @@ Next we write our render methods:
217217
// ...
218218
use masonry::accesskit::{Node, Role};
219219
use masonry::core::{AccessCtx, PaintCtx, PropertiesRef};
220-
use masonry::kurbo::Affine;
221-
use masonry::peniko::Fill;
222-
use masonry::vello::Scene;
220+
use masonry::imaging::Painter;
223221
// ...
224222
225223
impl Widget for ColorRectangle {
226224
// ...
227225
228-
fn paint(&mut self, ctx: &mut PaintCtx<'_>, _props: &PropertiesRef<'_>, scene: &mut Scene) {
226+
fn paint(
227+
&mut self,
228+
ctx: &mut PaintCtx<'_>,
229+
_props: &PropertiesRef<'_>,
230+
painter: &mut Painter<'_>,
231+
) {
229232
let rect = ctx.content_box();
230-
scene.fill(
231-
Fill::NonZero,
232-
Affine::IDENTITY,
233-
self.color,
234-
Some(Affine::IDENTITY),
235-
&rect,
236-
);
233+
painter.fill(rect, self.color).draw();
237234
}
238235
239236
fn accessibility_role(&self) -> Role {
@@ -253,7 +250,7 @@ impl Widget for ColorRectangle {
253250
}
254251
```
255252

256-
In our `paint` method, we're given a [`vello::Scene`] and paint a rectangle into it.
253+
In our `paint` method, we're given a [`Painter`](crate::imaging::Painter) and paint a rectangle into it.
257254

258255
We use `ctx.content_box()` to get a rectangle that precisely covers the content area of our widget.
259256

@@ -368,20 +365,19 @@ First, we update our paint method:
368365
impl Widget for ColorRectangle {
369366
// ...
370367
371-
fn paint(&mut self, ctx: &mut PaintCtx<'_>, _props: &PropertiesRef<'_>, scene: &mut Scene) {
368+
fn paint(
369+
&mut self,
370+
ctx: &mut PaintCtx<'_>,
371+
_props: &PropertiesRef<'_>,
372+
painter: &mut Painter<'_>,
373+
) {
372374
let rect = ctx.content_box();
373375
let color = if ctx.is_hovered() {
374376
Color::WHITE
375377
} else {
376378
self.color
377379
};
378-
scene.fill(
379-
Fill::NonZero,
380-
Affine::IDENTITY,
381-
color,
382-
Some(Affine::IDENTITY),
383-
&rect,
384-
);
380+
painter.fill(rect, color).draw();
385381
}
386382
387383
// ...
@@ -483,7 +479,7 @@ The next one is about creating a container widgets, and the complications it add
483479
[`Widget`]: crate::core::Widget
484480
[`WidgetMut`]: crate::core::WidgetMut
485481
[`PaintCtx::content_box()`]: crate::core::PaintCtx::content_box
486-
[`vello::Scene`]: vello::Scene
482+
[`Painter`]: crate::imaging::Painter
487483
[`Role::Button`]: accesskit::Role::Button
488484
[`RenderRoot::edit_base_layer()`]: crate::app::RenderRoot::edit_base_layer
489485
[`RenderRoot::edit_layer()`]: crate::app::RenderRoot::edit_layer

0 commit comments

Comments
 (0)