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).
This commit is contained in:
Philipp Mildenberger 2024-08-09 11:28:52 +02:00 committed by GitHub
parent 2f23ee117a
commit a52c3c7b3c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 84 additions and 9 deletions

View File

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

View File

@ -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<State, Action, V>(
}
}
/// Rather general join string function, might be reused somewhere else as well...
fn join(iter: &mut impl Iterator<Item: std::fmt::Display>, 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<V, State, Action> ViewMarker for Fill<V, State, Action> {}
impl<State, Action, V> View<State, Action, ViewCtx, DynMessage> for Fill<V, State, Action>
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<ChildState> {
brush_svg_repr: Cow<'static, str>,
stroke_dash_pattern_svg_repr: Option<Cow<'static, str>>,
child_state: ChildState,
}
impl<V, State, Action> ViewMarker for Stroke<V, State, Action> {}
impl<State, Action, V> View<State, Action, ViewCtx, DynMessage> for Stroke<V, State, Action>
where
@ -124,33 +158,66 @@ where
Action: 'static,
V: View<State, Action, ViewCtx, DynMessage, Element: ElementWithAttributes>,
{
type ViewState = (Cow<'static, str>, V::ViewState);
type ViewState = StrokeState<V::ViewState>;
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<Action, DynMessage> {
self.child.message(child_state, id_path, message, app_state)
self.child
.message(&mut view_state.child_state, id_path, message, app_state)
}
}

View File

@ -66,12 +66,16 @@ fn app_logic(state: &mut AppState) -> impl DomView<AppState> {
.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());
}),