diff --git a/xilem_web/src/interfaces.rs b/xilem_web/src/interfaces.rs index 6da8d95c2..927c49abf 100644 --- a/xilem_web/src/interfaces.rs +++ b/xilem_web/src/interfaces.rs @@ -18,7 +18,7 @@ use crate::{ attribute::{Attr, WithAttributes}, class::{AsClassIter, Class, WithClasses}, events, - style::{IntoStyles, Style, WithStyle}, + style::{IntoStyles, Rotate, Scale, ScaleValue, Style, WithStyle}, DomNode, DomView, IntoAttributeValue, OptionalAction, Pointer, PointerMsg, }; use wasm_bindgen::JsCast; @@ -146,6 +146,59 @@ pub trait Element: Attr::new(self, Cow::from("id"), value.into_attr_value()) } + /// Set a style attribute + fn style(self, style: impl IntoStyles) -> Style + where + ::Props: WithStyle, + { + let mut styles = vec![]; + style.into_styles(&mut styles); + Style::new(self, styles) + } + + /// Add a `rotate(rad)` [transform-function](https://developer.mozilla.org/en-US/docs/Web/CSS/transform-function) to the current CSS `transform` + /// # Examples + /// + /// ``` + /// use xilem_web::{style as s, interfaces::Element, svg::kurbo::Rect}; + /// + /// # fn component() -> impl Element<()> { + /// Rect::from_origin_size((0.0, 10.0), (20.0, 30.0)) + /// .style(s("transform", "translate(10px, 0)")) // can be combined with untyped `transform` + /// .rotate(std::f64::consts::PI / 4.0) + /// // results in the following html: + /// // + /// # } + /// ``` + fn rotate(self, radians: f64) -> Rotate + where + ::Props: WithStyle, + { + Rotate::new(self, radians) + } + + /// Add a `scale()` [transform-function](https://developer.mozilla.org/en-US/docs/Web/CSS/transform-function) to the current CSS `transform` + /// # Examples + /// + /// ``` + /// use xilem_web::{style as s, interfaces::Element, svg::kurbo::Circle}; + /// + /// # fn component() -> impl Element<()> { + /// Circle::new((10.0, 20.0), 30.0) + /// .style(s("transform", "translate(10px, 0)")) // can be combined with untyped `transform` + /// .scale(1.5) + /// .scale((1.5, 2.0)) + /// // results in the following html: + /// // + /// # } + /// ``` + fn scale(self, scale: impl Into) -> Scale + where + ::Props: WithStyle, + { + Scale::new(self, scale) + } + // event list from // https://html.spec.whatwg.org/multipage/webappapis.html#idl-definitions // @@ -498,16 +551,19 @@ where { } +// pub trait StyleExt { +// } + +// /// Keep this shared code in sync between `HtmlElement` and `SvgElement` +// macro_rules! style_impls { +// () => {}; +// } + // #[cfg(feature = "HtmlElement")] pub trait HtmlElement: Element + AsRef> { - /// Set a style attribute - fn style(self, style: impl IntoStyles) -> Style { - let mut styles = vec![]; - style.into_styles(&mut styles); - Style::new(self, styles) - } + // style_impls!(); } // #[cfg(feature = "HtmlElement")] @@ -1479,12 +1535,7 @@ where pub trait SvgElement: Element + AsRef> { - /// Set a style attribute - fn style(self, style: impl IntoStyles) -> Style { - let mut styles = vec![]; - style.into_styles(&mut styles); - Style::new(self, styles) - } + // style_impls!(); } // #[cfg(feature = "SvgElement")] diff --git a/xilem_web/src/one_of.rs b/xilem_web/src/one_of.rs index 99c83ed5f..a6f26f847 100644 --- a/xilem_web/src/one_of.rs +++ b/xilem_web/src/one_of.rs @@ -232,6 +232,14 @@ impl WithStyle for Noop { fn mark_end_of_style_modifier(&mut self) { unreachable!() } + + fn get_style(&self, _name: &str) -> Option<&CowStr> { + unreachable!() + } + + fn was_updated(&self, _name: &str) -> bool { + unreachable!() + } } impl AsRef for Noop { @@ -425,6 +433,34 @@ impl< OneOf::I(e) => e.mark_end_of_style_modifier(), } } + + fn get_style(&self, name: &str) -> Option<&CowStr> { + match self { + OneOf::A(e) => e.get_style(name), + OneOf::B(e) => e.get_style(name), + OneOf::C(e) => e.get_style(name), + OneOf::D(e) => e.get_style(name), + OneOf::E(e) => e.get_style(name), + OneOf::F(e) => e.get_style(name), + OneOf::G(e) => e.get_style(name), + OneOf::H(e) => e.get_style(name), + OneOf::I(e) => e.get_style(name), + } + } + + fn was_updated(&self, name: &str) -> bool { + match self { + OneOf::A(e) => e.was_updated(name), + OneOf::B(e) => e.was_updated(name), + OneOf::C(e) => e.was_updated(name), + OneOf::D(e) => e.was_updated(name), + OneOf::E(e) => e.was_updated(name), + OneOf::F(e) => e.was_updated(name), + OneOf::G(e) => e.was_updated(name), + OneOf::H(e) => e.was_updated(name), + OneOf::I(e) => e.was_updated(name), + } + } } impl DomNode for OneOf diff --git a/xilem_web/src/style.rs b/xilem_web/src/style.rs index 3d7aff1ca..f5b82e002 100644 --- a/xilem_web/src/style.rs +++ b/xilem_web/src/style.rs @@ -1,8 +1,10 @@ // Copyright 2024 the Xilem Authors // SPDX-License-Identifier: Apache-2.0 +use peniko::kurbo::Vec2; use std::{ collections::{BTreeMap, HashMap}, + fmt::Display, marker::PhantomData, }; use wasm_bindgen::{JsCast, UnwrapThrowExt}; @@ -120,8 +122,16 @@ pub trait WithStyle { /// When in [`View::rebuild`] this has to be invoked *after* traversing the inner `View` with [`View::rebuild`] fn set_style(&mut self, name: &CowStr, value: &Option); - // TODO first find a use-case for this... - // fn get_style(&self, name: &str) -> Option<&CowStr>; + /// Gets a previously set style from this modifier. + /// + /// When in [`View::rebuild`] this has to be invoked *after* traversing the inner `View` with [`View::rebuild`] + fn get_style(&self, name: &str) -> Option<&CowStr>; + + /// Returns `true` if a style property `name` was updated. + /// + /// This can be useful, for modifying a previously set value. + /// When in [`View::rebuild`] this has to be invoked *after* traversing the inner `View` with [`View::rebuild`] + fn was_updated(&self, name: &str) -> bool; } #[derive(Debug, PartialEq)] @@ -307,6 +317,21 @@ impl WithStyle for Styles { self.idx += 1; self.start_idx = self.idx | (self.start_idx & RESERVED_BIT_MASK); } + + fn get_style(&self, name: &str) -> Option<&CowStr> { + for modifier in self.style_modifiers[..self.idx as usize].iter().rev() { + match modifier { + StyleModifier::Remove(removed) if removed == name => return None, + StyleModifier::Set(key, value) if key == name => return Some(value), + _ => (), + } + } + None + } + + fn was_updated(&self, name: &str) -> bool { + self.updated_styles.contains_key(name) + } } impl WithStyle for ElementProps { @@ -321,6 +346,19 @@ impl WithStyle for ElementProps { fn set_style(&mut self, name: &CowStr, value: &Option) { self.styles().set_style(name, value); } + + fn get_style(&self, name: &str) -> Option<&CowStr> { + self.styles + .as_deref() + .and_then(|styles| styles.get_style(name)) + } + + fn was_updated(&self, name: &str) -> bool { + self.styles + .as_deref() + .map(|styles| styles.was_updated(name)) + .unwrap_or(false) + } } impl WithStyle for Pod @@ -338,6 +376,14 @@ where fn set_style(&mut self, name: &CowStr, value: &Option) { self.props.set_style(name, value); } + + fn get_style(&self, name: &str) -> Option<&CowStr> { + self.props.get_style(name) + } + + fn was_updated(&self, name: &str) -> bool { + self.props.was_updated(name) + } } impl WithStyle for PodMut<'_, N> @@ -355,6 +401,14 @@ where fn set_style(&mut self, name: &CowStr, value: &Option) { self.props.set_style(name, value); } + + fn get_style(&self, name: &str) -> Option<&CowStr> { + self.props.get_style(name) + } + + fn was_updated(&self, name: &str) -> bool { + self.props.was_updated(name) + } } /// Syntax sugar for adding a type bound on the `ViewElement` of a view, such that both, [`ViewElement`] and [`ViewElement::Mut`] are bound to [`WithStyle`] @@ -441,3 +495,200 @@ where self.el.message(view_state, id_path, message, app_state) } } + +/// Add a `rotate(rad)` [transform-function](https://developer.mozilla.org/en-US/docs/Web/CSS/transform-function) to the current CSS `transform` +pub struct Rotate { + el: E, + phantom: PhantomData (State, Action)>, + radians: f64, +} + +impl Rotate { + pub(crate) fn new(element: E, radians: f64) -> Self { + Rotate { + el: element, + phantom: PhantomData, + radians, + } + } +} + +fn modify_rotate_transform(transform: Option<&CowStr>, radians: f64) -> Option { + if let Some(transform) = transform { + Some(CowStr::from(format!("{transform} rotate({radians}rad)"))) + } else { + Some(CowStr::from(format!("rotate({radians}rad)"))) + } +} + +impl ViewMarker for Rotate {} +impl View for Rotate +where + T: 'static, + A: 'static, + E: View, +{ + type Element = E::Element; + + type ViewState = (E::ViewState, Option); + + fn build(&self, ctx: &mut ViewCtx) -> (Self::Element, Self::ViewState) { + ctx.add_modifier_size_hint::(1); + let (mut element, state) = self.el.build(ctx); + let css_repr = modify_rotate_transform(element.get_style("transform"), self.radians); + element.set_style(&"transform".into(), &css_repr); + element.mark_end_of_style_modifier(); + (element, (state, css_repr)) + } + + fn rebuild<'el>( + &self, + prev: &Self, + (view_state, css_repr): &mut Self::ViewState, + ctx: &mut ViewCtx, + mut element: Mut<'el, Self::Element>, + ) -> Mut<'el, Self::Element> { + element.rebuild_style_modifier(); + let mut element = self.el.rebuild(&prev.el, view_state, ctx, element); + if prev.radians != self.radians || element.was_updated("transform") { + *css_repr = modify_rotate_transform(element.get_style("transform"), self.radians); + } + element.set_style(&"transform".into(), css_repr); + element.mark_end_of_style_modifier(); + element + } + + fn teardown( + &self, + (view_state, _): &mut Self::ViewState, + ctx: &mut ViewCtx, + element: Mut<'_, Self::Element>, + ) { + self.el.teardown(view_state, ctx, element); + } + + fn message( + &self, + (view_state, _): &mut Self::ViewState, + id_path: &[ViewId], + message: DynMessage, + app_state: &mut T, + ) -> MessageResult { + self.el.message(view_state, id_path, message, app_state) + } +} + +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum ScaleValue { + Uniform(f64), + NonUniform(f64, f64), +} + +impl Display for ScaleValue { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ScaleValue::Uniform(uniform) => write!(f, "{uniform}"), + ScaleValue::NonUniform(x, y) => write!(f, "{x}, {y}"), + } + } +} + +impl From for ScaleValue { + fn from(value: f64) -> Self { + ScaleValue::Uniform(value) + } +} + +impl From<(f64, f64)> for ScaleValue { + fn from(value: (f64, f64)) -> Self { + ScaleValue::NonUniform(value.0, value.1) + } +} + +impl From for ScaleValue { + fn from(value: Vec2) -> Self { + ScaleValue::NonUniform(value.x, value.y) + } +} + +/// Add a `rotate(rad)` [transform-function](https://developer.mozilla.org/en-US/docs/Web/CSS/transform-function) to the current CSS `transform` +pub struct Scale { + el: E, + phantom: PhantomData (State, Action)>, + scale: ScaleValue, +} + +impl Scale { + pub(crate) fn new(element: E, scale: impl Into) -> Self { + Scale { + el: element, + phantom: PhantomData, + scale: scale.into(), + } + } +} + +fn modify_scale_transform(transform: Option<&CowStr>, scale: ScaleValue) -> Option { + if let Some(transform) = transform { + Some(CowStr::from(format!("{transform} scale({scale})"))) + } else { + Some(CowStr::from(format!("scale({scale})"))) + } +} + +impl ViewMarker for Scale {} +impl View for Scale +where + T: 'static, + A: 'static, + E: View, +{ + type Element = E::Element; + + type ViewState = (E::ViewState, Option); + + fn build(&self, ctx: &mut ViewCtx) -> (Self::Element, Self::ViewState) { + ctx.add_modifier_size_hint::(1); + let (mut element, state) = self.el.build(ctx); + let css_repr = modify_scale_transform(element.get_style("transform"), self.scale); + element.set_style(&"transform".into(), &css_repr); + element.mark_end_of_style_modifier(); + (element, (state, css_repr)) + } + + fn rebuild<'el>( + &self, + prev: &Self, + (view_state, css_repr): &mut Self::ViewState, + ctx: &mut ViewCtx, + mut element: Mut<'el, Self::Element>, + ) -> Mut<'el, Self::Element> { + element.rebuild_style_modifier(); + let mut element = self.el.rebuild(&prev.el, view_state, ctx, element); + if prev.scale != self.scale || element.was_updated("transform") { + *css_repr = modify_scale_transform(element.get_style("transform"), self.scale); + } + element.set_style(&"transform".into(), css_repr); + element.mark_end_of_style_modifier(); + element + } + + fn teardown( + &self, + (view_state, _): &mut Self::ViewState, + ctx: &mut ViewCtx, + element: Mut<'_, Self::Element>, + ) { + self.el.teardown(view_state, ctx, element); + } + + fn message( + &self, + (view_state, _): &mut Self::ViewState, + id_path: &[ViewId], + message: DynMessage, + app_state: &mut T, + ) -> MessageResult { + self.el.message(view_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 9ca24f1ae..3ba516e54 100644 --- a/xilem_web/web_examples/svgtoy/src/main.rs +++ b/xilem_web/web_examples/svgtoy/src/main.rs @@ -3,10 +3,11 @@ use xilem_web::{ document_body, - elements::svg::{g, svg}, + elements::svg::{g, svg, text}, interfaces::*, + style as s, svg::{ - kurbo::{self, Rect}, + kurbo::{Circle, Line, Rect, Stroke}, peniko::Color, }, App, DomView, PointerMsg, @@ -55,7 +56,10 @@ impl GrabState { fn app_logic(state: &mut AppState) -> impl DomView { let v = (0..10) - .map(|i| Rect::from_origin_size((10.0 * i as f64, 150.0), (8.0, 8.0))) + .map(|i| { + Rect::from_origin_size((10.0 * i as f64, 150.0), (8.0, 8.0)) + .rotate(0.003 * (i as f64) * state.x) + }) .collect::>(); svg(g(( Rect::new(100.0, 100.0, 200.0, 200.0).on_click(|_, _| { @@ -63,20 +67,28 @@ fn app_logic(state: &mut AppState) -> impl DomView { }), Rect::new(210.0, 100.0, 310.0, 200.0) .fill(Color::LIGHT_GRAY) - .stroke(Color::BLUE, Default::default()), + .stroke(Color::BLUE, Default::default()) + .scale((state.x / 100.0 + 1.0, state.y / 100.0 + 1.0)), 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), + text("drag me around") + .style(s( + "transform", + format!("translate({}px, {}px)", state.x, state.y + 50.0), + )) + .style([s("font-size", "10px"), s("pointer-events", "none")]), + g(v).style(s("transform", "translate(430px, 0)")) // untyped transform can be combined with transform modifiers, though this overwrites previously set `transform` values + .scale(state.y / 100.0 + 1.0), 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)).stroke( + 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]), + Stroke::new(1.0).with_dashes(state.x, [7.0, 1.0]), ), - kurbo::Circle::new((460.0, 260.0), 45.0).on_click(|_, _| { + Circle::new((460.0, 260.0), 45.0).on_click(|_, _| { web_sys::console::log_1(&"circle clicked".into()); }), )))