diff --git a/masonry/src/widget/board.rs b/masonry/src/widget/board.rs new file mode 100644 index 000000000..681c4d831 --- /dev/null +++ b/masonry/src/widget/board.rs @@ -0,0 +1,464 @@ +// Copyright 2018 the Xilem Authors and the Druid Authors +// SPDX-License-Identifier: Apache-2.0 + +//! A widget that arranges its children in fixed positions. + +use std::ops::{Deref as _, DerefMut as _}; + +use accesskit::Role; +use smallvec::SmallVec; +use tracing::{trace_span, Span}; +use vello::Scene; + +use crate::widget::WidgetMut; +use crate::{ + AccessCtx, AccessEvent, BoxConstraints, EventCtx, LayoutCtx, LifeCycle, LifeCycleCtx, PaintCtx, + Point, PointerEvent, Rect, Size, StatusChange, TextEvent, Widget, WidgetId, WidgetPod, +}; + +/// A container with absolute positioning layout. +pub struct Board { + children: Vec>>, +} + +/// Parameters for an item in a [`Board`] container. +#[derive(Debug, Copy, Clone, PartialEq)] +pub struct BoardParams { + pub origin: Point, + pub size: Size, +} + +/// Wrapper of a regular widget for use in [`Board`] +pub struct PositionedElement { + inner: W, + params: BoardParams, +} + +/// A trait representing a widget which knows its origin and size. +pub trait SvgElement: Widget { + // Origin of the widget relative to its parent + fn origin(&self) -> Point; + // Size of the widget + fn size(&self) -> Size; + + // Sets the origin of the widget relative to its parent + fn set_origin(&mut self, origin: Point); + // Sets the size of the widget + fn set_size(&mut self, size: Size); +} + +// --- MARK: IMPL BOARD --- +impl Board { + /// Create a new empty Board. + pub fn new() -> Self { + Board { + children: Vec::new(), + } + } + + /// Builder-style method to add a positioned child to the container. + pub fn with_child_pod(mut self, child: WidgetPod>) -> Self { + self.children.push(child); + self + } + + pub fn len(&self) -> usize { + self.children.len() + } + + pub fn is_empty(&self) -> bool { + self.len() == 0 + } +} + +impl Default for Board { + fn default() -> Self { + Self::new() + } +} + +// --- MARK: WIDGETMUT--- +impl<'a> WidgetMut<'a, Board> { + /// Add a positioned child widget. + pub fn add_child(&mut self, child: impl SvgElement) { + self.widget.children.push(WidgetPod::new(Box::new(child))); + self.ctx.children_changed(); + } + + pub fn insert_child(&mut self, idx: usize, child: WidgetPod>) { + self.widget.children.insert(idx, child); + self.ctx.children_changed(); + } + + pub fn remove_child(&mut self, idx: usize) { + let widget = self.widget.children.remove(idx); + self.ctx.remove_child(widget); + self.ctx.request_layout(); + } + + pub fn child_mut(&mut self, idx: usize) -> WidgetMut<'_, Box> { + self.ctx.get_mut(&mut self.widget.children[idx]) + } + + pub fn clear(&mut self) { + if !self.widget.children.is_empty() { + self.ctx.request_layout(); + + for child in self.widget.children.drain(..) { + self.ctx.remove_child(child); + } + } + } +} + +impl<'a, W: Widget> WidgetMut<'a, PositionedElement> { + pub fn inner_mut(&mut self) -> WidgetMut<'_, W> { + WidgetMut { + ctx: self.ctx.reborrow_mut(), + widget: &mut self.widget.inner, + } + } +} + +impl<'a> WidgetMut<'a, Box> { + /// Attempt to downcast to `WidgetMut` of concrete Widget type. + pub fn try_downcast(&mut self) -> Option> { + Some(WidgetMut { + ctx: self.ctx.reborrow_mut(), + widget: self.widget.as_mut_any().downcast_mut()?, + }) + } + + /// Downcasts to `WidgetMut` of concrete Widget type. + /// + /// ## Panics + /// + /// Panics if the downcast fails, with an error message that shows the + /// discrepancy between the expected and actual types. + pub fn downcast(&mut self) -> WidgetMut<'_, W2> { + let w1_name = self.widget.type_name(); + match self.widget.as_mut_any().downcast_mut() { + Some(widget) => WidgetMut { + ctx: self.ctx.reborrow_mut(), + widget, + }, + None => { + panic!( + "failed to downcast widget: expected widget of type `{}`, found `{}`", + std::any::type_name::(), + w1_name, + ); + } + } + } +} + +// --- MARK: IMPL WIDGET BOARD --- +impl Widget for Board { + fn on_pointer_event(&mut self, _ctx: &mut EventCtx, _event: &PointerEvent) {} + fn on_text_event(&mut self, _ctx: &mut EventCtx, _event: &TextEvent) {} + fn on_access_event(&mut self, _ctx: &mut EventCtx, _event: &AccessEvent) {} + fn on_status_change(&mut self, _ctx: &mut LifeCycleCtx, _event: &StatusChange) {} + + fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle) { + for child in &mut self.children { + child.lifecycle(ctx, event); + } + } + + fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size { + bc.debug_check("Board"); + + for child in &mut self.children { + let (size, origin) = { + let child_ref = ctx.get_raw_ref(child); + (child_ref.widget().size(), child_ref.widget().origin()) + }; + ctx.run_layout(child, &BoxConstraints::tight(size)); + ctx.place_child(child, origin); + } + + bc.max() + } + + fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut Scene) {} + + fn accessibility_role(&self) -> Role { + Role::GenericContainer + } + + fn accessibility(&mut self, _ctx: &mut AccessCtx) {} + + fn children_ids(&self) -> SmallVec<[WidgetId; 16]> { + self.children.iter().map(|child| child.id()).collect() + } + + fn make_trace_span(&self) -> Span { + trace_span!("Board") + } +} + +// --- MARK: IMPL WIDGET POSITIONEDELEMENT --- +impl Widget for PositionedElement { + fn on_status_change(&mut self, ctx: &mut LifeCycleCtx, event: &StatusChange) { + self.inner.on_status_change(ctx, event); + } + + fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle) { + self.inner.lifecycle(ctx, event); + } + + fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size { + self.inner.layout(ctx, bc) + } + + fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut Scene) { + self.inner.paint(ctx, scene); + } + + fn accessibility_role(&self) -> Role { + self.inner.accessibility_role() + } + + fn accessibility(&mut self, ctx: &mut AccessCtx) { + self.inner.accessibility(ctx); + } + + fn children_ids(&self) -> SmallVec<[WidgetId; 16]> { + self.inner.children_ids() + } + + fn on_pointer_event(&mut self, ctx: &mut EventCtx, event: &PointerEvent) { + self.inner.on_pointer_event(ctx, event); + } + + fn on_text_event(&mut self, ctx: &mut EventCtx, event: &TextEvent) { + self.inner.on_text_event(ctx, event); + } + + fn on_access_event(&mut self, ctx: &mut EventCtx, event: &AccessEvent) { + self.inner.on_access_event(ctx, event); + } + + fn compose(&mut self, ctx: &mut crate::ComposeCtx) { + self.inner.compose(ctx); + } + + fn skip_pointer(&self) -> bool { + self.inner.skip_pointer() + } + + fn get_debug_text(&self) -> Option { + self.inner.get_debug_text() + } + + fn get_cursor(&self) -> cursor_icon::CursorIcon { + self.inner.get_cursor() + } +} + +impl SvgElement for PositionedElement { + fn origin(&self) -> Point { + self.params.origin + } + + fn size(&self) -> Size { + self.params.size + } + + fn set_origin(&mut self, origin: Point) { + self.params.origin = origin; + } + + fn set_size(&mut self, size: Size) { + self.params.size = size; + } +} + +// --- MARK: IMPL WIDGET SVGELEMENT --- +impl Widget for Box { + fn on_pointer_event(&mut self, ctx: &mut EventCtx, event: &PointerEvent) { + self.deref_mut().on_pointer_event(ctx, event); + } + + fn on_text_event(&mut self, ctx: &mut EventCtx, event: &TextEvent) { + self.deref_mut().on_text_event(ctx, event); + } + + fn on_access_event(&mut self, ctx: &mut EventCtx, event: &AccessEvent) { + self.deref_mut().on_access_event(ctx, event); + } + + fn on_status_change(&mut self, ctx: &mut LifeCycleCtx, event: &StatusChange) { + self.deref_mut().on_status_change(ctx, event); + } + + fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle) { + self.deref_mut().lifecycle(ctx, event); + } + + fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size { + self.deref_mut().layout(ctx, bc) + } + + fn compose(&mut self, ctx: &mut crate::ComposeCtx) { + self.deref_mut().compose(ctx); + } + + fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut Scene) { + self.deref_mut().paint(ctx, scene); + } + + fn accessibility_role(&self) -> Role { + self.deref().accessibility_role() + } + + fn accessibility(&mut self, ctx: &mut AccessCtx) { + self.deref_mut().accessibility(ctx); + } + + fn type_name(&self) -> &'static str { + self.deref().type_name() + } + + fn short_type_name(&self) -> &'static str { + self.deref().short_type_name() + } + + fn children_ids(&self) -> SmallVec<[WidgetId; 16]> { + self.deref().children_ids() + } + + fn skip_pointer(&self) -> bool { + self.deref().skip_pointer() + } + + fn make_trace_span(&self) -> Span { + self.deref().make_trace_span() + } + + fn get_debug_text(&self) -> Option { + self.deref().get_debug_text() + } + + fn get_cursor(&self) -> cursor_icon::CursorIcon { + self.deref().get_cursor() + } + + fn as_any(&self) -> &dyn std::any::Any { + self.deref().as_any() + } + + fn as_mut_any(&mut self) -> &mut dyn std::any::Any { + self.deref_mut().as_mut_any() + } +} + +impl SvgElement for Box { + fn origin(&self) -> Point { + self.deref().origin() + } + + fn size(&self) -> Size { + self.deref().size() + } + + fn set_origin(&mut self, origin: Point) { + self.deref_mut().set_origin(origin); + } + + fn set_size(&mut self, size: Size) { + self.deref_mut().set_size(size); + } +} + +// --- MARK: OTHER IMPLS--- +impl BoardParams { + /// Create a `BoardParams` with a specific `origin` and `size`. + pub fn new(origin: impl Into, size: impl Into) -> Self { + BoardParams { + origin: origin.into(), + size: size.into(), + } + } +} + +impl From for BoardParams { + fn from(rect: Rect) -> Self { + BoardParams { + origin: rect.origin(), + size: rect.size(), + } + } +} + +impl WidgetPod { + pub fn positioned(self, params: impl Into) -> WidgetPod> { + let id = self.id(); + WidgetPod::new_with_id( + PositionedElement { + inner: self.inner().unwrap(), + params: params.into(), + }, + id, + ) + } +} + +impl WidgetPod> { + pub fn svg_boxed(self) -> WidgetPod> { + let id = self.id(); + WidgetPod::new_with_id(Box::new(self.inner().unwrap()), id) + } +} + +// --- MARK: TESTS --- +#[cfg(test)] +mod tests { + use vello::kurbo::{Circle, Stroke}; + use vello::peniko::Brush; + + use super::*; + use crate::assert_render_snapshot; + use crate::testing::TestHarness; + use crate::widget::{Button, KurboShape}; + + #[test] + fn board_absolute_placement_snapshots() { + let board = Board::new() + .with_child_pod( + WidgetPod::new(Button::new("hello")) + .positioned(Rect::new(10., 10., 60., 40.)) + .svg_boxed(), + ) + .with_child_pod( + WidgetPod::new(Button::new("world")) + .positioned(Rect::new(30., 30., 80., 60.)) + .svg_boxed(), + ); + + let mut harness = TestHarness::create(board); + + assert_render_snapshot!(harness, "absolute_placement"); + } + + #[test] + fn board_shape_placement_snapshots() { + let mut shape = KurboShape::new(Circle::new((70., 50.), 30.)); + shape.set_fill_brush(Brush::Solid(vello::peniko::Color::NAVY)); + shape.set_stroke_style(Stroke::new(2.).with_dashes(0., [2., 1.])); + shape.set_stroke_brush(Brush::Solid(vello::peniko::Color::PALE_VIOLET_RED)); + + let board = Board::new() + .with_child_pod( + WidgetPod::new(Button::new("hello")) + .positioned(Rect::new(10., 10., 60., 40.)) + .svg_boxed(), + ) + .with_child_pod(WidgetPod::new(Box::new(shape))); + + let mut harness = TestHarness::create(board); + + assert_render_snapshot!(harness, "shape_placement"); + } +} diff --git a/masonry/src/widget/kurbo.rs b/masonry/src/widget/kurbo.rs new file mode 100644 index 000000000..6d0799d2b --- /dev/null +++ b/masonry/src/widget/kurbo.rs @@ -0,0 +1,406 @@ +// Copyright 2024 the Xilem Authors +// SPDX-License-Identifier: Apache-2.0 + +//! A widget representing a `kurbo::Shape`. + +use accesskit::Role; +use smallvec::SmallVec; +use tracing::{trace_span, warn, Span}; +use vello::kurbo::{ + self, Affine, Arc, BezPath, Circle, CircleSegment, CubicBez, Ellipse, Line, PathEl, PathSeg, + QuadBez, RoundedRect, Shape, Stroke, +}; +use vello::peniko::{Brush, Fill}; +use vello::Scene; + +use crate::widget::{SvgElement, WidgetMut, WidgetPod}; +use crate::{ + AccessCtx, AccessEvent, BoxConstraints, EventCtx, LayoutCtx, LifeCycle, LifeCycleCtx, PaintCtx, + Point, PointerEvent, Rect, Size, StatusChange, TextEvent, Widget, WidgetId, +}; + +/// A widget representing a `kurbo::Shape`. +pub struct KurboShape { + shape: ConcreteShape, + transform: Affine, + fill: Option, + stroke: Option, +} + +struct FillParams { + mode: Fill, + brush: Brush, + brush_transform: Option, +} + +#[derive(Default)] +struct StrokeParams { + style: Stroke, + brush: Brush, + brush_transform: Option, +} + +/// A concrete type for all built-in `kurbo::Shape`s. +// TODO: Adopt `kurbo::ConcreteShape` once https://github.com/linebender/kurbo/pull/331 merges +#[derive(Debug, Clone, PartialEq)] +pub enum ConcreteShape { + PathSeg(PathSeg), + Arc(Arc), + BezPath(BezPath), + Circle(Circle), + CircleSegment(CircleSegment), + CubicBez(CubicBez), + Ellipse(Ellipse), + Line(Line), + QuadBez(QuadBez), + Rect(Rect), + RoundedRect(RoundedRect), +} + +// --- MARK: IMPL KURBOSHAPE --- +impl KurboShape { + pub fn new(shape: impl Into) -> Self { + KurboShape { + shape: shape.into(), + transform: Default::default(), + fill: None, + stroke: None, + } + } + + pub fn shape(&self) -> &ConcreteShape { + &self.shape + } + + pub fn set_transform(&mut self, transform: Affine) { + self.transform = transform; + } + + pub fn set_fill_mode(&mut self, fill_mode: Fill) { + self.fill.get_or_insert_with(Default::default).mode = fill_mode; + } + + pub fn set_fill_brush(&mut self, fill_brush: Brush) { + self.fill.get_or_insert_with(Default::default).brush = fill_brush; + } + + pub fn set_fill_brush_transform(&mut self, fill_brush_transform: Option) { + self.fill + .get_or_insert_with(Default::default) + .brush_transform = fill_brush_transform; + } + + pub fn set_stroke_style(&mut self, stroke_style: Stroke) { + self.stroke.get_or_insert_with(Default::default).style = stroke_style; + } + + pub fn set_stroke_brush(&mut self, stroke_brush: Brush) { + self.stroke.get_or_insert_with(Default::default).brush = stroke_brush; + } + + pub fn set_stroke_brush_transform(&mut self, stroke_brush_transform: Option) { + self.stroke + .get_or_insert_with(Default::default) + .brush_transform = stroke_brush_transform; + } +} + +// MARK: WIDGETMUT +impl<'a> WidgetMut<'a, KurboShape> { + pub fn set_shape(&mut self, shape: ConcreteShape) { + self.widget.shape = shape; + self.ctx.request_layout(); + self.ctx.request_paint(); + self.ctx.request_accessibility_update(); + } + + pub fn set_transform(&mut self, transform: Affine) { + self.widget.transform = transform; + self.ctx.request_paint(); + } + + pub fn set_fill_mode(&mut self, fill_mode: Fill) { + self.widget.fill.get_or_insert_with(Default::default).mode = fill_mode; + self.ctx.request_paint(); + } + + pub fn set_fill_brush(&mut self, fill_brush: Brush) { + self.widget.fill.get_or_insert_with(Default::default).brush = fill_brush; + self.ctx.request_paint(); + } + + pub fn set_fill_brush_transform(&mut self, fill_brush_transform: Option) { + self.widget + .fill + .get_or_insert_with(Default::default) + .brush_transform = fill_brush_transform; + self.ctx.request_paint(); + } + + pub fn set_stroke_style(&mut self, stroke_style: Stroke) { + self.widget + .stroke + .get_or_insert_with(Default::default) + .style = stroke_style; + self.ctx.request_paint(); + } + + pub fn set_stroke_brush(&mut self, stroke_brush: Brush) { + self.widget + .stroke + .get_or_insert_with(Default::default) + .brush = stroke_brush; + self.ctx.request_paint(); + } + + pub fn set_stroke_brush_transform(&mut self, stroke_brush_transform: Option) { + self.widget + .stroke + .get_or_insert_with(Default::default) + .brush_transform = stroke_brush_transform; + self.ctx.request_paint(); + } +} + +// MARK: IMPL WIDGET +impl Widget for KurboShape { + fn on_pointer_event(&mut self, _ctx: &mut EventCtx, _event: &PointerEvent) {} + fn on_text_event(&mut self, _ctx: &mut EventCtx, _event: &TextEvent) {} + fn on_access_event(&mut self, _ctx: &mut EventCtx, _event: &AccessEvent) {} + fn on_status_change(&mut self, _ctx: &mut LifeCycleCtx, _event: &StatusChange) {} + fn lifecycle(&mut self, _ctx: &mut LifeCycleCtx, _event: &LifeCycle) {} + + fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size { + let size = self.shape.bounding_box().size(); + if !bc.contains(size) { + warn!("The shape is oversized"); + } + size + } + + fn paint(&mut self, _ctx: &mut PaintCtx, scene: &mut Scene) { + let transform = self + .transform + .then_translate(-self.shape.bounding_box().origin().to_vec2()); + if let Some(FillParams { + mode, + brush, + brush_transform, + }) = &self.fill + { + scene.fill(*mode, transform, brush, *brush_transform, &self.shape); + } + if let Some(StrokeParams { + style, + brush, + brush_transform, + }) = &self.stroke + { + scene.stroke(style, transform, brush, *brush_transform, &self.shape); + } + } + + fn accessibility_role(&self) -> Role { + Role::GraphicsSymbol + } + + fn accessibility(&mut self, _ctx: &mut AccessCtx) {} + + fn children_ids(&self) -> SmallVec<[WidgetId; 16]> { + SmallVec::new() + } + + fn make_trace_span(&self) -> Span { + trace_span!("KurboShape") + } +} + +impl SvgElement for KurboShape { + fn origin(&self) -> Point { + self.shape.bounding_box().origin() + } + + fn size(&self) -> Size { + self.shape.bounding_box().size() + } + + fn set_origin(&mut self, _: Point) { + panic!("a shape does not support setting its origin after creation") + } + + fn set_size(&mut self, _: Size) { + panic!("a shape does not support setting its size after creation") + } +} + +// --- MARK: OTHER IMPLS --- +impl Default for FillParams { + fn default() -> Self { + Self { + mode: Fill::NonZero, + brush: Default::default(), + brush_transform: Default::default(), + } + } +} + +impl WidgetPod { + pub fn svg_boxed(self) -> WidgetPod> { + let id = self.id(); + WidgetPod::new_with_id(Box::new(self.inner().unwrap()), id) + } +} + +macro_rules! for_all_variants { + ($self:expr; $i:ident => $e:expr) => { + match $self { + Self::PathSeg($i) => $e, + Self::Arc($i) => $e, + Self::BezPath($i) => $e, + Self::Circle($i) => $e, + Self::CircleSegment($i) => $e, + Self::CubicBez($i) => $e, + Self::Ellipse($i) => $e, + Self::Line($i) => $e, + Self::QuadBez($i) => $e, + Self::Rect($i) => $e, + Self::RoundedRect($i) => $e, + } + }; +} + +impl Shape for ConcreteShape { + type PathElementsIter<'iter> = PathElementsIter<'iter>; + + fn path_elements(&self, tolerance: f64) -> Self::PathElementsIter<'_> { + match self { + Self::PathSeg(i) => PathElementsIter::PathSeg(i.path_elements(tolerance)), + Self::Arc(i) => PathElementsIter::Arc(i.path_elements(tolerance)), + Self::BezPath(i) => PathElementsIter::BezPath(i.path_elements(tolerance)), + Self::Circle(i) => PathElementsIter::Circle(i.path_elements(tolerance)), + Self::CircleSegment(i) => PathElementsIter::CircleSegment(i.path_elements(tolerance)), + Self::CubicBez(i) => PathElementsIter::CubicBez(i.path_elements(tolerance)), + Self::Ellipse(i) => PathElementsIter::Ellipse(i.path_elements(tolerance)), + Self::Line(i) => PathElementsIter::Line(i.path_elements(tolerance)), + Self::QuadBez(i) => PathElementsIter::QuadBez(i.path_elements(tolerance)), + Self::Rect(i) => PathElementsIter::Rect(i.path_elements(tolerance)), + Self::RoundedRect(i) => PathElementsIter::RoundedRect(i.path_elements(tolerance)), + } + } + + fn area(&self) -> f64 { + for_all_variants!(self; i => i.area()) + } + + fn perimeter(&self, accuracy: f64) -> f64 { + for_all_variants!(self; i => i.perimeter(accuracy)) + } + + fn winding(&self, pt: Point) -> i32 { + for_all_variants!(self; i => i.winding(pt)) + } + + fn bounding_box(&self) -> Rect { + for_all_variants!(self; i => i.bounding_box()) + } + + fn to_path(&self, tolerance: f64) -> BezPath { + for_all_variants!(self; i => i.to_path(tolerance)) + } + + fn into_path(self, tolerance: f64) -> BezPath { + for_all_variants!(self; i => i.into_path(tolerance)) + } + + fn contains(&self, pt: Point) -> bool { + for_all_variants!(self; i => i.contains(pt)) + } + + fn as_line(&self) -> Option { + for_all_variants!(self; i => i.as_line()) + } + + fn as_rect(&self) -> Option { + for_all_variants!(self; i => i.as_rect()) + } + + fn as_rounded_rect(&self) -> Option { + for_all_variants!(self; i => i.as_rounded_rect()) + } + + fn as_circle(&self) -> Option { + for_all_variants!(self; i => i.as_circle()) + } + + fn as_path_slice(&self) -> Option<&[PathEl]> { + for_all_variants!(self; i => i.as_path_slice()) + } +} + +macro_rules! impl_from_shape { + ($t:ident) => { + impl From for ConcreteShape { + fn from(value: kurbo::$t) -> Self { + ConcreteShape::$t(value) + } + } + }; +} + +impl_from_shape!(PathSeg); +impl_from_shape!(Arc); +impl_from_shape!(BezPath); +impl_from_shape!(Circle); +impl_from_shape!(CircleSegment); +impl_from_shape!(CubicBez); +impl_from_shape!(Ellipse); +impl_from_shape!(Line); +impl_from_shape!(QuadBez); +impl_from_shape!(Rect); +impl_from_shape!(RoundedRect); + +pub enum PathElementsIter<'i> { + PathSeg(::PathElementsIter<'i>), + Arc(::PathElementsIter<'i>), + BezPath(::PathElementsIter<'i>), + Circle(::PathElementsIter<'i>), + CircleSegment(::PathElementsIter<'i>), + CubicBez(::PathElementsIter<'i>), + Ellipse(::PathElementsIter<'i>), + Line(::PathElementsIter<'i>), + QuadBez(::PathElementsIter<'i>), + Rect(::PathElementsIter<'i>), + RoundedRect(::PathElementsIter<'i>), +} + +impl<'i> Iterator for PathElementsIter<'i> { + type Item = PathEl; + + fn next(&mut self) -> Option { + for_all_variants!(self; i => i.next()) + } +} + +// --- MARK: TESTS --- +#[cfg(test)] +mod tests { + use vello::{kurbo::Circle, peniko::Brush}; + + use super::*; + use crate::assert_render_snapshot; + use crate::testing::TestHarness; + + #[test] + fn kurbo_shape_circle() { + let mut widget = KurboShape::new(Circle::new((50., 50.), 30.)); + widget.set_fill_brush(Brush::Solid(vello::peniko::Color::CHARTREUSE)); + widget.set_stroke_style(Stroke::new(2.).with_dashes(0., [2., 1.])); + widget.set_stroke_brush(Brush::Solid(vello::peniko::Color::PALE_VIOLET_RED)); + + let mut harness = TestHarness::create(widget); + + assert_render_snapshot!(harness, "kurbo_shape_circle"); + } + + // TODO: add test for KurboShape in Flex +} diff --git a/masonry/src/widget/mod.rs b/masonry/src/widget/mod.rs index 7a3720ca2..76b90e4e2 100644 --- a/masonry/src/widget/mod.rs +++ b/masonry/src/widget/mod.rs @@ -14,11 +14,13 @@ mod widget_state; mod tests; mod align; +mod board; mod button; mod checkbox; mod flex; mod grid; mod image; +mod kurbo; mod label; mod portal; mod progress_bar; @@ -34,10 +36,12 @@ mod widget_arena; pub use self::image::Image; pub use align::Align; +pub use board::{Board, BoardParams, PositionedElement, SvgElement}; pub use button::Button; pub use checkbox::Checkbox; pub use flex::{Axis, CrossAxisAlignment, Flex, FlexParams, MainAxisAlignment}; pub use grid::{Grid, GridParams}; +pub use kurbo::{ConcreteShape, KurboShape}; pub use label::{Label, LineBreaking}; pub use portal::Portal; pub use progress_bar::ProgressBar; diff --git a/masonry/src/widget/screenshots/masonry__widget__board__tests__absolute_placement.png b/masonry/src/widget/screenshots/masonry__widget__board__tests__absolute_placement.png new file mode 100644 index 000000000..37313b233 --- /dev/null +++ b/masonry/src/widget/screenshots/masonry__widget__board__tests__absolute_placement.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1816caeab124dbfef4d81ec1e261788e968b5613a4c6c4bf937622b464aaac06 +size 7127 diff --git a/masonry/src/widget/screenshots/masonry__widget__board__tests__shape_placement.png b/masonry/src/widget/screenshots/masonry__widget__board__tests__shape_placement.png new file mode 100644 index 000000000..2ffbf9895 --- /dev/null +++ b/masonry/src/widget/screenshots/masonry__widget__board__tests__shape_placement.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d52e9a782b51e4c3d845e8a5b1d3b9e4eeb478c4e95213cfd02de5545b41401e +size 8085 diff --git a/masonry/src/widget/screenshots/masonry__widget__kurbo__tests__kurbo_shape_circle.png b/masonry/src/widget/screenshots/masonry__widget__kurbo__tests__kurbo_shape_circle.png new file mode 100644 index 000000000..72057cb16 --- /dev/null +++ b/masonry/src/widget/screenshots/masonry__widget__kurbo__tests__kurbo_shape_circle.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fd30d3d36b9ad1de1429d7210842f7ee71fd671039b6e671a234c2aba504f2d1 +size 6677 diff --git a/masonry/src/widget/widget_pod.rs b/masonry/src/widget/widget_pod.rs index 24ad7dfa7..9db9cb221 100644 --- a/masonry/src/widget/widget_pod.rs +++ b/masonry/src/widget/widget_pod.rs @@ -57,6 +57,39 @@ impl WidgetPod { pub fn id(&self) -> WidgetId { self.id } + + /// Take the inner widget, if it has not been inserted yet. + /// + /// Never call it outside of `Widget::lyfecycle` or `View::build` + pub fn inner(self) -> Option { + if let WidgetPodInner::Created(w) = self.inner { + Some(w) + } else { + None + } + } + + /// Get access to the inner widget, if it has not been inserted yet. + /// + /// Never call it outside of `Widget::lyfecycle` or `View::build` + pub fn as_ref(&self) -> Option<&W> { + if let WidgetPodInner::Created(w) = &self.inner { + Some(w) + } else { + None + } + } + + /// Get access to the inner widget, if it has not been inserted yet. + /// + /// Never call it outside of `Widget::lyfecycle` or `View::build` + pub fn as_mut(&mut self) -> Option<&mut W> { + if let WidgetPodInner::Created(w) = &mut self.inner { + Some(w) + } else { + None + } + } } impl WidgetPod { diff --git a/xilem/Cargo.toml b/xilem/Cargo.toml index 28a599c76..df6ec5996 100644 --- a/xilem/Cargo.toml +++ b/xilem/Cargo.toml @@ -76,7 +76,7 @@ crate-type = ["cdylib"] workspace = true [dependencies] -xilem_core.workspace = true +xilem_core = { workspace = true, features = ["kurbo"] } masonry.workspace = true winit.workspace = true tracing.workspace = true diff --git a/xilem/examples/board.rs b/xilem/examples/board.rs new file mode 100644 index 000000000..e7b321c2b --- /dev/null +++ b/xilem/examples/board.rs @@ -0,0 +1,78 @@ +// Copyright 2024 the Xilem Authors +// SPDX-License-Identifier: Apache-2.0 + +use masonry::{ + widget::{CrossAxisAlignment, MainAxisAlignment}, + Size, +}; +use winit::error::EventLoopError; +use xilem::view::{ + board, button, flex, label, Axis, BoardExt, BoardParams, FlexExt as _, FlexSpacer, GraphicsExt, +}; +use xilem::{Color, EventLoop, WidgetView, Xilem}; + +struct AppState { + buttons: Vec, + clicked: Option, +} + +impl AppState { + fn view(&mut self) -> impl WidgetView { + flex(( + FlexSpacer::Fixed(30.0), + flex(( + button("B", |state: &mut AppState| state.buttons.push(true)), + button("C", |state: &mut AppState| state.buttons.push(false)), + button("-", |state: &mut AppState| { + state.buttons.pop(); + state.clicked = None; + }), + label(self.clicked.map_or_else( + || String::from("Nothing has been clicked."), + |i| format!("Button {i} has been clicked."), + )), + )) + .direction(Axis::Horizontal), + FlexSpacer::Fixed(10.0), + board( + self.buttons + .iter() + .copied() + .enumerate() + .map(|(i, is_button)| { + let origin = i as f64 * 15. + 10.; + let size = Size::new(30., 30.); + if is_button { + button(i.to_string(), move |state: &mut AppState| { + state.clicked = Some(i); + }) + .positioned(BoardParams::new((origin, origin), size)) + .into_any_board() + } else { + vello::kurbo::Circle::new((origin + 15., origin + 15.), 15.) + .fill(Color::NAVY) + .stroke(Color::PAPAYA_WHIP, vello::kurbo::Stroke::new(2.)) + .into_any_board() + } + }) + .collect::>(), + ) + .flex(1.), + )) + .direction(Axis::Vertical) + .cross_axis_alignment(CrossAxisAlignment::Center) + .main_axis_alignment(MainAxisAlignment::Center) + } +} + +fn main() -> Result<(), EventLoopError> { + let app = Xilem::new( + AppState { + buttons: Vec::new(), + clicked: None, + }, + AppState::view, + ); + app.run_windowed(EventLoop::with_user_event(), "Board".into())?; + Ok(()) +} diff --git a/xilem/src/view/board.rs b/xilem/src/view/board.rs new file mode 100644 index 000000000..0d6ace3b4 --- /dev/null +++ b/xilem/src/view/board.rs @@ -0,0 +1,384 @@ +// Copyright 2024 the Xilem Authors +// SPDX-License-Identifier: Apache-2.0 + +use std::marker::PhantomData; + +use masonry::{ + widget::{self, KurboShape, PositionedElement, SvgElement, WidgetMut}, + Widget, +}; +use xilem_core::{ + AnyElement, AnyView, AppendVec, DynMessage, ElementSplice, MessageResult, Mut, SuperElement, + View, ViewElement, ViewMarker, ViewSequence, +}; + +use crate::{Pod, ViewCtx, WidgetView}; + +pub use masonry::widget::BoardParams; + +mod kurbo_shape; +mod style_modifier; + +pub use kurbo_shape::GraphicsView; +pub use style_modifier::{fill, stroke, transform, Fill, GraphicsExt, Stroke, Transform}; + +pub fn board>( + sequence: Seq, +) -> Board { + Board { + sequence, + phantom: PhantomData, + } +} + +pub struct Board { + sequence: Seq, + phantom: PhantomData (State, Action)>, +} + +impl ViewMarker for Board {} +impl View for Board +where + State: 'static, + Action: 'static, + Seq: BoardSequence, +{ + type Element = Pod; + + type ViewState = Seq::SeqState; + + fn build(&self, ctx: &mut ViewCtx) -> (Self::Element, Self::ViewState) { + let mut elements = AppendVec::default(); + let mut widget = widget::Board::new(); + let seq_state = self.sequence.seq_build(ctx, &mut elements); + for BoardElement { element } in elements.into_inner() { + widget = widget.with_child_pod(element.inner); + } + (Pod::new(widget), seq_state) + } + + fn rebuild<'el>( + &self, + prev: &Self, + view_state: &mut Self::ViewState, + ctx: &mut ViewCtx, + element: Mut<'el, Self::Element>, + ) -> Mut<'el, Self::Element> { + // TODO: Re-use scratch space? + let mut splice = BoardSplice::new(element); + self.sequence + .seq_rebuild(&prev.sequence, view_state, ctx, &mut splice); + debug_assert!(splice.scratch.is_empty()); + splice.element + } + + fn teardown( + &self, + view_state: &mut Self::ViewState, + ctx: &mut ViewCtx, + element: Mut<'_, Self::Element>, + ) { + let mut splice = BoardSplice::new(element); + self.sequence.seq_teardown(view_state, ctx, &mut splice); + debug_assert!(splice.scratch.into_inner().is_empty()); + } + + fn message( + &self, + view_state: &mut Self::ViewState, + id_path: &[xilem_core::ViewId], + message: DynMessage, + app_state: &mut State, + ) -> MessageResult { + self.sequence + .seq_message(view_state, id_path, message, app_state) + } +} + +pub type AnyBoardView = + dyn AnyView + Send + Sync; + +pub struct BoardElement { + element: Pod>, +} + +pub struct BoardElementMut<'w> { + parent: WidgetMut<'w, widget::Board>, + idx: usize, +} + +struct BoardSplice<'w> { + idx: usize, + element: WidgetMut<'w, widget::Board>, + scratch: AppendVec, +} + +impl<'w> BoardSplice<'w> { + fn new(element: WidgetMut<'w, widget::Board>) -> Self { + Self { + idx: 0, + element, + scratch: AppendVec::default(), + } + } +} + +impl ViewElement for BoardElement { + type Mut<'w> = BoardElementMut<'w>; +} + +impl SuperElement for BoardElement { + fn upcast(child: BoardElement) -> Self { + child + } + + fn with_downcast_val( + mut this: Mut<'_, Self>, + f: impl FnOnce(Mut<'_, BoardElement>) -> R, + ) -> (Self::Mut<'_>, R) { + let r = { + let parent = this.parent.reborrow_mut(); + let reborrow = BoardElementMut { + idx: this.idx, + parent, + }; + f(reborrow) + }; + (this, r) + } +} + +impl AnyElement for BoardElement { + fn replace_inner(mut this: Self::Mut<'_>, child: BoardElement) -> Self::Mut<'_> { + this.parent.remove_child(this.idx); + this.parent.insert_child(this.idx, child.element.inner); + this + } +} + +impl SuperElement>> for BoardElement { + fn upcast(child: Pod>) -> Self { + BoardElement { + element: child.inner.svg_boxed().into(), + } + } + + fn with_downcast_val( + mut this: Mut<'_, Self>, + f: impl FnOnce(Mut<'_, Pod>>) -> R, + ) -> (Self::Mut<'_>, R) { + let r = { + let mut child = this.parent.child_mut(this.idx); + f(child.downcast()) + }; + (this, r) + } +} + +impl AnyElement>> for BoardElement { + fn replace_inner(mut this: Self::Mut<'_>, child: Pod>) -> Self::Mut<'_> { + this.parent.remove_child(this.idx); + this.parent.insert_child(this.idx, child.inner.svg_boxed()); + this + } +} + +impl SuperElement> for BoardElement { + fn upcast(child: Pod) -> Self { + BoardElement { + element: child.inner.svg_boxed().into(), + } + } + + fn with_downcast_val( + mut this: Mut<'_, Self>, + f: impl FnOnce(Mut<'_, Pod>) -> R, + ) -> (Self::Mut<'_>, R) { + let r = { + let mut child = this.parent.child_mut(this.idx); + f(child.downcast()) + }; + (this, r) + } +} + +impl AnyElement> for BoardElement { + fn replace_inner(mut this: Self::Mut<'_>, child: Pod) -> Self::Mut<'_> { + this.parent.remove_child(this.idx); + this.parent.insert_child(this.idx, child.inner.svg_boxed()); + this + } +} + +impl ElementSplice for BoardSplice<'_> { + fn insert(&mut self, BoardElement { element }: BoardElement) { + self.element.insert_child(self.idx, element.inner); + self.idx += 1; + } + + fn with_scratch(&mut self, f: impl FnOnce(&mut AppendVec) -> R) -> R { + let ret = f(&mut self.scratch); + for BoardElement { element } in self.scratch.drain() { + self.element.insert_child(self.idx, element.inner); + self.idx += 1; + } + ret + } + + fn mutate(&mut self, f: impl FnOnce(Mut<'_, BoardElement>) -> R) -> R { + let child = BoardElementMut { + parent: self.element.reborrow_mut(), + idx: self.idx, + }; + let ret = f(child); + self.idx += 1; + ret + } + + fn delete(&mut self, f: impl FnOnce(Mut<'_, BoardElement>) -> R) -> R { + let ret = { + let child = BoardElementMut { + parent: self.element.reborrow_mut(), + idx: self.idx, + }; + f(child) + }; + self.element.remove_child(self.idx); + ret + } + + fn skip(&mut self, n: usize) { + self.idx += n; + } +} + +/// An ordered sequence of views for a [`Board`] view. +/// See [`ViewSequence`] for more technical details. +pub trait BoardSequence: + ViewSequence +{ +} + +impl BoardSequence for Seq where + Seq: ViewSequence +{ +} + +/// A trait which extends a [`WidgetView`] with methods to provide parameters for a positioned item, +/// or being able to use it interchangeably with a shape. +pub trait BoardExt: WidgetView { + /// Makes this view absolutely positioned in a `Board`. + fn positioned(self, params: impl Into) -> PositionedView + where + State: 'static, + Action: 'static, + Self: Sized, + { + positioned(self, params) + } +} + +impl> BoardExt for V {} + +/// A [`WidgetView`] that can be used within a [`Board`] [`View`] +pub struct PositionedView { + view: V, + params: BoardParams, + phantom: PhantomData (State, Action)>, +} + +/// Makes this view absolutely positioned in a [`Board`]. +pub fn positioned( + view: V, + params: impl Into, +) -> PositionedView +where + State: 'static, + Action: 'static, + V: WidgetView, +{ + PositionedView { + view, + params: params.into(), + phantom: PhantomData, + } +} + +impl From> for Box> +where + State: 'static, + Action: 'static, + V: WidgetView, +{ + fn from(view: PositionedView) -> Self { + Box::new(positioned(view.view, view.params)) + } +} + +impl PositionedView +where + State: 'static, + Action: 'static, + V: WidgetView, +{ + /// Turns this [`PositionedView`] into a boxed [`AnyBoardView`]. + pub fn into_any_board(self) -> Box> { + self.into() + } +} + +impl ViewMarker for PositionedView {} +impl View for PositionedView +where + State: 'static, + Action: 'static, + V: WidgetView, +{ + type Element = Pod>; + + type ViewState = V::ViewState; + + fn build(&self, ctx: &mut ViewCtx) -> (Self::Element, Self::ViewState) { + let (pod, state) = self.view.build(ctx); + (pod.inner.positioned(self.params).into(), state) + } + + fn rebuild<'el>( + &self, + prev: &Self, + view_state: &mut Self::ViewState, + ctx: &mut ViewCtx, + mut element: Mut<'el, Self::Element>, + ) -> Mut<'el, Self::Element> { + self.view + .rebuild(&prev.view, view_state, ctx, element.inner_mut()); + if self.params.origin != prev.params.origin { + element.widget.set_origin(self.params.origin); + element.ctx.request_layout(); + } + if self.params.size != prev.params.size { + element.widget.set_size(self.params.size); + element.ctx.request_layout(); + } + element + } + + fn teardown( + &self, + view_state: &mut Self::ViewState, + ctx: &mut ViewCtx, + mut element: Mut<'_, Self::Element>, + ) { + self.view.teardown(view_state, ctx, element.inner_mut()); + } + + fn message( + &self, + view_state: &mut Self::ViewState, + id_path: &[xilem_core::ViewId], + message: DynMessage, + app_state: &mut State, + ) -> MessageResult { + self.view.message(view_state, id_path, message, app_state) + } +} diff --git a/xilem/src/view/board/kurbo_shape.rs b/xilem/src/view/board/kurbo_shape.rs new file mode 100644 index 000000000..7bed6a332 --- /dev/null +++ b/xilem/src/view/board/kurbo_shape.rs @@ -0,0 +1,79 @@ +// Copyright 2024 the Xilem Authors +// SPDX-License-Identifier: Apache-2.0 + +//! Implementation of the View trait for various kurbo shapes. + +use masonry::widget::{KurboShape, SvgElement}; +use vello::kurbo; +use xilem_core::{DynMessage, MessageResult, Mut, OrphanView}; + +use crate::{Pod, ViewCtx, WidgetView}; + +pub trait GraphicsView: WidgetView {} + +impl GraphicsView for V where + V: WidgetView + Send + Sync +{ +} + +macro_rules! impl_orphan_view { + ($t:ident) => { + impl OrphanView + for ViewCtx + { + type OrphanViewState = (); + type OrphanElement = Pod; + + fn orphan_build( + view: &kurbo::$t, + _ctx: &mut ViewCtx, + ) -> (Self::OrphanElement, Self::OrphanViewState) { + (Pod::new(KurboShape::new(view.clone())), ()) + } + + fn orphan_rebuild<'el>( + new: &kurbo::$t, + prev: &kurbo::$t, + (): &mut Self::OrphanViewState, + ctx: &mut ViewCtx, + mut element: Mut<'el, Self::OrphanElement>, + ) -> Mut<'el, Self::OrphanElement> { + if new != prev { + element.set_shape(new.clone().into()); + ctx.mark_changed(); + } + element + } + + fn orphan_teardown( + _view: &kurbo::$t, + (): &mut Self::OrphanViewState, + _ctx: &mut ViewCtx, + _element: Mut<'_, Self::OrphanElement>, + ) { + } + + fn orphan_message( + _view: &kurbo::$t, + (): &mut Self::OrphanViewState, + _id_path: &[xilem_core::ViewId], + message: DynMessage, + _app_state: &mut State, + ) -> MessageResult { + MessageResult::Stale(message) + } + } + }; +} + +impl_orphan_view!(PathSeg); +impl_orphan_view!(Arc); +impl_orphan_view!(BezPath); +impl_orphan_view!(Circle); +impl_orphan_view!(CircleSegment); +impl_orphan_view!(CubicBez); +impl_orphan_view!(Ellipse); +impl_orphan_view!(Line); +impl_orphan_view!(QuadBez); +impl_orphan_view!(Rect); +impl_orphan_view!(RoundedRect); diff --git a/xilem/src/view/board/style_modifier.rs b/xilem/src/view/board/style_modifier.rs new file mode 100644 index 000000000..50364911c --- /dev/null +++ b/xilem/src/view/board/style_modifier.rs @@ -0,0 +1,304 @@ +// Copyright 2024 the Xilem Authors +// SPDX-License-Identifier: Apache-2.0 + +use std::marker::PhantomData; + +use masonry::{widget::KurboShape, Affine}; +use vello::{ + kurbo, + peniko::{self, Brush}, +}; +use xilem_core::{AnyView, DynMessage, MessageResult, Mut, View, ViewId, ViewMarker}; + +use crate::{Pod, ViewCtx}; + +use super::{AnyBoardView, BoardElement, GraphicsView}; + +pub struct Transform { + child: V, + transform: Affine, + phantom: PhantomData (State, Action)>, +} + +pub struct Fill { + child: V, + mode: peniko::Fill, + brush: Brush, + brush_transform: Option, + phantom: PhantomData (State, Action)>, +} + +pub struct Stroke { + child: V, + style: kurbo::Stroke, + brush: Brush, + brush_transform: Option, + phantom: PhantomData (State, Action)>, +} + +pub fn transform(child: V, transform: Affine) -> Transform { + Transform { + child, + transform, + phantom: PhantomData, + } +} + +pub fn fill(child: V, brush: impl Into) -> Fill { + Fill { + child, + mode: peniko::Fill::NonZero, + brush: brush.into(), + brush_transform: None, + phantom: PhantomData, + } +} + +pub fn stroke( + child: V, + brush: impl Into, + style: kurbo::Stroke, +) -> Stroke { + Stroke { + child, + style, + brush: brush.into(), + brush_transform: None, + phantom: PhantomData, + } +} + +impl Fill { + pub fn mode(mut self, mode: peniko::Fill) -> Self { + self.mode = mode; + self + } + + pub fn brush_transform(mut self, brush_transform: Affine) -> Self { + self.brush_transform = Some(brush_transform); + self + } +} + +impl Stroke { + pub fn brush_transform(mut self, brush_transform: Affine) -> Self { + self.brush_transform = Some(brush_transform); + self + } +} + +pub trait GraphicsExt: GraphicsView + Sized { + fn transform(self, affine: Affine) -> Transform { + transform(self, affine) + } + + fn fill(self, brush: impl Into) -> Fill { + fill(self, brush) + } + + fn stroke(self, brush: impl Into, style: kurbo::Stroke) -> Stroke { + stroke(self, brush, style) + } + + fn into_any_board(self) -> Box> + where + Self: AnyView + Send + Sync + 'static, + { + Box::new(self) + } +} + +impl> GraphicsExt + for V +{ +} + +impl ViewMarker for Transform {} +impl View for Transform +where + State: 'static, + Action: 'static, + V: GraphicsView, +{ + type ViewState = V::ViewState; + type Element = Pod; + + fn build(&self, ctx: &mut ViewCtx) -> (Self::Element, Self::ViewState) { + let (mut element, state) = self.child.build(ctx); + element + .inner + .as_mut() + .unwrap() + .set_transform(self.transform); + (element, state) + } + + fn rebuild<'el>( + &self, + prev: &Self, + child_state: &mut Self::ViewState, + ctx: &mut ViewCtx, + element: Mut<'el, Self::Element>, + ) -> Mut<'el, Self::Element> { + let mut element = self.child.rebuild(&prev.child, child_state, ctx, element); + if self.transform != prev.transform { + element.set_transform(self.transform); + ctx.mark_changed(); + } + element + } + + fn teardown( + &self, + child_state: &mut Self::ViewState, + ctx: &mut ViewCtx, + element: Mut<'_, Self::Element>, + ) { + self.child.teardown(child_state, ctx, element); + } + + fn message( + &self, + child_state: &mut Self::ViewState, + id_path: &[ViewId], + message: DynMessage, + app_state: &mut State, + ) -> MessageResult { + self.child.message(child_state, id_path, message, app_state) + } +} + +impl ViewMarker for Fill {} +impl View for Fill +where + State: 'static, + Action: 'static, + V: GraphicsView, +{ + type ViewState = V::ViewState; + type Element = Pod; + + fn build(&self, ctx: &mut ViewCtx) -> (Self::Element, Self::ViewState) { + let (mut element, state) = self.child.build(ctx); + let element_mut = element.inner.as_mut().unwrap(); + element_mut.set_fill_mode(self.mode); + element_mut.set_fill_brush(self.brush.clone()); + element_mut.set_fill_brush_transform(self.brush_transform); + (element, state) + } + + fn rebuild<'el>( + &self, + prev: &Self, + child_state: &mut Self::ViewState, + ctx: &mut ViewCtx, + element: Mut<'el, Self::Element>, + ) -> Mut<'el, Self::Element> { + let mut element = self.child.rebuild(&prev.child, child_state, ctx, element); + { + if self.mode != prev.mode { + element.set_fill_mode(self.mode); + ctx.mark_changed(); + } + if self.brush != prev.brush { + element.set_fill_brush(self.brush.clone()); + ctx.mark_changed(); + } + if self.brush_transform != prev.brush_transform { + element.set_fill_brush_transform(self.brush_transform); + ctx.mark_changed(); + } + } + element + } + + fn teardown( + &self, + child_state: &mut Self::ViewState, + ctx: &mut ViewCtx, + element: Mut<'_, Self::Element>, + ) { + self.child.teardown(child_state, ctx, element); + } + + fn message( + &self, + child_state: &mut Self::ViewState, + id_path: &[ViewId], + message: DynMessage, + app_state: &mut State, + ) -> MessageResult { + self.child.message(child_state, id_path, message, app_state) + } +} + +impl ViewMarker for Stroke {} +impl View for Stroke +where + State: 'static, + Action: 'static, + V: GraphicsView, +{ + type ViewState = V::ViewState; + type Element = Pod; + + fn build(&self, ctx: &mut ViewCtx) -> (Self::Element, Self::ViewState) { + let (mut element, state) = self.child.build(ctx); + let element_mut = element.inner.as_mut().unwrap(); + element_mut.set_stroke_style(self.style.clone()); + element_mut.set_stroke_brush(self.brush.clone()); + element_mut.set_stroke_brush_transform(self.brush_transform); + (element, state) + } + + fn rebuild<'el>( + &self, + prev: &Self, + child_state: &mut Self::ViewState, + ctx: &mut ViewCtx, + element: Mut<'el, Self::Element>, + ) -> Mut<'el, Self::Element> { + let mut element = self.child.rebuild(&prev.child, child_state, ctx, element); + { + if self.style.width != prev.style.width + || self.style.join != prev.style.join + || self.style.miter_limit != prev.style.miter_limit + || self.style.start_cap != prev.style.start_cap + || self.style.end_cap != prev.style.end_cap + || self.style.dash_pattern != prev.style.dash_pattern + || self.style.dash_offset != prev.style.dash_offset + { + element.set_stroke_style(self.style.clone()); + ctx.mark_changed(); + } + if self.brush != prev.brush { + element.set_stroke_brush(self.brush.clone()); + ctx.mark_changed(); + } + if self.brush_transform != prev.brush_transform { + element.set_stroke_brush_transform(self.brush_transform); + ctx.mark_changed(); + } + } + element + } + + fn teardown( + &self, + child_state: &mut Self::ViewState, + ctx: &mut ViewCtx, + element: Mut<'_, Self::Element>, + ) { + self.child.teardown(child_state, ctx, element); + } + + fn message( + &self, + child_state: &mut Self::ViewState, + id_path: &[ViewId], + message: DynMessage, + app_state: &mut State, + ) -> MessageResult { + self.child.message(child_state, id_path, message, app_state) + } +} diff --git a/xilem/src/view/mod.rs b/xilem/src/view/mod.rs index b879de45f..b1c548fb8 100644 --- a/xilem/src/view/mod.rs +++ b/xilem/src/view/mod.rs @@ -9,6 +9,9 @@ pub use task::*; mod worker; pub use worker::*; +mod board; +pub use board::*; + mod button; pub use button::*;