Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
43856af
add canvas widget
richard-uk1 Feb 24, 2025
1baa22e
impl suggestions and add xilem view for canvas
richard-uk1 Feb 27, 2025
0d43e8a
Apply suggestions from code review
richard-uk1 Mar 10, 2025
df55e68
implement suggestions
richard-uk1 Mar 10, 2025
3e973d2
fix after rebase
richard-uk1 Mar 10, 2025
43a6cfd
fix fmt
richard-uk1 Mar 10, 2025
2a0f296
Removed smallvec reliancy
philocalyst Oct 31, 2025
afd330a
fixed signature, replacing queryctx with widgetid
philocalyst Oct 31, 2025
a30deae
Made explicit hidden lifetimes
philocalyst Oct 31, 2025
62ca993
fixed test harness import
philocalyst Oct 31, 2025
2e03fad
Fixed the harness running method, by pulling in props
philocalyst Oct 31, 2025
2f8957f
Added the associated type Action
philocalyst Oct 31, 2025
053ff93
Fixed function signatures
philocalyst Oct 31, 2025
aaa629d
Changed name from new pod to create pod
philocalyst Oct 31, 2025
07f2eb3
Removed unused imports and parameters
philocalyst Oct 31, 2025
593cf85
Documentation for the canvas accessibility methods
philocalyst Oct 31, 2025
8e36ac6
removed needless qualification
philocalyst Oct 31, 2025
0d2dedc
fixed test snapshot name
philocalyst Oct 31, 2025
288d062
Renamed hello.new to hello_new
philocalyst Oct 31, 2025
2af781d
Fixed the harness name (whoops)
philocalyst Oct 31, 2025
47300c5
Removed unused files
philocalyst Nov 1, 2025
ed36469
Removed unwanted debug snapshot test
philocalyst Nov 1, 2025
7d1f65e
Renamed the testing snapshot to match test name
philocalyst Nov 3, 2025
4891caf
Expanded the documentation in public-facing canvas api
philocalyst Nov 3, 2025
bd83e3b
Removed needless image
philocalyst Nov 3, 2025
3b3cf61
Fixed the example and enabled as required doc test
philocalyst Nov 3, 2025
d162f63
Added alt text support on the "build" method
philocalyst Nov 3, 2025
1e722dd
Update masonry/src/widgets/canvas.rs
philocalyst Nov 12, 2025
ffdd9cf
Moved associated top to the beginning of impl
philocalyst Nov 12, 2025
e5ce6ae
Fixed the fucntion signature of canvas
philocalyst Nov 12, 2025
ce63927
Using vello through xilem
philocalyst Nov 12, 2025
b65db9e
Modified signatures and calls to effectively handle Canvas during reb…
philocalyst Nov 12, 2025
d5da439
add canvas widget
richard-uk1 Feb 24, 2025
a6f35cd
Fixed view location mismatch
philocalyst Nov 27, 2025
2e045f1
Renamed to messagectx
philocalyst Nov 28, 2025
5310240
Fixed clippy lint
philocalyst Nov 28, 2025
b3f677a
Updated signatures
philocalyst Nov 28, 2025
21ee038
Fixed example
philocalyst Nov 28, 2025
82e54b6
Incorporated suggestions for type-checked return value
philocalyst Nov 28, 2025
554c799
Fix imports
PoignardAzur Nov 30, 2025
d3a35b9
Fix up screenshot files
PoignardAzur Nov 30, 2025
f26556f
Remove extra annotation
PoignardAzur Nov 30, 2025
af646b4
Fix vscode markers
PoignardAzur Nov 30, 2025
403ecd7
Implement clipping
PoignardAzur Nov 30, 2025
404cdd2
Add comment
PoignardAzur Nov 30, 2025
c61251e
Update doc comments
PoignardAzur Nov 30, 2025
c56864c
Fix doc test
PoignardAzur Nov 30, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added masonry/screenshots/canvas_simple.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
231 changes: 231 additions & 0 deletions masonry/src/widgets/canvas.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
// Copyright 2025 the Xilem Authors and the Druid Authors
// SPDX-License-Identifier: Apache-2.0

//! A canvas widget.

use std::sync::Arc;

use accesskit::{Node, Role};
use masonry_core::core::{ChildrenIds, NoAction};
use tracing::{Span, trace_span};
use vello::Scene;
use vello::kurbo::Size;

use crate::core::{
AccessCtx, AccessEvent, BoxConstraints, EventCtx, LayoutCtx, PaintCtx, PointerEvent,
PropertiesMut, PropertiesRef, RegisterCtx, TextEvent, Update, UpdateCtx, Widget, WidgetId,
WidgetMut,
};

// TODO - Add background color?
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is better done in the draw function?

Copy link
Copy Markdown
Member

@Philipp-M Philipp-M Dec 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I would just remove TODO, I think it's a good default to draw nothing when nothing is specified in draw.


/// A widget allowing custom drawing.
///
/// A canvas takes a painter callback; every time the canvas is repainted, that callback
/// in run with a [`Scene`].
/// That Scene is then used as the canvas' contents.
pub struct Canvas {
draw: Arc<dyn Fn(&mut Scene, Size) + Send + Sync + 'static>,
alt_text: Option<String>,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't hurt to have an Option here, but it's not necessary, we can just use the property is_empty for that.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The thing is, "alt text is unset" and "alt text is set to empty string" have different semantic meanings, though I'm not sure whether screen readers really interpret them that differently in practice.

}

// --- MARK: BUILDERS
impl Canvas {
/// Create a new canvas with the given draw function.
pub fn new(draw: impl Fn(&mut Scene, Size) + Send + Sync + 'static) -> Self {
Self::from_arc(Arc::new(draw))
}

/// Create a new canvas from a function already contained in an [`Arc`].
pub fn from_arc(draw: Arc<dyn Fn(&mut Scene, Size) + Send + Sync + 'static>) -> Self {
Self {
draw,
alt_text: None,
}
}

/// Set the text that will describe the canvas to screen readers.
///
/// Users are encouraged to set alt text for the canvas.
/// If possible, the alt-text should succinctly describe what the canvas represents.
///
/// If the canvas is decorative users should set alt text to `""`.
/// If it's too hard to describe through text, the alt text should be left unset.
/// This allows accessibility clients to know that there is no accessible description of the canvas content.
pub fn with_alt_text(mut self, alt_text: impl Into<String>) -> Self {
self.alt_text = Some(alt_text.into());
self
}
}

// --- MARK: WIDGETMUT
impl Canvas {
/// Update the draw function.
pub fn set_painter(
this: &mut WidgetMut<'_, Self>,
draw: impl Fn(&mut Scene, Size) + Send + Sync + 'static,
) {
Self::set_painter_arc(this, Arc::new(draw));
}

/// Update the draw function.
pub fn set_painter_arc(
this: &mut WidgetMut<'_, Self>,
draw: Arc<dyn Fn(&mut Scene, Size) + Send + Sync + 'static>,
) {
this.widget.draw = draw;
this.ctx.request_render();
Comment on lines +76 to +77
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something I'm wondering for a longer time already, is whether we should check within these setters whether really something changed (i.e. in this case if the arc pointer has changed), and only then request a render.

As we often (always?) do this in Xilem it's not necessary here for Xilem at least, but I think it might be nice to have this built into masonry?

See also https://github.com/ricvelozo/xilem/pull/1/files#diff-7d717e7fd640e50cef624a6fa2afb9c20968470e5849c8b3b0abe9782499814eR67-R72
where I did this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know. In general, that kind of reasoning is how you end up with multiple layers of caching in your app that all check whether some parameter changed.

In practice, repainting is usually cheap and having Masonry always repaint something when a parameter is set is more predictable, so I'd lean towards the current design.

}

/// Set the text that will describe the canvas to screen readers.
///
/// See [`Canvas::with_alt_text`] for details.
pub fn set_alt_text(mut this: WidgetMut<'_, Self>, alt_text: String) {
this.widget.alt_text = Some(alt_text);
this.ctx.request_accessibility_update();
}

/// Remove the canvas' alt text.
///
/// See [`Canvas::with_alt_text`] for details.
pub fn remove_alt_text(mut this: WidgetMut<'_, Self>) {
this.widget.alt_text = None;
this.ctx.request_accessibility_update();
}
Comment on lines +80 to +94
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See the comment below, I think we can just sum this up in set_alt_text with alt_text: String (or ArcStr)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we might just want to pass an Option<String> and merge them.

}

// --- MARK: IMPL WIDGET
impl Widget for Canvas {
type Action = NoAction;

fn on_pointer_event(
&mut self,
_ctx: &mut EventCtx<'_>,
_props: &mut PropertiesMut<'_>,
_event: &PointerEvent,
) {
}
Comment on lines +101 to +107
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't methods with default impls be omitted?


// TODO - Do we want the Canvas to be transparent to pointer events?
fn accepts_pointer_interaction(&self) -> bool {
true
}

fn on_text_event(
&mut self,
_ctx: &mut EventCtx<'_>,
_props: &mut PropertiesMut<'_>,
_event: &TextEvent,
) {
}

fn on_access_event(
&mut self,
_ctx: &mut EventCtx<'_>,
_props: &mut PropertiesMut<'_>,
_event: &AccessEvent,
) {
}

fn register_children(&mut self, _ctx: &mut RegisterCtx<'_>) {}

fn update(
&mut self,
_ctx: &mut UpdateCtx<'_>,
_props: &mut PropertiesMut<'_>,
_event: &Update,
) {
}

fn layout(
&mut self,
ctx: &mut LayoutCtx<'_>,
_props: &mut PropertiesMut<'_>,
bc: &BoxConstraints,
) -> Size {
// We use all the available space as possible.
let size = bc.max();

// We clip the contents we draw.
ctx.set_clip_path(size.to_rect());

size
}

fn paint(&mut self, ctx: &mut PaintCtx<'_>, _props: &PropertiesRef<'_>, scene: &mut Scene) {
(self.draw)(scene, ctx.size());
}

fn accessibility_role(&self) -> Role {
Role::Canvas
}

fn accessibility(
&mut self,
_ctx: &mut AccessCtx<'_>,
_props: &PropertiesRef<'_>,
node: &mut Node,
) {
if let Some(text) = &self.alt_text {
node.set_description(text.clone());
}
}

fn children_ids(&self) -> ChildrenIds {
ChildrenIds::new()
}

fn make_trace_span(&self, widget_id: WidgetId) -> Span {
trace_span!("Canvas", id = widget_id.trace())
}

fn get_debug_text(&self) -> Option<String> {
self.alt_text.clone()
}
}

// --- MARK: TESTS
#[cfg(test)]
mod tests {
use masonry_core::core::{DefaultProperties, Properties};
use masonry_testing::assert_render_snapshot;
use vello::kurbo::{Affine, BezPath, Stroke};
use vello::peniko::{Color, Fill};

use super::*;
use crate::testing::TestHarness;

#[test]
fn simple_canvas() {
let canvas = Canvas::new(|scene, size| {
let scale = Affine::scale_non_uniform(size.width, size.height);
let mut path = BezPath::new();
path.move_to((0.1, 0.1));
path.line_to((0.9, 0.9));
path.line_to((0.9, 0.1));
path.close_path();
path = scale * path;
scene.fill(
Fill::NonZero,
Affine::IDENTITY,
Color::from_rgb8(100, 240, 150),
None,
&path,
);
scene.stroke(
&Stroke::new(4.),
Affine::IDENTITY,
Color::from_rgb8(200, 140, 50),
None,
&path,
);
});

let mut harness = TestHarness::create(
DefaultProperties::default(),
canvas.with_props(Properties::default()),
);

assert_render_snapshot!(harness, "canvas_simple");
}
}
2 changes: 2 additions & 0 deletions masonry/src/widgets/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

mod align;
mod button;
mod canvas;
mod checkbox;
mod flex;
mod grid;
Expand All @@ -29,6 +30,7 @@ mod zstack;

pub use self::align::Align;
pub use self::button::{Button, ButtonPress};
pub use self::canvas::Canvas;
pub use self::checkbox::{Checkbox, CheckboxToggled};
pub use self::flex::{Flex, FlexParams};
pub use self::grid::{Grid, GridParams};
Expand Down
112 changes: 112 additions & 0 deletions xilem/src/view/canvas.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
// Copyright 2024 the Xilem Authors
// SPDX-License-Identifier: Apache-2.0

use std::sync::Arc;

use masonry::widgets;
use vello::Scene;
use vello::kurbo::Size;

use crate::core::{Arg, MessageCtx, MessageResult, Mut, View, ViewArgument, ViewMarker};
use crate::{Pod, ViewCtx};

/// # Example
///
/// ```
/// use xilem::masonry::kurbo::{Rect, Size, Affine};
/// use xilem::masonry::peniko::Fill;
/// use xilem::masonry::vello::Scene;
/// use xilem::view::canvas;
/// use std::sync::Arc;
/// # use xilem::WidgetView;
///
/// # fn fill_canvas() -> impl WidgetView<()> + use<> {
/// let my_canvas = canvas(Arc::new(|scene: &mut Scene, size: Size| {
/// // Drawing a simple rectangle that fills the canvas.
/// scene.fill(
/// Fill::NonZero,
/// Affine::IDENTITY,
/// xilem::palette::css::AQUA,
/// None,
/// &Rect::new(0.0, 0.0, size.width, size.height),
/// );
/// }));
/// # my_canvas
/// # }
/// ```
pub fn canvas(draw: Arc<dyn Fn(&mut Scene, Size) + Send + Sync + 'static>) -> Canvas {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Btw. I think we should probably specify in the doc-comment, that this Arc<dyn Fn> should be ideally saved within the app state, otherwise it always results in a rerender. Ideally show how this is done better in the doc-comment example

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we can also use a function pointer instead, to avoid allocs and (mostly) unnecessary generics?

Canvas {
draw,
alt_text: None,
}
}

/// The [`View`] created by [`canvas`].
#[must_use = "View values do nothing unless provided to Xilem."]
pub struct Canvas {
draw: Arc<dyn Fn(&mut Scene, Size) + Send + Sync + 'static>,
alt_text: Option<String>,
}

impl Canvas {
/// Sets alt text for the contents of the canvas.
///
/// Users are strongly encouraged to provide alt text for accessibility tools
/// to use.
pub fn alt_text(mut self, alt_text: String) -> Self {
self.alt_text = Some(alt_text);
self
}
}

impl ViewMarker for Canvas {}

impl<State: ViewArgument, Action> View<State, Action, ViewCtx> for Canvas {
type Element = Pod<widgets::Canvas>;
type ViewState = ();

fn build(&self, ctx: &mut ViewCtx, _: Arg<'_, State>) -> (Self::Element, Self::ViewState) {
let mut widget = widgets::Canvas::from_arc(self.draw.clone());

if let Some(alt_text) = &self.alt_text {
widget = widget.with_alt_text(alt_text.to_owned());
}

let widget_pod = ctx.create_pod(widget);
(widget_pod, ())
}

fn rebuild(
&self,
prev: &Self,
(): &mut Self::ViewState,
_ctx: &mut ViewCtx,
mut element: Mut<'_, Self::Element>,
_: Arg<'_, State>,
) {
if !Arc::ptr_eq(&self.draw, &prev.draw) {
widgets::Canvas::set_painter_arc(&mut element, self.draw.clone());
}
if self.alt_text != prev.alt_text
&& let Some(alt_text) = &self.alt_text
{
widgets::Canvas::set_alt_text(element, alt_text.clone());
}
Comment on lines +90 to +94
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is safe?

When alt_test was Some and changed to None it won't be updated in masonry AFAICS.
So in the current state of this PR remove_alt_text needs to be called (but as described above, I think we can simplify this and the masonry code by not using an Option at all)

}

fn teardown(&self, (): &mut Self::ViewState, _: &mut ViewCtx, _: Mut<'_, Self::Element>) {}

fn message(
&self,
(): &mut Self::ViewState,
message: &mut MessageCtx,
_element: Mut<'_, Self::Element>,
_app_state: Arg<'_, State>,
) -> MessageResult<Action> {
tracing::error!(
?message,
"Message arrived in Canvas::message, but Canvas doesn't consume any messages, this is a bug"
);
MessageResult::Stale
}
}
2 changes: 2 additions & 0 deletions xilem/src/view/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
//! Views for the widgets which are built-in to Masonry. These are the primitives your Xilem app's view tree will generally be constructed from.

mod button;
mod canvas;
mod checkbox;
mod flex;
mod grid;
Expand All @@ -28,6 +29,7 @@ mod worker;
mod zstack;

pub use self::button::*;
pub use self::canvas::*;
pub use self::checkbox::*;
pub use self::flex::*;
pub use self::grid::*;
Expand Down