Skip to content
Closed
Show file tree
Hide file tree
Changes from 30 commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
3967c6e
add canvas widget
richard-uk1 Feb 24, 2025
e30fcfc
impl suggestions and add xilem view for canvas
richard-uk1 Feb 27, 2025
b4dfea6
Apply suggestions from code review
richard-uk1 Mar 10, 2025
ec5c653
implement suggestions
richard-uk1 Mar 10, 2025
033e5bd
fix after rebase
richard-uk1 Mar 10, 2025
b63a2c8
fix fmt
richard-uk1 Mar 10, 2025
39e178b
Merge branch 'canvas_widget'
Oct 30, 2025
81bd724
Removed smallvec reliancy
philocalyst Oct 31, 2025
0c20108
fixed signature, replacing queryctx with widgetid
philocalyst Oct 31, 2025
80f5026
Made explicit hidden lifetimes
philocalyst Oct 31, 2025
e01277d
fixed test harness import
philocalyst Oct 31, 2025
e33f0d2
Fixed the harness running method, by pulling in props
philocalyst Oct 31, 2025
0141e2f
Added the associated type Action
philocalyst Oct 31, 2025
085b383
removed needless imports
philocalyst Oct 31, 2025
68fc4a8
Fixed function signatures
philocalyst Oct 31, 2025
787b131
Changed name from new pod to create pod
philocalyst Oct 31, 2025
a2d1136
Removed unused imports and parameters
philocalyst Oct 31, 2025
bf710e5
Documentation for the canvas accessibility methods
philocalyst Oct 31, 2025
1af0887
Properly reexported checkbox and buttonpress for access in xilem view
philocalyst Oct 31, 2025
32bc30b
removed needless qualification
philocalyst Oct 31, 2025
964355d
fixed test snapshot name
philocalyst Oct 31, 2025
05e8eef
Renamed hello.new to hello_new
philocalyst Oct 31, 2025
54fef9d
Fixed the harness name (whoops)
philocalyst Oct 31, 2025
a3e9942
Removed unused files
philocalyst Nov 1, 2025
6b06356
Removed unwanted debug snapshot test
philocalyst Nov 1, 2025
7485faa
Renamed the testing snapshot to match test name
philocalyst Nov 3, 2025
a3e4b29
Expanded the documentation in public-facing canvas api
philocalyst Nov 3, 2025
3013312
Removed needless image
philocalyst Nov 3, 2025
4d6c88e
Fixed the example and enabled as required doc test
philocalyst Nov 3, 2025
2c13fab
Added alt text support on the "build" method
philocalyst Nov 3, 2025
0020cee
Update masonry/src/widgets/canvas.rs
philocalyst Nov 12, 2025
38b11e7
Moved associated top to the beginning of impl
philocalyst Nov 12, 2025
98c43dc
Fixed the fucntion signature of canvas
philocalyst Nov 12, 2025
07cd12d
Using vello through xilem
philocalyst Nov 12, 2025
ab4e1f7
Modified signatures and calls to effectively handle Canvas during reb…
philocalyst Nov 12, 2025
5c40096
Merge branch 'main' into canvas_widget_again
philocalyst Nov 12, 2025
d52a528
add canvas widget
richard-uk1 Feb 24, 2025
989d2cb
Reorder reexports in xilem crates (#1459)
PoignardAzur Nov 7, 2025
0199af7
add canvas widget
richard-uk1 Feb 24, 2025
26310d8
Reorder reexports in xilem crates (#1459)
PoignardAzur Nov 7, 2025
cc85d51
Fixed view location mismatch
philocalyst Nov 27, 2025
f2cc252
Rebase
philocalyst Nov 28, 2025
8b33331
Fixed exports
philocalyst Nov 28, 2025
c0dc096
Renamed to messagectx
philocalyst Nov 28, 2025
95fdb3d
Fixed clippy lint
philocalyst Nov 28, 2025
4419765
Updated signatures
philocalyst Nov 28, 2025
86a78a3
Fixed example
philocalyst Nov 28, 2025
2966586
Incorporated suggestions for type-checked return value
philocalyst Nov 28, 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/simple_canvas.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
217 changes: 217 additions & 0 deletions masonry/src/widgets/canvas.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
// 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,
};

/// A widget allowing custom drawing.
pub struct Canvas {
draw: Arc<dyn Fn(&mut Scene, Size) + Send + Sync + 'static>,
alt_text: Option<String>,
}

// --- 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 be used to communicate the meaning of the canvas to
/// those using 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 or too hard to describe through text, users should set alt text to `""`.
Copy link
Member

Choose a reason for hiding this comment

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

Again, the same comment applies as in #875, namely:

Suggested change
/// If the canvas is decorative or too hard to describe through text, users should set alt text to `""`.
/// 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
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
/// Update the draw function
/// Update the draw function.

And the same for the other items. We want all doc comments to end with a full stop, for consistency.
Unfortunately, this currently isn't linted for.

pub fn set_painter(
this: 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(
mut this: WidgetMut<'_, Self>,
draw: Arc<dyn Fn(&mut Scene, Size) + Send + Sync + 'static>,
) {
this.widget.draw = draw;
this.ctx.request_render();
}

/// Set the alternative text for this widget
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 existing alternative text on this widget (If there is any)
pub fn remove_alt_text(mut this: WidgetMut<'_, Self>) {
this.widget.alt_text = None;
this.ctx.request_accessibility_update();
}
}

// --- MARK: IMPL WIDGET ---
impl Widget for Canvas {
fn on_pointer_event(
&mut self,
_ctx: &mut EventCtx<'_>,
_props: &mut PropertiesMut<'_>,
_event: &PointerEvent,
) {
}

fn accepts_pointer_interaction(&self) -> bool {
true
Copy link
Member

Choose a reason for hiding this comment

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

Previous discussion at #875 (comment).

The only case this really impacts is using a canvas within a button; see for example #1429. I think that in most cases, we would want this to be false, as was my position in that original thread. But fixing this can be a follow-up; we certainly don't want to block on this.

That is concretely, please leave this as-is!

}

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 {
// use as much space as possible - caller can size it as necessary
bc.max()
}

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()
}

type Action
= NoAction
where
Self: Sized;
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
type Action
= NoAction
where
Self: Sized;
type Action = NoAction;

For consistency with the rest of the widgets, I'd probably also move this to the start of this implementation. But that isn't blocking.

}

// --- 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, "simple_canvas");
}
}
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 @@ -27,6 +28,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::core::WidgetMut;
use masonry::widgets;
use vello::Scene;
use vello::kurbo::Size;
use xilem_core::MessageContext;

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

/// Creates a non-interactive drawing surface.
///
/// The `canvas` function provides a way to render custom graphics using a
/// user-supplied drawing callback.
///
/// # Example
///
/// ```
/// use xilem::view::canvas;
/// use vello::{
Copy link
Member

Choose a reason for hiding this comment

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

More of a nit, and I see reasons for using vello directly, my suggestion of using vello via xilem was intentional, as it's for one not necessary to have vello as direct dependency, and it avoids possible version mismatch.

Copy link
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 see it re-exported anywhere though... Should I do that?

Copy link
Member

Choose a reason for hiding this comment

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

Check out my original suggestion (which I actually tested locally, and includes the suggestion below as well)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Now included :) Apologies, I should have integrated the suggestion at the time as I quickly forgot...

/// kurbo::{Affine, Rect},
/// peniko::{Color, Fill},
/// Scene,
/// };
///
/// let my_canvas = canvas(|scene: &mut Scene, size| {
Copy link
Member

Choose a reason for hiding this comment

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

Same here, wrapping this in a function (even if not visible in the doc for the user) returning impl WidgetView has the advantage of checking whether this is actually a view (i.e. test on the type-checking-level).

/// // Define a rectangle that fills the entire canvas.
/// let rect = Rect::new(0.0, 0.0, size.width, size.height);
///
/// // Fill the rectangle with a solid color.
/// scene.fill(
/// Fill::NonZero,
/// Affine::IDENTITY,
/// &Color::from_rgb8(51, 102, 204),
/// None,
/// &rect,
/// );
/// });
/// ```
pub fn canvas(draw: impl Fn(&mut Scene, Size) + Send + Sync + 'static) -> Canvas {
Copy link
Member

Choose a reason for hiding this comment

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

The same comment applies as in #875, that this signature is wrong.
This signature can only be:

Suggested change
pub fn canvas(draw: impl Fn(&mut Scene, Size) + Send + Sync + 'static) -> Canvas {
pub fn canvas(draw: Arc<dyn Fn(&mut Scene, Size) + Send + Sync + 'static>) -> Canvas
// or
pub fn canvas(draw: &Arc<dyn Fn(&mut Scene, Size) + Send + Sync + 'static>) -> Canvas

Because we want this to allow not resetting the draw function every rebuild. Currently the !Arc::ptr_eq line will always return true (i.e. that the items are not equal) because a new Arc is allocated in each call to canvas.

Canvas {
draw: Arc::new(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, Action> View<State, Action, ViewCtx> for Canvas {
type Element = Pod<widgets::Canvas>;
type ViewState = ();

fn build(&self, ctx: &mut ViewCtx, _state: &mut 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,
element: Mut<'_, Self::Element>,
_state: &mut State,
) {
if !Arc::ptr_eq(&self.draw, &prev.draw) {
widgets::Canvas::set_painter_arc(element, self.draw.clone());
}
}

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

fn message(
&self,
(): &mut Self::ViewState,
_ctx: &mut MessageContext,
_widget: WidgetMut<'_, widgets::Canvas>,
_app_state: &mut State,
) -> MessageResult<Action> {
tracing::error!(
"Message arrived in Canvas::message, but Canvas doesn't consume any messages, this is a bug"
);
MessageResult::Stale
}
}
3 changes: 3 additions & 0 deletions xilem/src/view/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ pub use worker::*;
mod button;
pub use button::*;

mod canvas;
pub use canvas::*;

mod checkbox;
pub use checkbox::*;

Expand Down