Skip to content

Add Canvas widget - cont.#1445

Closed
philocalyst wants to merge 48 commits intolinebender:mainfrom
philocalyst:canvas_widget_again
Closed

Add Canvas widget - cont.#1445
philocalyst wants to merge 48 commits intolinebender:mainfrom
philocalyst:canvas_widget_again

Conversation

@philocalyst
Copy link
Contributor

No description provided.

@PoignardAzur
Copy link
Contributor

To be clear: this is based on #875.

@PoignardAzur PoignardAzur changed the title Small fixes for compatability, based off of the original canvas_widget branch Add Canvas widget - contd Nov 1, 2025
@PoignardAzur PoignardAzur changed the title Add Canvas widget - contd Add Canvas widget - cont Nov 1, 2025
@PoignardAzur PoignardAzur changed the title Add Canvas widget - cont Add Canvas widget - cont. Nov 1, 2025
Copy link
Contributor

@PoignardAzur PoignardAzur left a comment

Choose a reason for hiding this comment

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

Thanks for picking up this work!

Copy link
Contributor

Choose a reason for hiding this comment

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

We almost never do these kinds of snapshot tests anymore. You should remove this file.

Comment on lines 24 to 31
/// ```ignore
/// use xilem::view::canvas;
/// use masonry::{Scene, Size};
///
/// let my_canvas = canvas(|scene: &mut Scene, size: Size| {
/// // Drawing a simple rectangle that fills the canvas.
/// scene.fill_rect((0.0, 0.0, size.width, size.height), [0.2, 0.4, 0.8, 1.0]);
/// });
Copy link
Member

Choose a reason for hiding this comment

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

I think we should strive towards not ignoring the examples in doc-comments.
This has the tendency to quickly be out of sync, I think you can already see that in the diff.

Suggested change
/// ```ignore
/// use xilem::view::canvas;
/// use masonry::{Scene, Size};
///
/// let my_canvas = canvas(|scene: &mut Scene, size: Size| {
/// // Drawing a simple rectangle that fills the canvas.
/// scene.fill_rect((0.0, 0.0, size.width, size.height), [0.2, 0.4, 0.8, 1.0]);
/// });
/// ```
/// use xilem::masonry::{kurbo::{Rect, Size}, peniko::Fill, vello::Scene};
/// use xilem::{Affine, view::canvas};
/// # use xilem::WidgetView;
///
/// # fn fill_canvas() -> impl WidgetView<()> + use<> {
/// let my_canvas = canvas(|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
/// # }

Copy link
Member

@DJMcNab DJMcNab left a comment

Choose a reason for hiding this comment

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

Thanks for reviving this! It's an important capability to have.
Thanks also for retaining Richard's commits, so this will have properly attributed authors when it lands.

Most of my comments are the same as in #875, for things which still need addressing.

Comment on lines 63 to 80
fn build(&self, ctx: &mut ViewCtx, _state: &mut State) -> (Self::Element, Self::ViewState) {
let widget = widgets::Canvas::from_arc(self.draw.clone());

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());
}
}
Copy link
Member

Choose a reason for hiding this comment

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

There are two answers here.

Firstly, the WidgetMut methods on the Canvas widget should be accepting &mut WidgetMut<Self>, rather than just plain WidgetMut. For example, you can look in:

// --- MARK: WIDGETMUT
impl Button {
/// Get a mutable reference to the label.
pub fn child_mut<'t>(this: &'t mut WidgetMut<'_, Self>) -> WidgetMut<'t, dyn Widget> {
this.ctx.get_mut(&mut this.widget.child)
}
}

That would solve this issue.

However, there is also a trick for when you do need to fork a WidgetMut. That's the reborrow_mut method.

/// 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.


// --- 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.

}

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!

Comment on lines 167 to 170
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.

/// );
/// });
/// ```
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.

///
/// ```
/// 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...

/// 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).

@paulzhang5511
Copy link

How to save a canvas as an image

@DJMcNab
Copy link
Member

DJMcNab commented Nov 11, 2025

How to save a canvas as an image

This will need to be a follow-up. To avoid locking up the entire app, it would require a lot of care.

@philocalyst
Copy link
Contributor Author

Welp... I picked these changes up again and I'm confused what was added? There's a new bound for ViewArgument and &mut state is no longer allowed as a parameter...

@Philipp-M
Copy link
Member

These changes came from #1444

@philocalyst
Copy link
Contributor Author

philocalyst commented Nov 12, 2025 via email

richard-uk1 and others added 12 commits November 27, 2025 18:48
This is shorter, plays better with rustfmt, and is less likely to lead
to merge conflicts if two people add a module in parallel.
This is shorter, plays better with rustfmt, and is less likely to lead
to merge conflicts if two people add a module in parallel.
Unneeded &mut
I can't find the vello rexport :(
Thanks to @Philipp-M!!! I forgot about the suggestion
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants