From 3967c6e5479fc6f3966b51dac60d92edd840193f Mon Sep 17 00:00:00 2001 From: Richard Dodd Date: Mon, 24 Feb 2025 00:59:05 +0000 Subject: [PATCH 1/6] add canvas widget --- masonry/src/testing/harness.rs | 5 +- masonry/src/widgets/canvas.rs | 137 ++++++++++++++++++ masonry/src/widgets/mod.rs | 2 + ...masonry__widgets__canvas__tests__hello.png | 3 + ...widgets__canvas__tests__simple_canvas.snap | 5 + 5 files changed, 151 insertions(+), 1 deletion(-) create mode 100644 masonry/src/widgets/canvas.rs create mode 100644 masonry/src/widgets/screenshots/masonry__widgets__canvas__tests__hello.png create mode 100644 masonry/src/widgets/snapshots/masonry__widgets__canvas__tests__simple_canvas.snap diff --git a/masonry/src/testing/harness.rs b/masonry/src/testing/harness.rs index 10c2104ed2..88c1a2804c 100644 --- a/masonry/src/testing/harness.rs +++ b/masonry/src/testing/harness.rs @@ -715,7 +715,10 @@ impl TestHarness { // Remove '.new.png' file if it exists let _ = std::fs::remove_file(&new_path); new_image.save(&new_path).unwrap(); - 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`" + ); } } } diff --git a/masonry/src/widgets/canvas.rs b/masonry/src/widgets/canvas.rs new file mode 100644 index 0000000000..4f8dd6234e --- /dev/null +++ b/masonry/src/widgets/canvas.rs @@ -0,0 +1,137 @@ +// Copyright 2025 the Xilem Authors and the Druid Authors +// SPDX-License-Identifier: Apache-2.0 + +#![warn(missing_docs)] + +//! A canvas widget. + +use accesskit::{Node, Role}; +use smallvec::SmallVec; +use tracing::{trace_span, Span}; +use vello::kurbo::Size; +use vello::Scene; + +use crate::core::{ + AccessCtx, AccessEvent, BoxConstraints, EventCtx, LayoutCtx, PaintCtx, PointerEvent, QueryCtx, + RegisterCtx, TextEvent, Update, UpdateCtx, Widget, WidgetId, WidgetMut, +}; + +/// A widget allowing custom drawing. +pub struct Canvas { + draw: Box, + // TODO: pointer events +} + +// --- 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 { + draw: Box::new(draw), + } + } +} + +// --- MARK: WIDGETMUT --- +impl Canvas { + /// Update the draw function + pub fn update_draw( + this: &mut WidgetMut<'_, Self>, + draw: impl Fn(&mut Scene, Size) + Send + Sync + 'static, + ) { + this.widget.draw = Box::new(draw); + this.ctx.request_render(); + } +} + +// --- 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 + } + + fn accepts_pointer_interaction(&self) -> bool { + true + } + + fn on_text_event(&mut self, _ctx: &mut EventCtx, _event: &TextEvent) {} + + fn on_access_event(&mut self, _ctx: &mut EventCtx, _event: &AccessEvent) {} + + fn register_children(&mut self, _ctx: &mut RegisterCtx) {} + + fn update(&mut self, _ctx: &mut UpdateCtx, _event: &Update) {} + + fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size { + // use as much space as possible - caller can size it as necessary + bc.max() + } + + fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut Scene) { + (self.draw)(scene, ctx.size()) + } + + fn accessibility_role(&self) -> Role { + Role::Canvas + } + + fn accessibility(&mut self, _ctx: &mut AccessCtx, _node: &mut Node) { + // TODO: should probably give the caller the opportunity to handle accessibility + } + + fn children_ids(&self) -> SmallVec<[WidgetId; 16]> { + SmallVec::new() + } + + fn make_trace_span(&self, ctx: &QueryCtx<'_>) -> Span { + trace_span!("Canvas", id = ctx.widget_id().trace()) + } + + fn get_debug_text(&self) -> Option { + None + } +} + +// --- MARK: TESTS --- +#[cfg(test)] +mod tests { + use insta::assert_debug_snapshot; + use vello::kurbo::{Affine, BezPath, Stroke}; + use vello::peniko::{Color, Fill}; + + use super::*; + use crate::assert_render_snapshot; + 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(canvas); + + assert_debug_snapshot!(harness.root_widget()); + assert_render_snapshot!(harness, "hello"); + } +} diff --git a/masonry/src/widgets/mod.rs b/masonry/src/widgets/mod.rs index 9c666221c1..758bef4795 100644 --- a/masonry/src/widgets/mod.rs +++ b/masonry/src/widgets/mod.rs @@ -11,6 +11,7 @@ mod tests; mod align; mod button; +mod canvas; mod checkbox; mod flex; mod grid; @@ -31,6 +32,7 @@ mod zstack; pub use self::align::Align; pub use self::button::Button; +pub use self::canvas::Canvas; pub use self::checkbox::Checkbox; pub use self::flex::{Axis, CrossAxisAlignment, Flex, FlexParams, MainAxisAlignment}; pub use self::grid::{Grid, GridParams}; diff --git a/masonry/src/widgets/screenshots/masonry__widgets__canvas__tests__hello.png b/masonry/src/widgets/screenshots/masonry__widgets__canvas__tests__hello.png new file mode 100644 index 0000000000..86c6c982df --- /dev/null +++ b/masonry/src/widgets/screenshots/masonry__widgets__canvas__tests__hello.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d7634b7801c3be71f13923bb9a77da9aeffe67eb3b042eab9ccffbd647655759 +size 11377 diff --git a/masonry/src/widgets/snapshots/masonry__widgets__canvas__tests__simple_canvas.snap b/masonry/src/widgets/snapshots/masonry__widgets__canvas__tests__simple_canvas.snap new file mode 100644 index 0000000000..2e20cf8589 --- /dev/null +++ b/masonry/src/widgets/snapshots/masonry__widgets__canvas__tests__simple_canvas.snap @@ -0,0 +1,5 @@ +--- +source: masonry/src/widgets/canvas.rs +expression: harness.root_widget() +--- +Canvas From e30fcfc13c825e272050bfdd70c4c1e29eca892a Mon Sep 17 00:00:00 2001 From: Richard Dodd Date: Thu, 27 Feb 2025 12:31:39 +0000 Subject: [PATCH 2/6] impl suggestions and add xilem view for canvas --- masonry/src/widgets/canvas.rs | 54 ++++++++++++++++----- xilem/src/view/canvas.rs | 90 +++++++++++++++++++++++++++++++++++ xilem/src/view/mod.rs | 3 ++ 3 files changed, 134 insertions(+), 13 deletions(-) create mode 100644 xilem/src/view/canvas.rs diff --git a/masonry/src/widgets/canvas.rs b/masonry/src/widgets/canvas.rs index 4f8dd6234e..cda427b254 100644 --- a/masonry/src/widgets/canvas.rs +++ b/masonry/src/widgets/canvas.rs @@ -5,6 +5,8 @@ //! A canvas widget. +use std::sync::Arc; + use accesskit::{Node, Role}; use smallvec::SmallVec; use tracing::{trace_span, Span}; @@ -18,40 +20,61 @@ use crate::core::{ /// A widget allowing custom drawing. pub struct Canvas { - draw: Box, - // TODO: pointer events + draw: Arc, + alt_text: Option, } // --- 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) -> Self { Self { - draw: Box::new(draw), + 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 strongly encouraged to set alt text for the canvas. + pub fn with_alt_text(mut self, alt_text: impl Into) -> Self { + self.alt_text = Some(alt_text.into()); + self + } } // --- MARK: WIDGETMUT --- impl Canvas { /// Update the draw function pub fn update_draw( - this: &mut WidgetMut<'_, Self>, + this: WidgetMut<'_, Self>, draw: impl Fn(&mut Scene, Size) + Send + Sync + 'static, ) { - this.widget.draw = Box::new(draw); + Self::update_from_arc(this, Arc::new(draw)); + } + + /// Update the draw function + pub fn update_from_arc( + mut this: WidgetMut<'_, Self>, + draw: Arc, + ) { + this.widget.draw = draw; this.ctx.request_render(); } } // --- 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 - } + fn on_pointer_event(&mut self, _ctx: &mut EventCtx, _event: &PointerEvent) {} fn accepts_pointer_interaction(&self) -> bool { - true + false } fn on_text_event(&mut self, _ctx: &mut EventCtx, _event: &TextEvent) {} @@ -68,15 +91,20 @@ impl Widget for Canvas { } fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut Scene) { - (self.draw)(scene, ctx.size()) + (self.draw)(scene, ctx.size()); } fn accessibility_role(&self) -> Role { Role::Canvas } - fn accessibility(&mut self, _ctx: &mut AccessCtx, _node: &mut Node) { - // TODO: should probably give the caller the opportunity to handle accessibility + fn accessibility(&mut self, _ctx: &mut AccessCtx, node: &mut Node) { + // TODO: is this correct? + if let Some(text) = &self.alt_text { + node.set_description(text.clone()); + } else { + node.clear_description(); + } } fn children_ids(&self) -> SmallVec<[WidgetId; 16]> { @@ -88,7 +116,7 @@ impl Widget for Canvas { } fn get_debug_text(&self) -> Option { - None + self.alt_text.clone() } } diff --git a/xilem/src/view/canvas.rs b/xilem/src/view/canvas.rs new file mode 100644 index 0000000000..e573ea170b --- /dev/null +++ b/xilem/src/view/canvas.rs @@ -0,0 +1,90 @@ +// Copyright 2024 the Xilem Authors +// SPDX-License-Identifier: Apache-2.0 + +use std::sync::Arc; + +use masonry::widgets; +use vello::kurbo::Size; +use vello::Scene; + +use crate::core::{DynMessage, Mut, ViewMarker}; +use crate::{MessageResult, Pod, View, ViewCtx, ViewId}; + +/// 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) +/// ``` +pub fn canvas(draw: impl Fn(&mut Scene, Size) + Send + Sync + 'static) -> Canvas { + Canvas { + draw: Arc::new(draw), + alt_text: None, + } +} + +/// Create a canvas view. +#[must_use = "View values do nothing unless provided to Xilem."] +pub struct Canvas { + draw: Arc, + alt_text: Option, +} + +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 View for Canvas { + type Element = Pod; + type ViewState = (); + + fn build(&self, ctx: &mut ViewCtx) -> (Self::Element, Self::ViewState) { + let widget = widgets::Canvas::from_arc(self.draw.clone()); + + let widget_pod = ctx.new_pod(widget); + (widget_pod, ()) + } + + fn rebuild( + &self, + prev: &Self, + (): &mut Self::ViewState, + _ctx: &mut ViewCtx, + element: Mut, + ) { + if !Arc::ptr_eq(&self.draw, &prev.draw) { + widgets::Canvas::update_from_arc(element, self.draw.clone()); + } + } + + fn teardown(&self, (): &mut Self::ViewState, _: &mut ViewCtx, _: Mut) {} + + fn message( + &self, + (): &mut Self::ViewState, + _id_path: &[ViewId], + message: DynMessage, + _app_state: &mut State, + ) -> MessageResult { + tracing::error!("Message arrived in Canvas::message, but Canvas doesn't consume any messages, this is a bug"); + MessageResult::Stale(message) + } +} diff --git a/xilem/src/view/mod.rs b/xilem/src/view/mod.rs index 45cc038b7b..e1c0cd4fda 100644 --- a/xilem/src/view/mod.rs +++ b/xilem/src/view/mod.rs @@ -12,6 +12,9 @@ pub use worker::*; mod button; pub use button::*; +mod canvas; +pub use canvas::*; + mod checkbox; pub use checkbox::*; From b4dfea6b66724b1ffeb267bc3df499c588c4a848 Mon Sep 17 00:00:00 2001 From: Richard Dodd Date: Mon, 10 Mar 2025 14:31:01 +0000 Subject: [PATCH 3/6] Apply suggestions from code review Co-authored-by: Olivier FAURE --- masonry/src/widgets/canvas.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/masonry/src/widgets/canvas.rs b/masonry/src/widgets/canvas.rs index cda427b254..f428fb51fc 100644 --- a/masonry/src/widgets/canvas.rs +++ b/masonry/src/widgets/canvas.rs @@ -26,7 +26,7 @@ pub struct Canvas { // --- MARK: BUILDERS --- impl Canvas { - /// Create a new canvas with the given draw function + /// 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)) } @@ -43,6 +43,9 @@ impl Canvas { /// those using screen readers. /// /// Users are strongly 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 `""`. pub fn with_alt_text(mut self, alt_text: impl Into) -> Self { self.alt_text = Some(alt_text.into()); self From ec5c6530a68447c4116ecd4112fc660c62253fa3 Mon Sep 17 00:00:00 2001 From: Richard Dodd Date: Mon, 10 Mar 2025 15:15:33 +0000 Subject: [PATCH 4/6] implement suggestions --- masonry/src/widgets/canvas.rs | 25 +++++++++++++++---------- xilem/src/view/canvas.rs | 2 +- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/masonry/src/widgets/canvas.rs b/masonry/src/widgets/canvas.rs index f428fb51fc..261b3a3e02 100644 --- a/masonry/src/widgets/canvas.rs +++ b/masonry/src/widgets/canvas.rs @@ -1,8 +1,6 @@ // Copyright 2025 the Xilem Authors and the Druid Authors // SPDX-License-Identifier: Apache-2.0 -#![warn(missing_docs)] - //! A canvas widget. use std::sync::Arc; @@ -42,7 +40,7 @@ impl Canvas { /// Set the text that will be used to communicate the meaning of the canvas to /// those using screen readers. /// - /// Users are strongly encouraged to set alt text for the canvas. + /// 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 `""`. @@ -55,21 +53,31 @@ impl Canvas { // --- MARK: WIDGETMUT --- impl Canvas { /// Update the draw function - pub fn update_draw( + pub fn set_painter( this: WidgetMut<'_, Self>, draw: impl Fn(&mut Scene, Size) + Send + Sync + 'static, ) { - Self::update_from_arc(this, Arc::new(draw)); + Self::set_painter_arc(this, Arc::new(draw)); } /// Update the draw function - pub fn update_from_arc( + pub fn set_painter_arc( mut this: WidgetMut<'_, Self>, draw: Arc, ) { this.widget.draw = draw; this.ctx.request_render(); } + + pub fn set_alt_text(mut this: WidgetMut<'_, Self>, alt_text: String) { + this.widget.alt_text = Some(alt_text); + this.ctx.request_accessibility_update(); + } + + pub fn remove_alt_text(mut this: WidgetMut<'_, Self>) { + this.widget.alt_text = None; + this.ctx.request_accessibility_update(); + } } // --- MARK: IMPL WIDGET --- @@ -77,7 +85,7 @@ impl Widget for Canvas { fn on_pointer_event(&mut self, _ctx: &mut EventCtx, _event: &PointerEvent) {} fn accepts_pointer_interaction(&self) -> bool { - false + true } fn on_text_event(&mut self, _ctx: &mut EventCtx, _event: &TextEvent) {} @@ -102,11 +110,8 @@ impl Widget for Canvas { } fn accessibility(&mut self, _ctx: &mut AccessCtx, node: &mut Node) { - // TODO: is this correct? if let Some(text) = &self.alt_text { node.set_description(text.clone()); - } else { - node.clear_description(); } } diff --git a/xilem/src/view/canvas.rs b/xilem/src/view/canvas.rs index e573ea170b..83bf259d7f 100644 --- a/xilem/src/view/canvas.rs +++ b/xilem/src/view/canvas.rs @@ -71,7 +71,7 @@ impl View for Canvas { element: Mut, ) { if !Arc::ptr_eq(&self.draw, &prev.draw) { - widgets::Canvas::update_from_arc(element, self.draw.clone()); + widgets::Canvas::set_painter_arc(element, self.draw.clone()); } } From 033e5bd658787ad5f182f0679b998f760b48556d Mon Sep 17 00:00:00 2001 From: Richard Dodd Date: Mon, 10 Mar 2025 15:19:56 +0000 Subject: [PATCH 5/6] fix after rebase --- masonry/src/widgets/canvas.rs | 46 ++++++++++++++++++++++++++--------- 1 file changed, 35 insertions(+), 11 deletions(-) diff --git a/masonry/src/widgets/canvas.rs b/masonry/src/widgets/canvas.rs index 261b3a3e02..5a531c7a71 100644 --- a/masonry/src/widgets/canvas.rs +++ b/masonry/src/widgets/canvas.rs @@ -7,13 +7,14 @@ use std::sync::Arc; use accesskit::{Node, Role}; use smallvec::SmallVec; -use tracing::{trace_span, Span}; -use vello::kurbo::Size; +use tracing::{Span, trace_span}; use vello::Scene; +use vello::kurbo::Size; use crate::core::{ - AccessCtx, AccessEvent, BoxConstraints, EventCtx, LayoutCtx, PaintCtx, PointerEvent, QueryCtx, - RegisterCtx, TextEvent, Update, UpdateCtx, Widget, WidgetId, WidgetMut, + AccessCtx, AccessEvent, BoxConstraints, EventCtx, LayoutCtx, PaintCtx, PointerEvent, + PropertiesMut, PropertiesRef, QueryCtx, RegisterCtx, TextEvent, Update, UpdateCtx, Widget, + WidgetId, WidgetMut, }; /// A widget allowing custom drawing. @@ -82,26 +83,49 @@ impl Canvas { // --- MARK: IMPL WIDGET --- impl Widget for Canvas { - fn on_pointer_event(&mut self, _ctx: &mut EventCtx, _event: &PointerEvent) {} + fn on_pointer_event( + &mut self, + _ctx: &mut EventCtx, + _props: &mut PropertiesMut, + _event: &PointerEvent, + ) { + } fn accepts_pointer_interaction(&self) -> bool { true } - fn on_text_event(&mut self, _ctx: &mut EventCtx, _event: &TextEvent) {} + fn on_text_event( + &mut self, + _ctx: &mut EventCtx, + _props: &mut PropertiesMut, + _event: &TextEvent, + ) { + } - fn on_access_event(&mut self, _ctx: &mut EventCtx, _event: &AccessEvent) {} + 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, _event: &Update) {} + fn update(&mut self, _ctx: &mut UpdateCtx, _props: &mut PropertiesMut, _event: &Update) {} - fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size { + 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, scene: &mut Scene) { + fn paint(&mut self, ctx: &mut PaintCtx, _props: &PropertiesRef, scene: &mut Scene) { (self.draw)(scene, ctx.size()); } @@ -109,7 +133,7 @@ impl Widget for Canvas { Role::Canvas } - fn accessibility(&mut self, _ctx: &mut AccessCtx, node: &mut Node) { + fn accessibility(&mut self, _ctx: &mut AccessCtx, _props: &PropertiesRef, node: &mut Node) { if let Some(text) = &self.alt_text { node.set_description(text.clone()); } From b63a2c8e68125754019c7b467b4132bb27cc6b1f Mon Sep 17 00:00:00 2001 From: Richard Dodd Date: Mon, 10 Mar 2025 15:23:06 +0000 Subject: [PATCH 6/6] fix fmt --- xilem/src/view/canvas.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/xilem/src/view/canvas.rs b/xilem/src/view/canvas.rs index 83bf259d7f..61b6311e9b 100644 --- a/xilem/src/view/canvas.rs +++ b/xilem/src/view/canvas.rs @@ -4,8 +4,8 @@ use std::sync::Arc; use masonry::widgets; -use vello::kurbo::Size; use vello::Scene; +use vello::kurbo::Size; use crate::core::{DynMessage, Mut, ViewMarker}; use crate::{MessageResult, Pod, View, ViewCtx, ViewId}; @@ -84,7 +84,9 @@ impl View for Canvas { message: DynMessage, _app_state: &mut State, ) -> MessageResult { - tracing::error!("Message arrived in Canvas::message, but Canvas doesn't consume any messages, this is a bug"); + tracing::error!( + "Message arrived in Canvas::message, but Canvas doesn't consume any messages, this is a bug" + ); MessageResult::Stale(message) } }