Skip to content

Commit

Permalink
Dust off xilem_svg (#139)
Browse files Browse the repository at this point in the history
* Make xilem_svg into a library

Instead of having the demo app hardcoded, make it a library with a web_examples folder, just like its xilem_html sibling. This patch doesn't change the example or any functionality.

* Migrate KurboShape to ViewExt

Exports a `ViewExt` trait, and wires up types so that type inference can flow through the methods on this trait. That removes the hacky `KurboShape` workaround.

* clippy

* Add fill and stroke

Supports very basic fill and stroke (solid colors, stroke width but no other style parameters).

* rustfmt

* Address review feedback

Fix some cut'n'paste errors, and update README slightly.
  • Loading branch information
raphlinus authored Nov 5, 2023
1 parent 8a5825d commit 71d1db0
Show file tree
Hide file tree
Showing 16 changed files with 387 additions and 158 deletions.
41 changes: 35 additions & 6 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
5 changes: 1 addition & 4 deletions crates/xilem_svg/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
4 changes: 2 additions & 2 deletions crates/xilem_svg/README.md
Original file line number Diff line number Diff line change
@@ -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/
8 changes: 0 additions & 8 deletions crates/xilem_svg/index.html

This file was deleted.

12 changes: 7 additions & 5 deletions crates/xilem_svg/src/class.rs
Original file line number Diff line number Diff line change
@@ -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};

Expand All @@ -10,23 +10,25 @@ use crate::{
view::{DomElement, View, ViewMarker},
};

pub struct Class<V> {
pub struct Class<T, V> {
child: V,
// This could reasonably be static Cow also, but keep things simple
class: String,
phantom: PhantomData<T>,
}

pub fn class<V>(child: V, class: impl Into<String>) -> Class<V> {
pub fn class<T, V>(child: V, class: impl Into<String>) -> Class<T, V> {
Class {
child,
class: class.into(),
phantom: Default::default(),
}
}

impl<V> ViewMarker for Class<V> {}
impl<T, V> ViewMarker for Class<T, V> {}

// TODO: make generic over A (probably requires Phantom)
impl<T, V: View<T>> View<T> for Class<V> {
impl<T, V: View<T>> View<T> for Class<T, V> {
type State = V::State;
type Element = V::Element;

Expand Down
17 changes: 11 additions & 6 deletions crates/xilem_svg/src/clicked.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -13,9 +13,10 @@ use crate::{
view::{DomElement, View, ViewMarker},
};

pub struct Clicked<V, F> {
pub struct Clicked<T, V, F> {
child: V,
callback: F,
phantom: PhantomData<T>,
}

pub struct ClickedState<S> {
Expand All @@ -27,13 +28,17 @@ pub struct ClickedState<S> {

struct ClickedMsg;

pub fn clicked<T, F: Fn(&mut T), V: View<T>>(child: V, callback: F) -> Clicked<V, F> {
Clicked { child, callback }
pub fn clicked<T, F: Fn(&mut T), V: View<T>>(child: V, callback: F) -> Clicked<T, V, F> {
Clicked {
child,
callback,
phantom: Default::default(),
}
}

impl<V, F> ViewMarker for Clicked<V, F> {}
impl<T, V, F> ViewMarker for Clicked<T, V, F> {}

impl<T, F: Fn(&mut T) + Send, V: View<T>> View<T> for Clicked<V, F> {
impl<T, F: Fn(&mut T) + Send, V: View<T>> View<T> for Clicked<T, V, F> {
type State = ClickedState<V::State>;

type Element = V::Element;
Expand Down
163 changes: 163 additions & 0 deletions crates/xilem_svg/src/common_attrs.rs
Original file line number Diff line number Diff line change
@@ -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<T, V> {
child: V,
brush: Brush,
phantom: PhantomData<T>,
}

pub struct Stroke<T, V> {
child: V,
brush: Brush,
style: peniko::kurbo::Stroke,
phantom: PhantomData<T>,
}

pub fn fill<T, V>(child: V, brush: impl Into<Brush>) -> Fill<T, V> {
Fill {
child,
brush: brush.into(),
phantom: Default::default(),
}
}

pub fn stroke<T, V>(
child: V,
brush: impl Into<Brush>,
style: peniko::kurbo::Stroke,
) -> Stroke<T, V> {
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<T, V> ViewMarker for Fill<T, V> {}

// TODO: make generic over A (probably requires Phantom)
impl<T, V: View<T>> View<T> for Fill<T, V> {
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<dyn Any>,
app_state: &mut T,
) -> MessageResult<()> {
self.child.message(id_path, state, message, app_state)
}
}

impl<T, V> ViewMarker for Stroke<T, V> {}

// TODO: make generic over A (probably requires Phantom)
impl<T, V: View<T>> View<T> for Stroke<T, V> {
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<dyn Any>,
app_state: &mut T,
) -> MessageResult<()> {
self.child.message(id_path, state, message, app_state)
}
}
Loading

0 comments on commit 71d1db0

Please sign in to comment.