diff --git a/Cargo.lock b/Cargo.lock
index 9cdcabe87..1acbd903c 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1033,7 +1033,7 @@ dependencies = [
"instant",
"js-sys",
"keyboard-types",
- "kurbo",
+ "kurbo 0.9.5",
"lazy_static",
"memchr",
"nix 0.25.1",
@@ -1333,6 +1333,16 @@ dependencies = [
"arrayvec",
]
+[[package]]
+name = "kurbo"
+version = "0.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1618d4ebd923e97d67e7cd363d80aef35fe961005cbbbb3d2dad8bdd1bc63440"
+dependencies = [
+ "arrayvec",
+ "smallvec",
+]
+
[[package]]
name = "kv-log-macro"
version = "1.0.7"
@@ -1684,12 +1694,21 @@ version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099"
+[[package]]
+name = "peniko"
+version = "0.1.0"
+source = "git+https://github.com/linebender/peniko?rev=629fc3325b016a8c98b1cd6204cb4ddf1c6b3daa#629fc3325b016a8c98b1cd6204cb4ddf1c6b3daa"
+dependencies = [
+ "kurbo 0.10.4",
+ "smallvec",
+]
+
[[package]]
name = "peniko"
version = "0.1.0"
source = "git+https://github.com/linebender/peniko?rev=cafdac9a211a0fb2fec5656bd663d1ac770bcc81#cafdac9a211a0fb2fec5656bd663d1ac770bcc81"
dependencies = [
- "kurbo",
+ "kurbo 0.9.5",
"smallvec",
]
@@ -2094,6 +2113,16 @@ version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fb1df15f412ee2e9dfc1c504260fa695c1c3f10fe9f4a6ee2d2184d7d6450e2"
+[[package]]
+name = "svgtoy"
+version = "0.1.0"
+dependencies = [
+ "console_error_panic_hook",
+ "wasm-bindgen",
+ "web-sys",
+ "xilem_svg",
+]
+
[[package]]
name = "swash"
version = "0.1.8"
@@ -2401,7 +2430,7 @@ dependencies = [
"bytemuck",
"fello",
"futures-intrusive",
- "peniko",
+ "peniko 0.1.0 (git+https://github.com/linebender/peniko?rev=cafdac9a211a0fb2fec5656bd663d1ac770bcc81)",
"raw-window-handle",
"vello_encoding",
"wgpu",
@@ -2415,7 +2444,7 @@ dependencies = [
"bytemuck",
"fello",
"guillotiere",
- "peniko",
+ "peniko 0.1.0 (git+https://github.com/linebender/peniko?rev=cafdac9a211a0fb2fec5656bd663d1ac770bcc81)",
]
[[package]]
@@ -2999,7 +3028,7 @@ version = "0.1.0"
dependencies = [
"bitflags 2.3.3",
"gloo",
- "kurbo",
+ "kurbo 0.9.5",
"log",
"wasm-bindgen",
"web-sys",
@@ -3011,7 +3040,7 @@ name = "xilem_svg"
version = "0.1.0"
dependencies = [
"bitflags 2.3.3",
- "kurbo",
+ "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 7a813d401..e346fce1c 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -6,6 +6,7 @@ members = [
"crates/xilem_html/web_examples/counter_untyped",
"crates/xilem_html/web_examples/todomvc",
"crates/xilem_svg",
+ "crates/xilem_svg/web_examples/svgtoy",
]
[workspace.package]
diff --git a/crates/xilem_svg/Cargo.toml b/crates/xilem_svg/Cargo.toml
index 7e9e2a61f..86565dfb7 100644
--- a/crates/xilem_svg/Cargo.toml
+++ b/crates/xilem_svg/Cargo.toml
@@ -17,14 +17,11 @@ default-target = "x86_64-pc-windows-msvc"
# rustdoc-scrape-examples tracking issue https://github.com/rust-lang/rust/issues/88791
cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples"]
-[lib]
-crate-type = ["cdylib"]
-
[dependencies]
xilem_core.workspace = true
-kurbo.workspace = true
bitflags = "2"
wasm-bindgen = "0.2.84"
+peniko = { git = "https://github.com/linebender/peniko", rev = "629fc3325b016a8c98b1cd6204cb4ddf1c6b3daa" }
[dependencies.web-sys]
version = "0.3.4"
diff --git a/crates/xilem_svg/README.md b/crates/xilem_svg/README.md
index 2fb1fbf66..a9cf3661a 100644
--- a/crates/xilem_svg/README.md
+++ b/crates/xilem_svg/README.md
@@ -1,7 +1,7 @@
# Xilemsvg prototype
-This is a proof of concept showing how to use `xilem_core` to render interactive vector graphics into SVG DOM nodes, running in a browser. A next step would be to factor it into a library so that applications can depend on it, but at the moment the test scene is baked in.
+This is a proof of concept showing how to use `xilem_core` to render interactive vector graphics into SVG DOM nodes, running in a browser. It is provided as a library and some examples.
-The easiest way to run it is to use [Trunk]. Run `trunk serve`, then navigate the browser to the link provided (usually `http://localhost:8080`).
+The easiest way to run the examples is to use [Trunk]. Go into the appropriate subdirectory of `web_examples`, run `trunk serve`, then navigate the browser to the link provided (usually `http://localhost:8080`).
[Trunk]: https://trunkrs.dev/
diff --git a/crates/xilem_svg/index.html b/crates/xilem_svg/index.html
deleted file mode 100644
index 9a850a6fe..000000000
--- a/crates/xilem_svg/index.html
+++ /dev/null
@@ -1,8 +0,0 @@
-
diff --git a/crates/xilem_svg/src/class.rs b/crates/xilem_svg/src/class.rs
index 4e096ec35..ac7ad4add 100644
--- a/crates/xilem_svg/src/class.rs
+++ b/crates/xilem_svg/src/class.rs
@@ -1,7 +1,7 @@
// Copyright 2023 the Druid Authors.
// SPDX-License-Identifier: Apache-2.0
-use std::any::Any;
+use std::{any::Any, marker::PhantomData};
use xilem_core::{Id, MessageResult};
@@ -10,23 +10,25 @@ use crate::{
view::{DomElement, View, ViewMarker},
};
-pub struct Class {
+pub struct Class {
child: V,
// This could reasonably be static Cow also, but keep things simple
class: String,
+ phantom: PhantomData,
}
-pub fn class(child: V, class: impl Into) -> Class {
+pub fn class(child: V, class: impl Into) -> Class {
Class {
child,
class: class.into(),
+ phantom: Default::default(),
}
}
-impl ViewMarker for Class {}
+impl ViewMarker for Class {}
// TODO: make generic over A (probably requires Phantom)
-impl> View for Class {
+impl> View for Class {
type State = V::State;
type Element = V::Element;
diff --git a/crates/xilem_svg/src/clicked.rs b/crates/xilem_svg/src/clicked.rs
index 1eed1a408..8fe513eb0 100644
--- a/crates/xilem_svg/src/clicked.rs
+++ b/crates/xilem_svg/src/clicked.rs
@@ -1,7 +1,7 @@
// Copyright 2023 the Druid Authors.
// SPDX-License-Identifier: Apache-2.0
-use std::any::Any;
+use std::{any::Any, marker::PhantomData};
use wasm_bindgen::{prelude::Closure, JsCast};
use web_sys::SvgElement;
@@ -13,9 +13,10 @@ use crate::{
view::{DomElement, View, ViewMarker},
};
-pub struct Clicked {
+pub struct Clicked {
child: V,
callback: F,
+ phantom: PhantomData,
}
pub struct ClickedState {
@@ -27,13 +28,17 @@ pub struct ClickedState {
struct ClickedMsg;
-pub fn clicked>(child: V, callback: F) -> Clicked {
- Clicked { child, callback }
+pub fn clicked>(child: V, callback: F) -> Clicked {
+ Clicked {
+ child,
+ callback,
+ phantom: Default::default(),
+ }
}
-impl ViewMarker for Clicked {}
+impl ViewMarker for Clicked {}
-impl> View for Clicked {
+impl> View for Clicked {
type State = ClickedState;
type Element = V::Element;
diff --git a/crates/xilem_svg/src/common_attrs.rs b/crates/xilem_svg/src/common_attrs.rs
new file mode 100644
index 000000000..a11d82e51
--- /dev/null
+++ b/crates/xilem_svg/src/common_attrs.rs
@@ -0,0 +1,163 @@
+// Copyright 2023 the Druid Authors.
+// SPDX-License-Identifier: Apache-2.0
+
+use std::{any::Any, marker::PhantomData};
+
+use peniko::Brush;
+use xilem_core::{Id, MessageResult};
+
+use crate::{
+ context::{ChangeFlags, Cx},
+ view::{DomElement, View, ViewMarker},
+};
+
+pub struct Fill {
+ child: V,
+ brush: Brush,
+ phantom: PhantomData,
+}
+
+pub struct Stroke {
+ child: V,
+ brush: Brush,
+ style: peniko::kurbo::Stroke,
+ phantom: PhantomData,
+}
+
+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"),
+ }
+}
+
+impl ViewMarker 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_element_ref()
+ .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_element_ref()
+ .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 ViewMarker for Stroke {}
+
+// TODO: make generic over A (probably requires Phantom)
+impl> View for Stroke {
+ 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_element_ref()
+ .set_attribute("stroke", &brush_to_string(&self.brush))
+ .unwrap();
+ element
+ .as_element_ref()
+ .set_attribute("stroke-width", &format!("{}", self.style.width))
+ .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_element_ref()
+ .set_attribute("stroke", &brush_to_string(&self.brush))
+ .unwrap();
+ changed.insert(ChangeFlags::OTHER_CHANGE);
+ }
+ if self.style.width != prev.style.width || prev_id != *id {
+ element
+ .as_element_ref()
+ .set_attribute("stroke-width", &format!("{}", self.style.width))
+ .unwrap();
+ }
+ 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)
+ }
+}
diff --git a/crates/xilem_svg/src/context.rs b/crates/xilem_svg/src/context.rs
index 82d8fe3c3..82bc75a47 100644
--- a/crates/xilem_svg/src/context.rs
+++ b/crates/xilem_svg/src/context.rs
@@ -14,7 +14,7 @@ pub struct Cx {
app_ref: Option>,
}
-pub struct MessageThunk {
+pub(crate) struct MessageThunk {
id_path: IdPath,
app_ref: Box,
}
@@ -82,7 +82,7 @@ impl Cx {
&self.document
}
- pub fn message_thunk(&self) -> MessageThunk {
+ pub(crate) fn message_thunk(&self) -> MessageThunk {
MessageThunk {
id_path: self.id_path.clone(),
app_ref: self.app_ref.as_ref().unwrap().clone_box(),
@@ -93,6 +93,12 @@ impl Cx {
}
}
+impl Default for Cx {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
impl MessageThunk {
pub fn push_message(&self, message_body: impl Any + Send + 'static) {
let message = Message {
diff --git a/crates/xilem_svg/src/kurbo_shape.rs b/crates/xilem_svg/src/kurbo_shape.rs
index 9ad2486a1..30a266935 100644
--- a/crates/xilem_svg/src/kurbo_shape.rs
+++ b/crates/xilem_svg/src/kurbo_shape.rs
@@ -3,42 +3,16 @@
//! Implementation of the View trait for various kurbo shapes.
-use kurbo::{BezPath, Circle, Line, Rect};
+use peniko::kurbo::{BezPath, Circle, Line, Rect};
use web_sys::Element;
use xilem_core::{Id, MessageResult};
use crate::{
context::{ChangeFlags, Cx},
- pointer::PointerMsg,
view::{View, ViewMarker},
};
-pub trait KurboShape: Sized {
- fn class(self, class: impl Into) -> crate::class::Class {
- crate::class::class(self, class)
- }
-
- fn clicked(self, f: F) -> crate::clicked::Clicked
- where
- Self: View,
- {
- crate::clicked::clicked(self, f)
- }
-
- fn pointer(self, f: F) -> crate::pointer::Pointer
- where
- Self: View,
- {
- crate::pointer::pointer(self, f)
- }
-}
-
-impl KurboShape for Line {}
-impl KurboShape for Rect {}
-impl KurboShape for Circle {}
-impl KurboShape for BezPath {}
-
impl ViewMarker for Line {}
impl View for Line {
diff --git a/crates/xilem_svg/src/lib.rs b/crates/xilem_svg/src/lib.rs
index e7a733ab0..f8cb7d90a 100644
--- a/crates/xilem_svg/src/lib.rs
+++ b/crates/xilem_svg/src/lib.rs
@@ -1,14 +1,12 @@
// Copyright 2023 the Druid Authors.
// SPDX-License-Identifier: Apache-2.0
-//! A test program to exercise using xilem_core to generate SVG nodes that
-//! render in a browser.
-//!
-//! Run using `trunk serve`.
+//! An experimental library for making reactive SVG graphics.
mod app;
mod class;
mod clicked;
+mod common_attrs;
mod context;
mod group;
mod kurbo_shape;
@@ -16,88 +14,16 @@ mod pointer;
mod view;
mod view_ext;
-use app::App;
-use group::group;
-use kurbo::Rect;
-use kurbo_shape::KurboShape;
-use pointer::PointerMsg;
-use view::View;
-use wasm_bindgen::prelude::*;
+pub use peniko;
+pub use peniko::kurbo;
+
+pub use app::App;
+pub use context::Cx;
+pub use group::group;
+pub use pointer::{PointerDetails, PointerMsg};
+pub use view::{AnyView, Memoize, View, ViewMarker, ViewSequence};
+pub use view_ext::ViewExt;
pub use context::ChangeFlags;
xilem_core::message!(Send);
-
-#[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::>();
- group((
- Rect::new(100.0, 100.0, 200.0, 200.0).clicked(|_| {
- web_sys::console::log_1(&"app logic clicked".into());
- }),
- Rect::new(210.0, 100.0, 310.0, 200.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.)
- .pointer(|s: &mut AppState, msg| s.grab.handle(&mut s.x, &mut s.y, &msg)),
- group(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).clicked(|_| {
- web_sys::console::log_1(&"circle clicked".into());
- }),
- ))
- //button(format!("Count {}", count), |count| *count += 1)
-}
-
-// Called by our JS entry point to run the example
-#[wasm_bindgen(start)]
-pub fn run() -> Result<(), JsValue> {
- let app = App::new(AppState::default(), app_logic);
- app.run();
-
- Ok(())
-}
diff --git a/crates/xilem_svg/src/pointer.rs b/crates/xilem_svg/src/pointer.rs
index 0cebd2d05..7cdd29521 100644
--- a/crates/xilem_svg/src/pointer.rs
+++ b/crates/xilem_svg/src/pointer.rs
@@ -3,7 +3,7 @@
//! Interactivity with pointer events.
-use std::any::Any;
+use std::{any::Any, marker::PhantomData};
use wasm_bindgen::{prelude::Closure, JsCast};
use web_sys::PointerEvent;
@@ -15,9 +15,10 @@ use crate::{
view::{DomElement, View, ViewMarker},
};
-pub struct Pointer {
+pub struct Pointer {
child: V,
callback: F,
+ phantom: PhantomData,
}
pub struct PointerState {
@@ -32,6 +33,7 @@ pub struct PointerState {
}
#[derive(Debug)]
+/// A message representing a pointer event.
pub enum PointerMsg {
Down(PointerDetails),
Move(PointerDetails),
@@ -39,6 +41,7 @@ pub enum PointerMsg {
}
#[derive(Debug)]
+/// Details of a pointer event.
pub struct PointerDetails {
pub id: i32,
pub button: i16,
@@ -57,13 +60,20 @@ impl PointerDetails {
}
}
-pub fn pointer>(child: V, callback: F) -> Pointer {
- Pointer { child, callback }
+pub fn pointer>(
+ child: V,
+ callback: F,
+) -> Pointer {
+ Pointer {
+ child,
+ callback,
+ phantom: Default::default(),
+ }
}
-impl ViewMarker for Pointer {}
+impl ViewMarker for Pointer {}
-impl> View for Pointer {
+impl> View for Pointer {
type State = PointerState;
type Element = V::Element;
diff --git a/crates/xilem_svg/src/view_ext.rs b/crates/xilem_svg/src/view_ext.rs
index db6ea116e..edfd870c3 100644
--- a/crates/xilem_svg/src/view_ext.rs
+++ b/crates/xilem_svg/src/view_ext.rs
@@ -1,25 +1,36 @@
// Copyright 2023 the Druid Authors.
// SPDX-License-Identifier: Apache-2.0
+use peniko::Brush;
+
use crate::{
class::Class,
clicked::Clicked,
+ common_attrs::{Fill, Stroke},
pointer::{Pointer, PointerMsg},
view::View,
};
pub trait ViewExt: View + Sized {
- fn clicked(self, f: F) -> Clicked;
- fn pointer(self, f: F) -> Pointer {
+ fn clicked(self, f: F) -> Clicked {
+ crate::clicked::clicked(self, f)
+ }
+
+ fn pointer(self, f: F) -> Pointer {
crate::pointer::pointer(self, f)
}
- fn class(self, class: impl Into) -> Class {
+
+ fn class(self, class: impl Into) -> Class {
crate::class::class(self, class)
}
-}
-impl> ViewExt for V {
- fn clicked(self, f: F) -> Clicked {
- crate::clicked::clicked(self, f)
+ fn fill(self, brush: impl Into) -> Fill {
+ crate::common_attrs::fill(self, brush)
+ }
+
+ fn stroke(self, brush: impl Into, style: peniko::kurbo::Stroke) -> Stroke {
+ crate::common_attrs::stroke(self, brush, style)
}
}
+
+impl> ViewExt for V {}
diff --git a/crates/xilem_svg/web_examples/svgtoy/Cargo.toml b/crates/xilem_svg/web_examples/svgtoy/Cargo.toml
new file mode 100644
index 000000000..6e0fb2706
--- /dev/null
+++ b/crates/xilem_svg/web_examples/svgtoy/Cargo.toml
@@ -0,0 +1,12 @@
+[package]
+name = "svgtoy"
+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_svg = { path = "../.." }
diff --git a/crates/xilem_svg/web_examples/svgtoy/index.html b/crates/xilem_svg/web_examples/svgtoy/index.html
new file mode 100644
index 000000000..0bc7c6ac5
--- /dev/null
+++ b/crates/xilem_svg/web_examples/svgtoy/index.html
@@ -0,0 +1,19 @@
+
+
+
+
+
+
diff --git a/crates/xilem_svg/web_examples/svgtoy/src/main.rs b/crates/xilem_svg/web_examples/svgtoy/src/main.rs
new file mode 100644
index 000000000..06cc20b96
--- /dev/null
+++ b/crates/xilem_svg/web_examples/svgtoy/src/main.rs
@@ -0,0 +1,82 @@
+// Copyright 2023 the Druid Authors.
+// SPDX-License-Identifier: Apache-2.0
+
+use xilem_svg::{
+ group,
+ kurbo::{self, Rect},
+ peniko::Color,
+ App, PointerMsg, View, ViewExt,
+};
+
+#[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::>();
+ group((
+ Rect::new(100.0, 100.0, 200.0, 200.0).clicked(|_| {
+ 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)),
+ group(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).clicked(|_| {
+ web_sys::console::log_1(&"circle clicked".into());
+ }),
+ ))
+ //button(format!("Count {}", count), |count| *count += 1)
+}
+
+pub fn main() {
+ console_error_panic_hook::set_once();
+ let app = App::new(AppState::default(), app_logic);
+ app.run();
+}