From a52c3c7b3c1b31f3205bada4f88c70b8297b21a9 Mon Sep 17 00:00:00 2001 From: Philipp Mildenberger Date: Fri, 9 Aug 2024 11:28:52 +0200 Subject: [PATCH] xilem_web: Add support for opacity in `svg::Fill` and dashes, opacity in `svg::Stroke` (#493) Uses `ViewState` for a little bit of optimization (of allocations mostly). --- xilem_web/src/events.rs | 2 + xilem_web/src/svg/common_attrs.rs | 85 ++++++++++++++++++++--- xilem_web/web_examples/svgtoy/src/main.rs | 6 +- 3 files changed, 84 insertions(+), 9 deletions(-) diff --git a/xilem_web/src/events.rs b/xilem_web/src/events.rs index b3706804..0f0f1525 100644 --- a/xilem_web/src/events.rs +++ b/xilem_web/src/events.rs @@ -8,6 +8,8 @@ use xilem_core::{MessageResult, Mut, View, ViewId, ViewMarker, ViewPathTracker}; use crate::{DynMessage, ElementAsRef, OptionalAction, ViewCtx}; +/// Use a distinctive number here, to be able to catch bugs. +/// In case the generational-id view path in `View::Message` lead to a wrong view const ON_EVENT_VIEW_ID: ViewId = ViewId::new(0x2357_1113); /// Wraps a [`View`] `V` and attaches an event listener. diff --git a/xilem_web/src/svg/common_attrs.rs b/xilem_web/src/svg/common_attrs.rs index 472446b8..cae9edd5 100644 --- a/xilem_web/src/svg/common_attrs.rs +++ b/xilem_web/src/svg/common_attrs.rs @@ -1,8 +1,8 @@ // Copyright 2023 the Xilem Authors // SPDX-License-Identifier: Apache-2.0 -use std::borrow::Cow; use std::marker::PhantomData; +use std::{borrow::Cow, fmt::Write as _}; use peniko::Brush; use xilem_core::{MessageResult, Mut, View, ViewId, ViewMarker}; @@ -48,6 +48,24 @@ pub fn stroke( } } +/// Rather general join string function, might be reused somewhere else as well... +fn join(iter: &mut impl Iterator, sep: &str) -> String { + match iter.next() { + None => String::new(), + Some(first_elt) => { + // estimate lower bound of capacity needed + let (lower, _) = iter.size_hint(); + let mut result = String::with_capacity(sep.len() * lower); + write!(&mut result, "{}", first_elt).unwrap(); + iter.for_each(|elt| { + result.push_str(sep); + write!(&mut result, "{}", elt).unwrap(); + }); + result + } + } +} + fn brush_to_string(brush: &Brush) -> String { match brush { Brush::Solid(color) => { @@ -61,6 +79,14 @@ fn brush_to_string(brush: &Brush) -> String { } } +fn add_opacity_to_element(brush: &Brush, element: &mut impl WithAttributes, attr: &'static str) { + let opacity = match brush { + Brush::Solid(color) if color.a != u8::MAX => Some(color.a as f64 / 255.0), + _ => None, + }; + element.set_attribute(attr.into(), opacity.into_attr_value()); +} + impl ViewMarker for Fill {} impl View for Fill where @@ -76,6 +102,7 @@ where let brush_svg_repr = Cow::from(brush_to_string(&self.brush)); element.start_attribute_modifier(); element.set_attribute("fill".into(), brush_svg_repr.clone().into_attr_value()); + add_opacity_to_element(&self.brush, &mut element, "fill-opacity"); element.end_attribute_modifier(); (element, (brush_svg_repr, child_state)) } @@ -93,6 +120,7 @@ where *brush_svg_repr = Cow::from(brush_to_string(&self.brush)); } element.set_attribute("fill".into(), brush_svg_repr.clone().into_attr_value()); + add_opacity_to_element(&self.brush, &mut element, "fill-opacity"); element.end_attribute_modifier(); element } @@ -117,6 +145,12 @@ where } } +pub struct StrokeState { + brush_svg_repr: Cow<'static, str>, + stroke_dash_pattern_svg_repr: Option>, + child_state: ChildState, +} + impl ViewMarker for Stroke {} impl View for Stroke where @@ -124,33 +158,66 @@ where Action: 'static, V: View, { - type ViewState = (Cow<'static, str>, V::ViewState); + type ViewState = StrokeState; type Element = V::Element; fn build(&self, ctx: &mut ViewCtx) -> (Self::Element, Self::ViewState) { let (mut element, child_state) = self.child.build(ctx); - let brush_svg_repr = Cow::from(brush_to_string(&self.brush)); + element.start_attribute_modifier(); + + let brush_svg_repr = Cow::from(brush_to_string(&self.brush)); element.set_attribute("stroke".into(), brush_svg_repr.clone().into_attr_value()); + let stroke_dash_pattern_svg_repr = (!self.style.dash_pattern.is_empty()) + .then(|| Cow::from(join(&mut self.style.dash_pattern.iter(), " "))); + let dash_pattern = stroke_dash_pattern_svg_repr.clone().into_attr_value(); + element.set_attribute("stroke-dasharray".into(), dash_pattern); + let dash_offset = (self.style.dash_offset != 0.0).then_some(self.style.dash_offset); + element.set_attribute("stroke-dashoffset".into(), dash_offset.into_attr_value()); element.set_attribute("stroke-width".into(), self.style.width.into_attr_value()); + add_opacity_to_element(&self.brush, &mut element, "stroke-opacity"); + element.end_attribute_modifier(); - (element, (brush_svg_repr, child_state)) + ( + element, + StrokeState { + brush_svg_repr, + stroke_dash_pattern_svg_repr, + child_state, + }, + ) } fn rebuild<'el>( &self, prev: &Self, - (brush_svg_repr, child_state): &mut Self::ViewState, + StrokeState { + brush_svg_repr, + stroke_dash_pattern_svg_repr, + child_state, + }: &mut Self::ViewState, ctx: &mut ViewCtx, mut element: Mut<'el, Self::Element>, ) -> Mut<'el, Self::Element> { element.start_attribute_modifier(); + let mut element = self.child.rebuild(&prev.child, child_state, ctx, element); + if self.brush != prev.brush { *brush_svg_repr = Cow::from(brush_to_string(&self.brush)); } element.set_attribute("stroke".into(), brush_svg_repr.clone().into_attr_value()); + if self.style.dash_pattern != prev.style.dash_pattern { + *stroke_dash_pattern_svg_repr = (!self.style.dash_pattern.is_empty()) + .then(|| Cow::from(join(&mut self.style.dash_pattern.iter(), " "))); + } + let dash_pattern = stroke_dash_pattern_svg_repr.clone().into_attr_value(); + element.set_attribute("stroke-dasharray".into(), dash_pattern); + let dash_offset = (self.style.dash_offset != 0.0).then_some(self.style.dash_offset); + element.set_attribute("stroke-dashoffset".into(), dash_offset.into_attr_value()); element.set_attribute("stroke-width".into(), self.style.width.into_attr_value()); + add_opacity_to_element(&self.brush, &mut element, "stroke-opacity"); + element.end_attribute_modifier(); element } @@ -161,16 +228,18 @@ where ctx: &mut ViewCtx, element: Mut<'_, Self::Element>, ) { - self.child.teardown(&mut view_state.1, ctx, element); + self.child + .teardown(&mut view_state.child_state, ctx, element); } fn message( &self, - (_, child_state): &mut Self::ViewState, + view_state: &mut Self::ViewState, id_path: &[ViewId], message: DynMessage, app_state: &mut State, ) -> MessageResult { - self.child.message(child_state, id_path, message, app_state) + self.child + .message(&mut view_state.child_state, id_path, message, app_state) } } diff --git a/xilem_web/web_examples/svgtoy/src/main.rs b/xilem_web/web_examples/svgtoy/src/main.rs index 6b744651..9ca24f1a 100644 --- a/xilem_web/web_examples/svgtoy/src/main.rs +++ b/xilem_web/web_examples/svgtoy/src/main.rs @@ -66,12 +66,16 @@ fn app_logic(state: &mut AppState) -> impl DomView { .stroke(Color::BLUE, Default::default()), Rect::new(320.0, 100.0, 420.0, 200.0).class("red"), Rect::new(state.x, state.y, state.x + 100., state.y + 100.) + .fill(Color::rgba8(100, 100, 255, 100)) .pointer(|s: &mut AppState, msg| s.grab.handle(&mut s.x, &mut s.y, &msg)), g(v), Rect::new(210.0, 210.0, 310.0, 310.0).pointer(|_, e| { web_sys::console::log_1(&format!("pointer event {e:?}").into()); }), - kurbo::Line::new((310.0, 210.0), (410.0, 310.0)), + kurbo::Line::new((310.0, 210.0), (410.0, 310.0)).stroke( + Color::YELLOW_GREEN, + kurbo::Stroke::new(1.0).with_dashes(state.x, [7.0, 1.0]), + ), kurbo::Circle::new((460.0, 260.0), 45.0).on_click(|_, _| { web_sys::console::log_1(&"circle clicked".into()); }),