From aed50ca841c063004cdbd76d6cf328566a4472df Mon Sep 17 00:00:00 2001 From: Philipp Mildenberger Date: Sat, 4 Nov 2023 22:41:55 +0100 Subject: [PATCH] Merged xilem_svg into xilem_html --- Cargo.lock | 11 + Cargo.toml | 1 + crates/xilem_html/Cargo.toml | 8 + crates/xilem_html/src/elements.rs | 5 + crates/xilem_html/src/interfaces.rs | 22 +- crates/xilem_html/src/lib.rs | 1 + crates/xilem_html/src/svg/common_attrs.rs | 209 ++++++++++++++++ crates/xilem_html/src/svg/kurbo_shape.rs | 235 ++++++++++++++++++ crates/xilem_html/src/svg/mod.rs | 7 + crates/xilem_html/src/svg/pointer.rs | 150 +++++++++++ .../xilem_html/web_examples/svgtoy/Cargo.toml | 12 + .../xilem_html/web_examples/svgtoy/index.html | 19 ++ .../web_examples/svgtoy/src/main.rs | 88 +++++++ 13 files changed, 767 insertions(+), 1 deletion(-) create mode 100644 crates/xilem_html/src/svg/common_attrs.rs create mode 100644 crates/xilem_html/src/svg/kurbo_shape.rs create mode 100644 crates/xilem_html/src/svg/mod.rs create mode 100644 crates/xilem_html/src/svg/pointer.rs create mode 100644 crates/xilem_html/web_examples/svgtoy/Cargo.toml create mode 100644 crates/xilem_html/web_examples/svgtoy/index.html create mode 100644 crates/xilem_html/web_examples/svgtoy/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index 0a44f8608..bd59c3b9f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2123,6 +2123,16 @@ dependencies = [ "xilem_svg", ] +[[package]] +name = "svgtoy_html" +version = "0.1.0" +dependencies = [ + "console_error_panic_hook", + "wasm-bindgen", + "web-sys", + "xilem_html", +] + [[package]] name = "swash" version = "0.1.8" @@ -3030,6 +3040,7 @@ dependencies = [ "gloo", "kurbo 0.9.5", "log", + "peniko 0.1.0 (git+https://github.com/linebender/peniko?rev=629fc3325b016a8c98b1cd6204cb4ddf1c6b3daa)", "wasm-bindgen", "web-sys", "xilem_core", diff --git a/Cargo.toml b/Cargo.toml index d3de1c075..983d198e2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ members = [ "crates/xilem_html/web_examples/counter", "crates/xilem_html/web_examples/counter_custom_element", "crates/xilem_html/web_examples/todomvc", + "crates/xilem_html/web_examples/svgtoy", "crates/xilem_svg", "crates/xilem_svg/web_examples/svgtoy", ] diff --git a/crates/xilem_html/Cargo.toml b/crates/xilem_html/Cargo.toml index 3b9432717..5890348fa 100644 --- a/crates/xilem_html/Cargo.toml +++ b/crates/xilem_html/Cargo.toml @@ -24,6 +24,7 @@ bitflags = "2" wasm-bindgen = "0.2.87" log = "0.4.19" gloo = { version = "0.8.1", default-features = false, features = ["events", "utils"] } +peniko = { git = "https://github.com/linebender/peniko", rev = "629fc3325b016a8c98b1cd6204cb4ddf1c6b3daa" } [dependencies.web-sys] version = "0.3.4" @@ -36,6 +37,13 @@ features = [ "Node", "NodeList", "SvgElement", + "SvgGraphicsElement", + "SvggElement", + "SvgGeometryElement", + "SvgCircleElement", + "SvgLineElement", + "SvgRectElement", + "SvgPathElement", "Text", "Window", "FocusEvent", diff --git a/crates/xilem_html/src/elements.rs b/crates/xilem_html/src/elements.rs index d6972c434..676ffbf39 100644 --- a/crates/xilem_html/src/elements.rs +++ b/crates/xilem_html/src/elements.rs @@ -431,4 +431,9 @@ define_elements!( (MATHML_NS, Semantics, semantics, Element), // SVG (TODO all SVG elements and their interfaces) (SVG_NS, Svg, svg, SvgElement), + (SVG_NS, Group, g, SvggElement), // TODO `group` or `g`? (for consistency `g` seems to be the better option, xilem_svg uses `group`) + (SVG_NS, Rect, rect, SvgRectElement), + (SVG_NS, Path, path, SvgPathElement), + (SVG_NS, Circle, circle, SvgCircleElement), + (SVG_NS, Line, line, SvgLineElement), ); diff --git a/crates/xilem_html/src/interfaces.rs b/crates/xilem_html/src/interfaces.rs index 12b07eb06..9f9aef12d 100644 --- a/crates/xilem_html/src/interfaces.rs +++ b/crates/xilem_html/src/interfaces.rs @@ -303,5 +303,25 @@ dom_interface_macro_and_trait_definitions!($, self.attr("height", value) } }, - SvgElement: Element {} + // TODO include all SVG interfaces... + SvgElement: Element { + // TODO consider stateful event views like this in general + fn pointer(self, f: F) -> crate::svg::pointer::Pointer { + crate::svg::pointer::pointer(self, f) + } + }, + SvgGraphicsElement: SvgElement {}, + SvgGeometryElement: SvgGraphicsElement { + fn fill(self, brush: impl Into) -> crate::svg::common_attrs::Fill { + crate::svg::common_attrs::fill(self, brush) + } + fn stroke(self, brush: impl Into, style: peniko::kurbo::Stroke) -> crate::svg::common_attrs::Stroke { + crate::svg::common_attrs::stroke(self, brush, style) + } + }, + SvgRectElement: SvgGeometryElement {}, + SvgPathElement: SvgGeometryElement {}, + SvgCircleElement: SvgGeometryElement {}, + SvgLineElement: SvgGeometryElement {}, + SvggElement: SvgGraphicsElement {} ); diff --git a/crates/xilem_html/src/lib.rs b/crates/xilem_html/src/lib.rs index 40ef9a025..2fdb8ec80 100644 --- a/crates/xilem_html/src/lib.rs +++ b/crates/xilem_html/src/lib.rs @@ -11,6 +11,7 @@ mod app; mod attribute; mod attribute_value; mod context; +pub mod svg; mod diff; pub mod elements; pub mod events; diff --git a/crates/xilem_html/src/svg/common_attrs.rs b/crates/xilem_html/src/svg/common_attrs.rs new file mode 100644 index 000000000..47d45bfb2 --- /dev/null +++ b/crates/xilem_html/src/svg/common_attrs.rs @@ -0,0 +1,209 @@ +// Copyright 2023 the Druid Authors. +// SPDX-License-Identifier: Apache-2.0 + +use std::borrow::Cow; +use std::{any::Any, marker::PhantomData}; + +use peniko::Brush; +use wasm_bindgen::JsCast; +use xilem_core::{Id, MessageResult}; + +use crate::IntoAttributeValue; +use crate::{ + context::{ChangeFlags, Cx}, + view::{DomNode, View, ViewMarker}, +}; + +pub struct Fill { + child: V, + // This could reasonably be static Cow also, but keep things simple + brush: Brush, + phantom: PhantomData (T, A)>, +} + +pub struct Stroke { + child: V, + // This could reasonably be static Cow also, but keep things simple + brush: Brush, + style: peniko::kurbo::Stroke, + phantom: PhantomData (T, A)>, +} + +pub fn fill(child: V, brush: impl Into) -> Fill { + Fill { + child, + brush: brush.into(), + phantom: Default::default(), + } +} + +pub fn stroke( + child: V, + brush: impl Into, + style: peniko::kurbo::Stroke, +) -> Stroke { + Stroke { + child, + brush: brush.into(), + style, + phantom: Default::default(), + } +} + +fn brush_to_string(brush: &Brush) -> String { + match brush { + Brush::Solid(color) => { + if color.a == 0 { + "none".into() + } else { + format!("#{:02x}{:02x}{:02x}", color.r, color.g, color.b) + } + } + _ => todo!("gradients not implemented"), + } +} + +macro_rules! impl_dom_interface_for_ty { + ($dom_interface:ident, $ty:ident) => { + impl> + $crate::interfaces::$dom_interface for $ty + { + } + }; +} + +macro_rules! impl_dom_interfaces_for_ty { + ($ty:ident: $($dom_interface:ident,)*) => { + $(impl_dom_interface_for_ty!($dom_interface, $ty);)* + } +} + +// TODO it would be great to have a macro that does automatically derive all child interfaces of some DOM interface... +impl_dom_interfaces_for_ty!(Fill: + Element, + SvgElement, + SvgGraphicsElement, + SvgGeometryElement, + SvgRectElement, + SvgPathElement, + SvgCircleElement, + SvgLineElement, +); + +impl ViewMarker for Fill {} +impl crate::interfaces::sealed::Sealed for Fill {} + +// TODO: make generic over A (probably requires Phantom) +impl> View for Fill { + type State = V::State; + type Element = V::Element; + + fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) { + let (id, child_state, element) = self.child.build(cx); + element + .as_node_ref() + .dyn_ref::() + .unwrap() + .set_attribute("fill", &brush_to_string(&self.brush)) + .unwrap(); + (id, child_state, element) + } + + fn rebuild( + &self, + cx: &mut Cx, + prev: &Self, + id: &mut Id, + state: &mut Self::State, + element: &mut V::Element, + ) -> ChangeFlags { + let prev_id = *id; + let mut changed = self.child.rebuild(cx, &prev.child, id, state, element); + if self.brush != prev.brush || prev_id != *id { + element + .as_node_ref() + .dyn_ref::() + .unwrap() + .set_attribute("fill", &brush_to_string(&self.brush)) + .unwrap(); + changed.insert(ChangeFlags::OTHER_CHANGE); + } + changed + } + + fn message( + &self, + id_path: &[Id], + state: &mut Self::State, + message: Box, + app_state: &mut T, + ) -> MessageResult { + self.child.message(id_path, state, message, app_state) + } +} + +impl_dom_interfaces_for_ty!(Stroke: + Element, + SvgElement, + SvgGraphicsElement, + SvgGeometryElement, + SvgRectElement, + SvgPathElement, + SvgCircleElement, + SvgLineElement, +); + +impl ViewMarker for Stroke {} +impl crate::interfaces::sealed::Sealed for Stroke {} + +impl> View for Stroke { + type State = (Cow<'static, str>, V::State); + type Element = V::Element; + + fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) { + let brush_svg_repr = Cow::from(brush_to_string(&self.brush)); + cx.add_new_attribute_to_current_element( + &"stroke".into(), + &brush_svg_repr.clone().into_attribute_value(), + ); + cx.add_new_attribute_to_current_element( + &"stroke-width".into(), + &self.style.width.into_attribute_value(), + ); + let (id, child_state, element) = self.child.build(cx); + (id, (brush_svg_repr, child_state), element) + } + + fn rebuild( + &self, + cx: &mut Cx, + prev: &Self, + id: &mut Id, + (brush_svg_repr, child_state): &mut Self::State, + element: &mut V::Element, + ) -> ChangeFlags { + if self.brush != prev.brush { + *brush_svg_repr = Cow::from(brush_to_string(&self.brush)); + } + cx.add_new_attribute_to_current_element( + &"stroke".into(), + &brush_svg_repr.clone().into_attribute_value(), + ); + cx.add_new_attribute_to_current_element( + &"stroke-width".into(), + &self.style.width.into_attribute_value(), + ); + self.child + .rebuild(cx, &prev.child, id, child_state, element) + } + + fn message( + &self, + id_path: &[Id], + (_, child_state): &mut Self::State, + message: Box, + app_state: &mut T, + ) -> MessageResult { + self.child.message(id_path, child_state, message, app_state) + } +} diff --git a/crates/xilem_html/src/svg/kurbo_shape.rs b/crates/xilem_html/src/svg/kurbo_shape.rs new file mode 100644 index 000000000..6caf0823c --- /dev/null +++ b/crates/xilem_html/src/svg/kurbo_shape.rs @@ -0,0 +1,235 @@ +// Copyright 2023 the Druid Authors. +// SPDX-License-Identifier: Apache-2.0 + +//! Implementation of the View trait for various kurbo shapes. + +use peniko::kurbo::{BezPath, Circle, Line, Rect}; +use std::borrow::Cow; + +use xilem_core::{Id, MessageResult}; + +use crate::{ + context::{ChangeFlags, Cx}, + interfaces::for_all_dom_interface_relatives, + interfaces::sealed::Sealed, + vecmap::VecMap, + view::{View, ViewMarker}, + AttributeValue, IntoAttributeValue, SVG_NS, +}; + +macro_rules! generate_dom_interface_impl { + ($dom_interface:ident, $ty_name:ident, $t:ident, $a:ident) => { + impl<$t, $a> $crate::interfaces::$dom_interface<$t, $a> for $ty_name {} + }; +} + +for_all_dom_interface_relatives!(SvgLineElement, generate_dom_interface_impl, Line, T, A); + +impl ViewMarker for Line {} +impl Sealed for Line {} + +impl View for Line { + type State = VecMap, AttributeValue>; + type Element = web_sys::Element; + + fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) { + cx.add_new_attribute_to_current_element(&"x1".into(), &self.p0.x.into_attribute_value()); + cx.add_new_attribute_to_current_element(&"y1".into(), &self.p0.y.into_attribute_value()); + cx.add_new_attribute_to_current_element(&"x2".into(), &self.p1.x.into_attribute_value()); + cx.add_new_attribute_to_current_element(&"y2".into(), &self.p1.y.into_attribute_value()); + let (el, attributes) = cx.build_element(SVG_NS, "line"); + let id = Id::next(); + (id, attributes, el) + } + + fn rebuild( + &self, + cx: &mut Cx, + _prev: &Self, + _id: &mut Id, + attributes: &mut Self::State, + element: &mut Self::Element, + ) -> ChangeFlags { + cx.add_new_attribute_to_current_element(&"x1".into(), &self.p0.x.into_attribute_value()); + cx.add_new_attribute_to_current_element(&"y1".into(), &self.p0.y.into_attribute_value()); + cx.add_new_attribute_to_current_element(&"x2".into(), &self.p1.x.into_attribute_value()); + cx.add_new_attribute_to_current_element(&"y2".into(), &self.p1.y.into_attribute_value()); + cx.rebuild_element(element, attributes) + } + + fn message( + &self, + _id_path: &[Id], + _state: &mut Self::State, + message: Box, + _app_state: &mut T, + ) -> MessageResult { + MessageResult::Stale(message) + } +} + +for_all_dom_interface_relatives!(SvgRectElement, generate_dom_interface_impl, Rect, T, A); + +impl ViewMarker for Rect {} +impl Sealed for Rect {} + +impl View for Rect { + type State = VecMap, AttributeValue>; + type Element = web_sys::Element; + + fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) { + cx.add_new_attribute_to_current_element(&"x".into(), &self.x0.into_attribute_value()); + cx.add_new_attribute_to_current_element(&"y".into(), &self.y0.into_attribute_value()); + let size = self.size(); + cx.add_new_attribute_to_current_element( + &"width".into(), + &size.width.into_attribute_value(), + ); + cx.add_new_attribute_to_current_element( + &"height".into(), + &size.height.into_attribute_value(), + ); + let (el, attributes) = cx.build_element(SVG_NS, "rect"); + let id = Id::next(); + (id, attributes, el) + } + + fn rebuild( + &self, + cx: &mut Cx, + _prev: &Self, + _id: &mut Id, + attributes: &mut Self::State, + element: &mut Self::Element, + ) -> ChangeFlags { + cx.add_new_attribute_to_current_element(&"x".into(), &self.x0.into_attribute_value()); + cx.add_new_attribute_to_current_element(&"y".into(), &self.y0.into_attribute_value()); + let size = self.size(); + cx.add_new_attribute_to_current_element( + &"width".into(), + &size.width.into_attribute_value(), + ); + cx.add_new_attribute_to_current_element( + &"height".into(), + &size.height.into_attribute_value(), + ); + cx.rebuild_element(element, attributes) + } + + fn message( + &self, + _id_path: &[Id], + _state: &mut Self::State, + message: Box, + _app_state: &mut T, + ) -> MessageResult { + MessageResult::Stale(message) + } +} + +for_all_dom_interface_relatives!(SvgCircleElement, generate_dom_interface_impl, Circle, T, A); + +impl ViewMarker for Circle {} +impl Sealed for Circle {} + +impl View for Circle { + type State = VecMap, AttributeValue>; + type Element = web_sys::Element; + + fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) { + cx.add_new_attribute_to_current_element( + &"cx".into(), + &self.center.x.into_attribute_value(), + ); + cx.add_new_attribute_to_current_element( + &"cy".into(), + &self.center.y.into_attribute_value(), + ); + cx.add_new_attribute_to_current_element(&"r".into(), &self.radius.into_attribute_value()); + let (el, attributes) = cx.build_element(SVG_NS, "circle"); + let id = Id::next(); + (id, attributes, el) + } + + fn rebuild( + &self, + cx: &mut Cx, + _prev: &Self, + _id: &mut Id, + attributes: &mut Self::State, + element: &mut Self::Element, + ) -> ChangeFlags { + cx.add_new_attribute_to_current_element( + &"cx".into(), + &self.center.x.into_attribute_value(), + ); + cx.add_new_attribute_to_current_element( + &"cy".into(), + &self.center.y.into_attribute_value(), + ); + cx.add_new_attribute_to_current_element(&"r".into(), &self.radius.into_attribute_value()); + cx.rebuild_element(element, attributes) + } + + fn message( + &self, + _id_path: &[Id], + _state: &mut Self::State, + message: Box, + _app_state: &mut T, + ) -> MessageResult { + MessageResult::Stale(message) + } +} + +for_all_dom_interface_relatives!(SvgPathElement, generate_dom_interface_impl, BezPath, T, A); + +impl ViewMarker for BezPath {} +impl Sealed for BezPath {} + +impl View for BezPath { + type State = (Cow<'static, str>, VecMap, AttributeValue>); + type Element = web_sys::Element; + + fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) { + let svg_repr = Cow::from(self.to_svg()); + cx.add_new_attribute_to_current_element( + &"d".into(), + &svg_repr.clone().into_attribute_value(), + ); + let (el, attributes) = cx.build_element(SVG_NS, "path"); + let id = Id::next(); + (id, (svg_repr, attributes), el) + } + + fn rebuild( + &self, + cx: &mut Cx, + prev: &Self, + _id: &mut Id, + (svg_repr, attributes): &mut Self::State, + element: &mut Self::Element, + ) -> ChangeFlags { + // slight optimization to avoid serialization/allocation + if self != prev { + *svg_repr = Cow::from(self.to_svg()); + } + cx.add_new_attribute_to_current_element( + &"d".into(), + &svg_repr.clone().into_attribute_value(), + ); + cx.rebuild_element(element, attributes) + } + + fn message( + &self, + _id_path: &[Id], + _state: &mut Self::State, + message: Box, + _app_state: &mut T, + ) -> MessageResult { + MessageResult::Stale(message) + } +} + +// TODO: RoundedRect diff --git a/crates/xilem_html/src/svg/mod.rs b/crates/xilem_html/src/svg/mod.rs new file mode 100644 index 000000000..d80f42c83 --- /dev/null +++ b/crates/xilem_html/src/svg/mod.rs @@ -0,0 +1,7 @@ +pub(crate) mod common_attrs; +pub(crate) mod kurbo_shape; +pub mod pointer; + +pub use peniko; +pub use peniko::kurbo; +pub use pointer::{PointerDetails, PointerMsg}; diff --git a/crates/xilem_html/src/svg/pointer.rs b/crates/xilem_html/src/svg/pointer.rs new file mode 100644 index 000000000..6921dafda --- /dev/null +++ b/crates/xilem_html/src/svg/pointer.rs @@ -0,0 +1,150 @@ +// Copyright 2023 the Druid Authors. +// SPDX-License-Identifier: Apache-2.0 + +//! Interactivity with pointer events. + +use std::{any::Any, marker::PhantomData}; + +use wasm_bindgen::{prelude::Closure, JsCast}; +use web_sys::PointerEvent; + +use xilem_core::{Id, MessageResult}; + +use crate::{ + context::{ChangeFlags, Cx}, + interfaces::Element, + view::{DomNode, View, ViewMarker}, +}; + +pub struct Pointer { + child: V, + callback: F, + phantom: PhantomData (T, A)>, +} + +pub struct PointerState { + // Closures are retained so they can be called by environment + #[allow(unused)] + down_closure: Closure, + #[allow(unused)] + move_closure: Closure, + #[allow(unused)] + up_closure: Closure, + child_state: S, +} + +#[derive(Debug)] +/// A message representing a pointer event. +pub enum PointerMsg { + Down(PointerDetails), + Move(PointerDetails), + Up(PointerDetails), +} + +#[derive(Debug)] +/// Details of a pointer event. +pub struct PointerDetails { + pub id: i32, + pub button: i16, + pub x: f64, + pub y: f64, +} + +impl PointerDetails { + fn from_pointer_event(e: &PointerEvent) -> Self { + PointerDetails { + id: e.pointer_id(), + button: e.button(), + x: e.client_x() as f64, + y: e.client_y() as f64, + } + } +} + +pub fn pointer>( + child: V, + callback: F, +) -> Pointer { + Pointer { + child, + callback, + phantom: Default::default(), + } +} + +impl ViewMarker for Pointer {} + +impl A + Send, V: View> View + for Pointer +{ + type State = PointerState; + type Element = V::Element; + + fn build(&self, cx: &mut Cx) -> (Id, Self::State, Self::Element) { + let (id, child_state, element) = self.child.build(cx); + let thunk = cx.with_id(id, |cx| cx.message_thunk()); + let el = element.as_node_ref().dyn_ref::().unwrap(); + let el_clone = el.clone(); + let down_closure = Closure::new(move |e: PointerEvent| { + thunk.push_message(PointerMsg::Down(PointerDetails::from_pointer_event(&e))); + el_clone.set_pointer_capture(e.pointer_id()).unwrap(); + e.prevent_default(); + e.stop_propagation(); + }); + el.add_event_listener_with_callback("pointerdown", down_closure.as_ref().unchecked_ref()) + .unwrap(); + let thunk = cx.with_id(id, |cx| cx.message_thunk()); + let move_closure = Closure::new(move |e: PointerEvent| { + thunk.push_message(PointerMsg::Move(PointerDetails::from_pointer_event(&e))); + e.prevent_default(); + e.stop_propagation(); + }); + el.add_event_listener_with_callback("pointermove", move_closure.as_ref().unchecked_ref()) + .unwrap(); + let thunk = cx.with_id(id, |cx| cx.message_thunk()); + let up_closure = Closure::new(move |e: PointerEvent| { + thunk.push_message(PointerMsg::Up(PointerDetails::from_pointer_event(&e))); + e.prevent_default(); + e.stop_propagation(); + }); + el.add_event_listener_with_callback("pointerup", up_closure.as_ref().unchecked_ref()) + .unwrap(); + let state = PointerState { + down_closure, + move_closure, + up_closure, + child_state, + }; + (id, state, element) + } + + fn rebuild( + &self, + cx: &mut Cx, + prev: &Self, + id: &mut Id, + state: &mut Self::State, + element: &mut Self::Element, + ) -> ChangeFlags { + // TODO: if the child id changes (as can happen with AnyView), reinstall closure + self.child + .rebuild(cx, &prev.child, id, &mut state.child_state, element) + } + + fn message( + &self, + id_path: &[Id], + state: &mut Self::State, + message: Box, + app_state: &mut T, + ) -> MessageResult { + match message.downcast() { + Ok(msg) => { + MessageResult::Action((self.callback)(app_state, *msg)) + } + Err(message) => self + .child + .message(id_path, &mut state.child_state, message, app_state), + } + } +} diff --git a/crates/xilem_html/web_examples/svgtoy/Cargo.toml b/crates/xilem_html/web_examples/svgtoy/Cargo.toml new file mode 100644 index 000000000..d6491209e --- /dev/null +++ b/crates/xilem_html/web_examples/svgtoy/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "svgtoy_html" +version = "0.1.0" +publish = false +license.workspace = true +edition.workspace = true + +[dependencies] +console_error_panic_hook = "0.1" +wasm-bindgen = "0.2.87" +web-sys = "0.3.64" +xilem_html = { path = "../.." } diff --git a/crates/xilem_html/web_examples/svgtoy/index.html b/crates/xilem_html/web_examples/svgtoy/index.html new file mode 100644 index 000000000..0bc7c6ac5 --- /dev/null +++ b/crates/xilem_html/web_examples/svgtoy/index.html @@ -0,0 +1,19 @@ + + + + + + diff --git a/crates/xilem_html/web_examples/svgtoy/src/main.rs b/crates/xilem_html/web_examples/svgtoy/src/main.rs new file mode 100644 index 000000000..654fa5d2a --- /dev/null +++ b/crates/xilem_html/web_examples/svgtoy/src/main.rs @@ -0,0 +1,88 @@ +// Copyright 2023 the Druid Authors. +// SPDX-License-Identifier: Apache-2.0 + +use xilem_html::{ + document_body, + elements::{g, svg}, + interfaces::*, + svg::{ + kurbo::{self, Rect}, + peniko::Color, + PointerMsg, + }, + App, View, +}; + +#[derive(Default)] +struct AppState { + x: f64, + y: f64, + grab: GrabState, +} + +#[derive(Default)] +struct GrabState { + is_down: bool, + id: i32, + dx: f64, + dy: f64, +} + +impl GrabState { + fn handle(&mut self, x: &mut f64, y: &mut f64, p: &PointerMsg) { + match p { + PointerMsg::Down(e) => { + if e.button == 0 { + self.dx = *x - e.x; + self.dy = *y - e.y; + self.id = e.id; + self.is_down = true; + } + } + PointerMsg::Move(e) => { + if self.is_down && self.id == e.id { + *x = self.dx + e.x; + *y = self.dy + e.y; + } + } + PointerMsg::Up(e) => { + if self.id == e.id { + self.is_down = false; + } + } + } + } +} + +fn app_logic(state: &mut AppState) -> impl View { + let v = (0..10) + .map(|i| Rect::from_origin_size((10.0 * i as f64, 150.0), (8.0, 8.0))) + .collect::>(); + svg(g(( + Rect::new(100.0, 100.0, 200.0, 200.0).on_click(|_, _| { + web_sys::console::log_1(&"app logic clicked".into()); + }), + Rect::new(210.0, 100.0, 310.0, 200.0) + .fill(Color::LIGHT_GRAY) + .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.) + .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::Circle::new((460.0, 260.0), 45.0).on_click(|_, _| { + web_sys::console::log_1(&"circle clicked".into()); + }), + ))) + .attr("width", 800) + .attr("height", 600) +} + +pub fn main() { + console_error_panic_hook::set_once(); + let app = App::new(AppState::default(), app_logic); + app.run(&document_body()); +}