mirror of https://github.com/linebender/xilem
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:
parent
2f23ee117a
commit
a52c3c7b3c
|
@ -8,6 +8,8 @@ use xilem_core::{MessageResult, Mut, View, ViewId, ViewMarker, ViewPathTracker};
|
||||||
|
|
||||||
use crate::{DynMessage, ElementAsRef, OptionalAction, ViewCtx};
|
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);
|
const ON_EVENT_VIEW_ID: ViewId = ViewId::new(0x2357_1113);
|
||||||
|
|
||||||
/// Wraps a [`View`] `V` and attaches an event listener.
|
/// Wraps a [`View`] `V` and attaches an event listener.
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
// Copyright 2023 the Xilem Authors
|
// Copyright 2023 the Xilem Authors
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
use std::borrow::Cow;
|
|
||||||
use std::marker::PhantomData;
|
use std::marker::PhantomData;
|
||||||
|
use std::{borrow::Cow, fmt::Write as _};
|
||||||
|
|
||||||
use peniko::Brush;
|
use peniko::Brush;
|
||||||
use xilem_core::{MessageResult, Mut, View, ViewId, ViewMarker};
|
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 {
|
fn brush_to_string(brush: &Brush) -> String {
|
||||||
match brush {
|
match brush {
|
||||||
Brush::Solid(color) => {
|
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<V, State, Action> ViewMarker for Fill<V, State, Action> {}
|
||||||
impl<State, Action, V> View<State, Action, ViewCtx, DynMessage> for Fill<V, State, Action>
|
impl<State, Action, V> View<State, Action, ViewCtx, DynMessage> for Fill<V, State, Action>
|
||||||
where
|
where
|
||||||
|
@ -76,6 +102,7 @@ where
|
||||||
let brush_svg_repr = Cow::from(brush_to_string(&self.brush));
|
let brush_svg_repr = Cow::from(brush_to_string(&self.brush));
|
||||||
element.start_attribute_modifier();
|
element.start_attribute_modifier();
|
||||||
element.set_attribute("fill".into(), brush_svg_repr.clone().into_attr_value());
|
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.end_attribute_modifier();
|
||||||
(element, (brush_svg_repr, child_state))
|
(element, (brush_svg_repr, child_state))
|
||||||
}
|
}
|
||||||
|
@ -93,6 +120,7 @@ where
|
||||||
*brush_svg_repr = Cow::from(brush_to_string(&self.brush));
|
*brush_svg_repr = Cow::from(brush_to_string(&self.brush));
|
||||||
}
|
}
|
||||||
element.set_attribute("fill".into(), brush_svg_repr.clone().into_attr_value());
|
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.end_attribute_modifier();
|
||||||
element
|
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<V, State, Action> ViewMarker for Stroke<V, State, Action> {}
|
||||||
impl<State, Action, V> View<State, Action, ViewCtx, DynMessage> for Stroke<V, State, Action>
|
impl<State, Action, V> View<State, Action, ViewCtx, DynMessage> for Stroke<V, State, Action>
|
||||||
where
|
where
|
||||||
|
@ -124,33 +158,66 @@ where
|
||||||
Action: 'static,
|
Action: 'static,
|
||||||
V: View<State, Action, ViewCtx, DynMessage, Element: ElementWithAttributes>,
|
V: View<State, Action, ViewCtx, DynMessage, Element: ElementWithAttributes>,
|
||||||
{
|
{
|
||||||
type ViewState = (Cow<'static, str>, V::ViewState);
|
type ViewState = StrokeState<V::ViewState>;
|
||||||
type Element = V::Element;
|
type Element = V::Element;
|
||||||
|
|
||||||
fn build(&self, ctx: &mut ViewCtx) -> (Self::Element, Self::ViewState) {
|
fn build(&self, ctx: &mut ViewCtx) -> (Self::Element, Self::ViewState) {
|
||||||
let (mut element, child_state) = self.child.build(ctx);
|
let (mut element, child_state) = self.child.build(ctx);
|
||||||
let brush_svg_repr = Cow::from(brush_to_string(&self.brush));
|
|
||||||
element.start_attribute_modifier();
|
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());
|
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());
|
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.end_attribute_modifier();
|
||||||
(element, (brush_svg_repr, child_state))
|
(
|
||||||
|
element,
|
||||||
|
StrokeState {
|
||||||
|
brush_svg_repr,
|
||||||
|
stroke_dash_pattern_svg_repr,
|
||||||
|
child_state,
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn rebuild<'el>(
|
fn rebuild<'el>(
|
||||||
&self,
|
&self,
|
||||||
prev: &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,
|
ctx: &mut ViewCtx,
|
||||||
mut element: Mut<'el, Self::Element>,
|
mut element: Mut<'el, Self::Element>,
|
||||||
) -> Mut<'el, Self::Element> {
|
) -> Mut<'el, Self::Element> {
|
||||||
element.start_attribute_modifier();
|
element.start_attribute_modifier();
|
||||||
|
|
||||||
let mut element = self.child.rebuild(&prev.child, child_state, ctx, element);
|
let mut element = self.child.rebuild(&prev.child, child_state, ctx, element);
|
||||||
|
|
||||||
if self.brush != prev.brush {
|
if self.brush != prev.brush {
|
||||||
*brush_svg_repr = Cow::from(brush_to_string(&self.brush));
|
*brush_svg_repr = Cow::from(brush_to_string(&self.brush));
|
||||||
}
|
}
|
||||||
element.set_attribute("stroke".into(), brush_svg_repr.clone().into_attr_value());
|
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());
|
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.end_attribute_modifier();
|
||||||
element
|
element
|
||||||
}
|
}
|
||||||
|
@ -161,16 +228,18 @@ where
|
||||||
ctx: &mut ViewCtx,
|
ctx: &mut ViewCtx,
|
||||||
element: Mut<'_, Self::Element>,
|
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(
|
fn message(
|
||||||
&self,
|
&self,
|
||||||
(_, child_state): &mut Self::ViewState,
|
view_state: &mut Self::ViewState,
|
||||||
id_path: &[ViewId],
|
id_path: &[ViewId],
|
||||||
message: DynMessage,
|
message: DynMessage,
|
||||||
app_state: &mut State,
|
app_state: &mut State,
|
||||||
) -> MessageResult<Action, DynMessage> {
|
) -> 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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -66,12 +66,16 @@ fn app_logic(state: &mut AppState) -> impl DomView<AppState> {
|
||||||
.stroke(Color::BLUE, Default::default()),
|
.stroke(Color::BLUE, Default::default()),
|
||||||
Rect::new(320.0, 100.0, 420.0, 200.0).class("red"),
|
Rect::new(320.0, 100.0, 420.0, 200.0).class("red"),
|
||||||
Rect::new(state.x, state.y, state.x + 100., state.y + 100.)
|
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)),
|
.pointer(|s: &mut AppState, msg| s.grab.handle(&mut s.x, &mut s.y, &msg)),
|
||||||
g(v),
|
g(v),
|
||||||
Rect::new(210.0, 210.0, 310.0, 310.0).pointer(|_, e| {
|
Rect::new(210.0, 210.0, 310.0, 310.0).pointer(|_, e| {
|
||||||
web_sys::console::log_1(&format!("pointer event {e:?}").into());
|
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(|_, _| {
|
kurbo::Circle::new((460.0, 260.0), 45.0).on_click(|_, _| {
|
||||||
web_sys::console::log_1(&"circle clicked".into());
|
web_sys::console::log_1(&"circle clicked".into());
|
||||||
}),
|
}),
|
||||||
|
|
Loading…
Reference in New Issue