mirror of https://github.com/linebender/xilem
xilem_web: Add `Rotate` and `Scale` (CSS) transform views (#621)
This sketches/implements (parts of) the transform modifier API described in [Add `Affine` transform to `Widget` trait?](https://xi.zulipchat.com/#narrow/stream/317477-masonry/topic/Add.20.60Affine.60.20transform.20to.20.60Widget.60.20trait.3F/near/472076600) for xilem_web. This currently includes `rotate` and `scale`, because there are less cases to handle for these [CSS transform functions](https://developer.mozilla.org/en-US/docs/Web/CSS/transform-function). In `rotate` I think we can reduce this to just radians (there are more units for `<angle>`: `turn`, `grad`, `deg` and `rad` (used here) but they can all be derived from radians), and percent in scale is kinda redundant (as `200%` == `2.0`)), for example `translate` has more cases as it includes all kinds of units (like `px`, `em`, `%` etc.), so this may require more thought (and is thus probably better for a future PR). This can be combined with untyped `transform: ...` as style, these modifier views extend the `transform` style (while the untyped `style(..)` overwrites previous set values). The `svgtoy` example is updated to include these views.
This commit is contained in:
parent
da92ba6cf0
commit
a4f88b7d5c
|
@ -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<State, Action = ()>:
|
|||
Attr::new(self, Cow::from("id"), value.into_attr_value())
|
||||
}
|
||||
|
||||
/// Set a style attribute
|
||||
fn style(self, style: impl IntoStyles) -> Style<Self, State, Action>
|
||||
where
|
||||
<Self::DomNode as DomNode>::Props: WithStyle,
|
||||
{
|
||||
let mut styles = vec![];
|
||||
style.into_styles(&mut styles);
|
||||
Style::new(self, styles)
|
||||
}
|
||||
|
||||
/// Add a `rotate(<radians>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:
|
||||
/// // <rect width="20" height="30" x="0.0" y="10.0" style="transform: translate(10px, 0) rotate(0.78539rad);"></rect>
|
||||
/// # }
|
||||
/// ```
|
||||
fn rotate(self, radians: f64) -> Rotate<Self, State, Action>
|
||||
where
|
||||
<Self::DomNode as DomNode>::Props: WithStyle,
|
||||
{
|
||||
Rotate::new(self, radians)
|
||||
}
|
||||
|
||||
/// Add a `scale(<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:
|
||||
/// // <circle r="30" cy="20" cx="10" style="transform: translate(10px, 0) scale(1.5) scale(1.5, 2);"></circle>
|
||||
/// # }
|
||||
/// ```
|
||||
fn scale(self, scale: impl Into<ScaleValue>) -> Scale<Self, State, Action>
|
||||
where
|
||||
<Self::DomNode as DomNode>::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<State, Action = ()>:
|
||||
Element<State, Action, DomNode: DomNode<Props: WithStyle> + AsRef<web_sys::HtmlElement>>
|
||||
{
|
||||
/// Set a style attribute
|
||||
fn style(self, style: impl IntoStyles) -> Style<Self, State, Action> {
|
||||
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<State, Action = ()>:
|
||||
Element<State, Action, DomNode: DomNode<Props: WithStyle> + AsRef<web_sys::SvgElement>>
|
||||
{
|
||||
/// Set a style attribute
|
||||
fn style(self, style: impl IntoStyles) -> Style<Self, State, Action> {
|
||||
let mut styles = vec![];
|
||||
style.into_styles(&mut styles);
|
||||
Style::new(self, styles)
|
||||
}
|
||||
// style_impls!();
|
||||
}
|
||||
|
||||
// #[cfg(feature = "SvgElement")]
|
||||
|
|
|
@ -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<T> AsRef<T> 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<N1, N2, N3, N4, N5, N6, N7, N8, N9> DomNode for OneOf<N1, N2, N3, N4, N5, N6, N7, N8, N9>
|
||||
|
|
|
@ -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<CowStr>);
|
||||
|
||||
// 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<CowStr>) {
|
||||
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<N: DomNode> WithStyle for Pod<N>
|
||||
|
@ -338,6 +376,14 @@ where
|
|||
fn set_style(&mut self, name: &CowStr, value: &Option<CowStr>) {
|
||||
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<N: DomNode> WithStyle for PodMut<'_, N>
|
||||
|
@ -355,6 +401,14 @@ where
|
|||
fn set_style(&mut self, name: &CowStr, value: &Option<CowStr>) {
|
||||
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(<radians>rad)` [transform-function](https://developer.mozilla.org/en-US/docs/Web/CSS/transform-function) to the current CSS `transform`
|
||||
pub struct Rotate<E, State, Action> {
|
||||
el: E,
|
||||
phantom: PhantomData<fn() -> (State, Action)>,
|
||||
radians: f64,
|
||||
}
|
||||
|
||||
impl<E, State, Action> Rotate<E, State, Action> {
|
||||
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<CowStr> {
|
||||
if let Some(transform) = transform {
|
||||
Some(CowStr::from(format!("{transform} rotate({radians}rad)")))
|
||||
} else {
|
||||
Some(CowStr::from(format!("rotate({radians}rad)")))
|
||||
}
|
||||
}
|
||||
|
||||
impl<E, T, A> ViewMarker for Rotate<E, T, A> {}
|
||||
impl<T, A, E> View<T, A, ViewCtx, DynMessage> for Rotate<E, T, A>
|
||||
where
|
||||
T: 'static,
|
||||
A: 'static,
|
||||
E: View<T, A, ViewCtx, DynMessage, Element: ElementWithStyle>,
|
||||
{
|
||||
type Element = E::Element;
|
||||
|
||||
type ViewState = (E::ViewState, Option<CowStr>);
|
||||
|
||||
fn build(&self, ctx: &mut ViewCtx) -> (Self::Element, Self::ViewState) {
|
||||
ctx.add_modifier_size_hint::<Styles>(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<A, DynMessage> {
|
||||
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<f64> 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<Vec2> for ScaleValue {
|
||||
fn from(value: Vec2) -> Self {
|
||||
ScaleValue::NonUniform(value.x, value.y)
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a `rotate(<radians>rad)` [transform-function](https://developer.mozilla.org/en-US/docs/Web/CSS/transform-function) to the current CSS `transform`
|
||||
pub struct Scale<E, State, Action> {
|
||||
el: E,
|
||||
phantom: PhantomData<fn() -> (State, Action)>,
|
||||
scale: ScaleValue,
|
||||
}
|
||||
|
||||
impl<E, State, Action> Scale<E, State, Action> {
|
||||
pub(crate) fn new(element: E, scale: impl Into<ScaleValue>) -> Self {
|
||||
Scale {
|
||||
el: element,
|
||||
phantom: PhantomData,
|
||||
scale: scale.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn modify_scale_transform(transform: Option<&CowStr>, scale: ScaleValue) -> Option<CowStr> {
|
||||
if let Some(transform) = transform {
|
||||
Some(CowStr::from(format!("{transform} scale({scale})")))
|
||||
} else {
|
||||
Some(CowStr::from(format!("scale({scale})")))
|
||||
}
|
||||
}
|
||||
|
||||
impl<E, T, A> ViewMarker for Scale<E, T, A> {}
|
||||
impl<T, A, E> View<T, A, ViewCtx, DynMessage> for Scale<E, T, A>
|
||||
where
|
||||
T: 'static,
|
||||
A: 'static,
|
||||
E: View<T, A, ViewCtx, DynMessage, Element: ElementWithStyle>,
|
||||
{
|
||||
type Element = E::Element;
|
||||
|
||||
type ViewState = (E::ViewState, Option<CowStr>);
|
||||
|
||||
fn build(&self, ctx: &mut ViewCtx) -> (Self::Element, Self::ViewState) {
|
||||
ctx.add_modifier_size_hint::<Styles>(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<A, DynMessage> {
|
||||
self.el.message(view_state, id_path, message, app_state)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<AppState> {
|
||||
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::<Vec<_>>();
|
||||
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<AppState> {
|
|||
}),
|
||||
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());
|
||||
}),
|
||||
)))
|
||||
|
|
Loading…
Reference in New Issue