Skip to content

Add Canvas widget #875

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open

Add Canvas widget #875

wants to merge 6 commits into from

Conversation

richard-uk1
Copy link
Contributor

I'm playing with making a vector path editor in xilem. This requires some extra stuff over what is currently available so I'm going to maintain a fork. I will try to merge upstream anything I think is more widely useful.

This PR allows the user to draw directly onto the scene fragment for a widget. Currently, the user must recreate the draw function every time there is a change in state. I wonder if there should be separated out state and draw parts, so the function needs to be updated less often.

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.

Apart from the pointer event TODO comment, this seems reasonable as an addition to Masonry.

// --- MARK: IMPL WIDGET ---
impl Widget for Canvas {
fn on_pointer_event(&mut self, _ctx: &mut EventCtx, _event: &PointerEvent) {
// TODO: ensure coordinates are correct and pass to callback
Copy link
Member

Choose a reason for hiding this comment

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

This comment is misleading. Something has gone wrong if the canvas widget is managing the pointer event. At that point, the user should be writing their own widget, I believe.

A canvas is basically equivalent to an Image, but containing a vector scene (ish).

}

fn accessibility(&mut self, _ctx: &mut AccessCtx, _node: &mut Node) {
// TODO: should probably give the caller the opportunity to handle accessibility
Copy link
Member

Choose a reason for hiding this comment

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

This would probably just be an alt-text like mechanism, although maybe we should also be strongly encouraging that a caption exist?

We haven't handled this for Image, which this is very similar to.

@richard-uk1
Copy link
Contributor Author

One question I have: can a scene fragment be retained and appended to the scene on paint?

@DJMcNab
Copy link
Member

DJMcNab commented Feb 25, 2025

Masonry is already doing that internally; your paint callback will only be called once, except when it gets replaced or the layout changes

@richard-uk1 richard-uk1 force-pushed the canvas_widget branch 2 times, most recently from 594c48d to 0326db5 Compare February 27, 2025 12:41
@richard-uk1
Copy link
Contributor Author

OK ready for review.

@richard-uk1
Copy link
Contributor Author

How is the CI so fast!?!?!

@richard-uk1 richard-uk1 requested a review from DJMcNab February 27, 2025 16:33
@PoignardAzur PoignardAzur changed the title add canvas widget Add Canvas widget Mar 1, 2025
Comment on lines 76 to 89
fn accepts_pointer_interaction(&self) -> bool {
false
}
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think you should set this flag to false. It makes Canvas transparent to pointers, meaning for example that if there's a widget behind the canvas, that widget will get pointer events, affect the pointer icon, etc.

Copy link
Member

Choose a reason for hiding this comment

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

I'm not certain that I agree; I can see arguments either way (since the canvas isn't filled by default, for example). But since if it did intercept pointer events, they would still bubble up to the parents, then either way is fine.

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 suggest setting it to true for now, then seeing how that works in practice.

Comment on lines 93 to 106
fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut Scene) {
(self.draw)(scene, ctx.size());
}
Copy link
Contributor

Choose a reason for hiding this comment

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

So, to be clear, this is a static canvas, right? The draw function isn't expected to paint different things from frame to frame?

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 think so - it could be used for things like icons. But what should be different if it were more dynamic?

@richard-uk1
Copy link
Contributor Author

Should be ready for review again (assuming CI is green). I will squash before merging, as I don't think there is anything to be gained by keeping the commits.

@Philipp-M
Copy link
Contributor

I will squash before merging, as I don't think there is anything to be gained by keeping the commits.

Not necessary though, as this is the default anyways when merging (with the title/text in the PR top comment as commit description), so keeping the commits should give a better timeline of this PR shouldn't it?

Comment on lines +13 to +28
/// A non-interactive text element.
/// # Example
///
/// ```ignore
/// use xilem::palette;
/// use xilem::view::label;
/// use masonry::TextAlignment;
/// use masonry::parley::fontique;
///
/// label("Text example.")
/// .brush(palette::css::RED)
/// .alignment(TextAlignment::Middle)
/// .text_size(24.0)
/// .weight(FontWeight::BOLD)
/// .with_font(fontique::GenericFamily::Serif)
/// ```
Copy link
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 these docs are right...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oops sorry I missed this!

_ctx: &mut ViewCtx,
element: Mut<Self::Element>,
) {
if !Arc::ptr_eq(&self.draw, &prev.draw) {
Copy link
Member

Choose a reason for hiding this comment

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

This is... always true, because canvas always allocates. I think the only sound way to handle this currently is to force the user to provide an already Arced function. That's... far from ideal from an ergonomics perspective, but it matches the semantics you need.

Essentially, you'd need to combine it with something like memoize for this API to be reasonable; it's the unfortunate ergonomics papercut around closures being incomparable (rust-lang/rfcs#3538)

let mut harness = TestHarness::create(canvas);

assert_debug_snapshot!(harness.root_widget());
assert_render_snapshot!(harness, "hello");
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
assert_render_snapshot!(harness, "hello");
assert_render_snapshot!(harness, "stroked_triangle");

(I'm not sure if _ or - is more idiomatic here)

this.ctx.request_render();
}

pub fn set_alt_text(mut this: WidgetMut<'_, Self>, alt_text: String) {
Copy link
Member

Choose a reason for hiding this comment

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

This not getting a warning for missing_docs suggests that something is wrong here. (It's fine for the docs to just point to Canvas::with_alt_text)

}

pub fn remove_alt_text(mut this: WidgetMut<'_, Self>) {
this.widget.alt_text = None;
Copy link
Member

Choose a reason for hiding this comment

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

What are the semantics of no alt text as opposed to setting it to ""? (cc @PoignardAzur, as you suggest setting it to Some(String::new()) if it's not a semantically meaningful canvas)

Copy link
Contributor

Choose a reason for hiding this comment

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

You'd have to ask @mwcampbell for an authoritative answer, but from what I remember:

  • Setting no alt-text means you "forgot" to add it, and some screen reader tools might add a default one for you.
  • Setting an empty alt text means you consider that the item shouldn't be described by screen readers.

At least I think that's how ARIA treats it.

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

@PoignardAzur, do you have an example of something which is "too hard" to describe through text, but which should be a canvas?

That is, if the "too hard" clause is for highly dynamic content, then it probably should be a bespoke widget with a proper accessibility implementation, and so the "too hard" clause doesn't apply. Would it be better for the alt text to say "we've chosen not to make this accessible because we can't be bothered" in that case?

Copy link
Contributor

Choose a reason for hiding this comment

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

The canonical way to say "We've chosen to not include this in the accessibility tree" is to add an empty string as alt-text.

Would it be better for the alt text to say "we've chosen not to make this accessible because we can't be bothered" in that case?

For this kind of question, you must keep in the mind the experience of reading a page with a screen reader. Like, if you're reading an article or using an app, does it make sense to have the reader say "Menu, Help, Files, New Project, Open Project, We're sorry but we've chosen to not make this app accessible"? Does it help you navigate the app better?

I don't have answers for the general case, and I'd advise against giving one. I want to discourage the mode of thinking that making an app accessible is about giving each widget a maximally descriptive alt text.

Copy link
Member

Choose a reason for hiding this comment

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

It's a good point. My thinking here does need to be focused on the user experience, and having a snarky message isn't that. But I still think there is an important distinction between the two cases, and using the same handling for both is doing a disservice to the end user. That is, the cases are:

  1. Not in the accessibility tree/only for visual display purposes. In that case setting to the empty string is the right behaviour. I agree completely with that.
  2. The content of this widget is potentially important to all users, but we've decided not to make it accessible because doing so would be "too hard". Add Canvas widget #875 (comment) seems to answer this; that if it's "too hard" to add alt text, then we should just not add it. That would then use the screen reader's default handling of "developer didn't bother to make this accessible".

This sentence in the docs conflates those two cases, I think. Having both be the same case has one of two consequences:

  1. It just completely hides that important information is missing from the user; i.e. creates an unknown unknown; OR
  2. It forces the user to doubt all items which are indicated as display-only

Copy link
Contributor

Choose a reason for hiding this comment

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

This comment is entirely correct.

Copy link
Contributor

@PoignardAzur PoignardAzur Mar 14, 2025

Choose a reason for hiding this comment

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

Which one? You mean Daniel's?

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes. You were also correct about the distinction between no alt text and empty alt text.

panic!("Snapshot test '{test_name}' failed: No reference file");
panic!(
"Snapshot test '{test_name}' failed: No reference file\n\
New screenshot created with `.new.png` extension. If correct, change to `.png`"
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 it would be slightly better style to indent this to match the start of the string, rather than the panic

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.

(Marking as request changes for the sake of GitHub's UI, to indicate that this is waiting on the author)

github-merge-queue bot pushed a commit that referenced this pull request Mar 19, 2025
Any new widget code needs to have full docs.
Not having `missing_docs` trigger by default for new widgets is quite
bad.

See #875 and #882 for cases where this over-broad allow has bitten us.

The comment about not using `expect(missing_docs)` because of
rust-analyzer is confusing to me; I don't run into an issue. It might
have been rust-lang/rust#130021, which is now
fixed. That comment should have had a link to an upstream issue for more
context.
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.

5 participants