Skip to main content

hadrone_dioxus/
lib.rs

1use dioxus::prelude::keyboard_types::Key;
2use dioxus::prelude::*;
3use hadrone_core::interaction::{InteractionSession, InteractionType};
4use hadrone_core::{
5    CollisionStrategy, CompactionType, Compactor, FreePlacementCompactor, InteractionPhase,
6    LayoutEngine, LayoutEvent, LayoutItem, ResizeHandle, RisingTideCompactor,
7    resize_handle_aria_label,
8};
9use std::time::Duration;
10
11fn apply_keyboard_cell_nudge(
12    mut layout: Signal<Vec<LayoutItem>>,
13    cols: i32,
14    compaction: CompactionType,
15    item_id: &str,
16    dx: i32,
17    dy: i32,
18) {
19    let mut l = layout.peek().clone();
20    let Some((nx, ny)) = l
21        .iter()
22        .find(|i| i.id == item_id)
23        .filter(|it| it.can_drag())
24        .map(|it| (it.x + dx, it.y + dy))
25    else {
26        return;
27    };
28    let compactor: Box<dyn Compactor> = match compaction {
29        CompactionType::Gravity => Box::new(RisingTideCompactor),
30        CompactionType::FreePlacement => Box::new(FreePlacementCompactor),
31    };
32    let engine = LayoutEngine::with_default_collision(compactor, cols);
33    engine.move_element(&mut l, item_id, nx, ny);
34    layout.set(l);
35}
36
37#[cfg(target_arch = "wasm32")]
38fn wasm_attach_resize_width_observer(el: web_sys::Element, mut width: Signal<f32>) {
39    use wasm_bindgen::JsCast;
40    use wasm_bindgen::closure::Closure;
41
42    let initial = el.client_width() as f32;
43    if initial > 0.0 {
44        width.set(initial);
45    }
46
47    let el_measure = el.clone();
48    let closure = Closure::wrap(Box::new(
49        move |_entries: js_sys::Array, _obs: web_sys::ResizeObserver| {
50            let w = el_measure.client_width() as f32;
51            if w > 0.0 {
52                width.set(w);
53            }
54        },
55    )
56        as Box<dyn FnMut(js_sys::Array, web_sys::ResizeObserver)>);
57
58    if let Ok(obs) = web_sys::ResizeObserver::new(closure.as_ref().unchecked_ref()) {
59        obs.observe(&el);
60    }
61    closure.forget();
62}
63
64/// Configuration for the grid system layout.
65#[derive(Clone, Copy, Debug, PartialEq)]
66pub struct GridConfig {
67    /// Number of columns in the grid.
68    pub cols: i32,
69    /// Height of each row in pixels.
70    pub row_height: f32,
71    /// Vertical and horizontal margin between items (x, y).
72    pub margin: (i32, i32),
73    /// Padding inside the container around the grid (matches [`GridLayoutProps::container_padding`]).
74    pub container_padding: (i32, i32),
75}
76
77/// Props for the [GridLayout] component.
78#[derive(Props, Clone, PartialEq)]
79#[allow(unpredictable_function_pointer_comparisons)]
80pub struct GridLayoutProps {
81    /// Reactive signal containing the current arrangement of items.
82    pub layout: Signal<Vec<LayoutItem>>,
83    /// Fixed number of columns for the grid.
84    /// Change this to trigger a re-render and re-compaction of the layout.
85    pub cols: i32,
86    /// Base height for a single grid row.
87    pub row_height: f32,
88    /// Spacing between items (horizontal, vertical).
89    pub margin: (i32, i32),
90    /// Strategy for compacting items (Gravity or FreePlacement).
91    pub compaction: CompactionType,
92    /// Render function for the visual content of each item.
93    pub render_item: fn(LayoutItem) -> Element,
94    /// Optional event handler triggered after an item is moved or resized.
95    pub on_layout_change: Option<EventHandler<Vec<LayoutItem>>>,
96    /// Padding inside the container around the grid content (px), for visual alignment with CSS.
97    #[props(default = (0, 0))]
98    pub container_padding: (i32, i32),
99    /// Collision policy while dragging/resizing.
100    #[props(default = CollisionStrategy::PushDown)]
101    pub collision_strategy: CollisionStrategy,
102    /// Structured lifecycle events (start/stop/update).
103    pub on_layout_event: Option<EventHandler<LayoutEvent>>,
104    /// When true and `on_layout_event` is set, emit [`InteractionPhase::Update`] on every pointer move.
105    #[props(default = false)]
106    pub emit_interaction_updates: bool,
107    /// When true, arrow keys move the focused widget by one grid cell (focus the widget body first).
108    #[props(default = false)]
109    pub keyboard_cell_nudge: bool,
110}
111
112/// The primary container for the grid layout system.
113///
114/// This component manages the spatial distribution of [GridItem] children and handles
115/// all drag/resize interactions using high-performance pointer capture.
116#[component]
117pub fn GridLayout(props: GridLayoutProps) -> Element {
118    let mut layout = props.layout;
119    let compaction = props.compaction;
120    let collision_strategy = props.collision_strategy;
121    let emit_interaction_updates = props.emit_interaction_updates;
122    let on_layout_event = props.on_layout_event;
123    let container_pad = props.container_padding;
124    let config = GridConfig {
125        cols: props.cols,
126        row_height: props.row_height,
127        margin: props.margin,
128        container_padding: container_pad,
129    };
130
131    let mut active = use_signal(|| None::<InteractionSession>);
132    let mut visual_delta = use_signal(|| None::<(f32, f32, f32, f32)>);
133    let container_width = use_signal(|| 1200.0);
134
135    // Style for the container
136    let is_active = active.read().is_some();
137
138    // Track container width: ResizeObserver on wasm; polling eval on native/desktop renderers.
139    #[cfg(not(target_arch = "wasm32"))]
140    use_effect(move || {
141        let mut width = container_width;
142        spawn(async move {
143            loop {
144                if let Ok(eval) =
145                    document::eval("document.querySelector('.hadrone-container')?.clientWidth")
146                        .await
147                    && let Some(w) = eval.as_f64()
148                {
149                    width.set(w as f32);
150                }
151                tokio::time::sleep(Duration::from_millis(500)).await;
152            }
153        });
154    });
155
156    let total_height = use_memo(move || {
157        let max_y = layout
158            .read()
159            .iter()
160            .map(|item| item.y + item.h)
161            .max()
162            .unwrap_or(0);
163        (max_y as f32 * (props.row_height + props.margin.1 as f32)).max(500.0)
164    });
165    let container_style = format!(
166        "position: relative; width: 100%; height: {h}px; contain: layout; touch-action: none; user-select: none; \
167         box-sizing: border-box; padding-left: {pad_x}px; padding-top: {pad_y}px; \
168         --grid-cols: {cols}; --row-height: {row_height}px; --margin-x: {mx}px; --margin-y: {my}px;",
169        h = total_height(),
170        pad_x = container_pad.0,
171        pad_y = container_pad.1,
172        cols = props.cols,
173        row_height = props.row_height,
174        mx = props.margin.0,
175        my = props.margin.1
176    );
177
178    // Auto-Compaction on column change or compaction strategy change
179    use_effect(move || {
180        // Skip reactive compaction during interactions to avoid double-renders
181        if active.peek().is_some() {
182            return;
183        }
184
185        let mut current_layout = layout.peek().clone();
186        let compactor: Box<dyn Compactor> = match compaction {
187            CompactionType::Gravity => Box::new(RisingTideCompactor),
188            CompactionType::FreePlacement => Box::new(FreePlacementCompactor),
189        };
190        let engine = LayoutEngine::with_default_collision(compactor, props.cols);
191
192        // Ensure no items are wider than the grid
193        for item in current_layout.iter_mut() {
194            if !item.is_static {
195                item.w = item.w.min(props.cols);
196                item.x = item.x.max(0).min(props.cols - item.w);
197            }
198        }
199
200        engine.compact(&mut current_layout);
201        layout.set(current_layout);
202    });
203
204    // Clone for stable iteration in RSX
205    let current_layout = layout.read().clone();
206    let interaction_active = active.read().is_some();
207    let keyboard_cell_nudge = props.keyboard_cell_nudge;
208
209    rsx! {
210        div {
211            class: "hadrone-container",
212            style: "{container_style}",
213            "data-active": "{is_active}",
214            role: "application",
215            aria_label: "Draggable grid layout. Use Tab to reach widgets and resize handles. Arrow keys move the focused widget when keyboard nudge is enabled.",
216            onmounted: move |evt| {
217                #[cfg(target_arch = "wasm32")]
218                if let Some(el) = evt.data().downcast::<web_sys::Element>() {
219                    wasm_attach_resize_width_observer(el.clone(), container_width);
220                }
221                #[cfg(not(target_arch = "wasm32"))]
222                drop(evt);
223            },
224
225            // Unified Event Handlers on the Container
226            onpointermove: move |e: Event<PointerData>| {
227                if let Some(interaction) = active.read().as_ref() {
228                    let coords = e.data.client_coordinates();
229                    visual_delta.set(Some(interaction.get_visual_delta((coords.x as f32, coords.y as f32))));
230
231                    // --- Optimized Auto-Scroll (calculated in Rust) ---
232                    #[cfg(target_arch = "wasm32")]
233                    {
234                        let y = coords.y as f32;
235                        if y < 100.0 {
236                            let _ = document::eval("window.scrollBy(0, -10)");
237                        } else {
238                            let _ = document::eval(&format!(r#"if (window.innerHeight - {} < 100) window.scrollBy(0, 10);"#, y));
239                        }
240                    }
241
242                    let mut new_layout = layout.peek().clone();
243                    interaction.update(
244                        (coords.x as f32, coords.y as f32),
245                        &mut new_layout,
246                        config.cols,
247                    );
248
249                    if new_layout != *layout.peek() {
250                        layout.set(new_layout);
251                    }
252
253                    if emit_interaction_updates
254                        && let Some(ref h) = on_layout_event
255                        && let Some(interaction) = active.read().as_ref()
256                    {
257                        h.call(LayoutEvent::Interaction {
258                            phase: InteractionPhase::Update,
259                            id: interaction.id.clone(),
260                            interaction: interaction.interaction_type,
261                            layout: layout.peek().clone(),
262                            compaction,
263                            collision: collision_strategy,
264                        });
265                    }
266                }
267            },
268            onpointerup: move |e| {
269                let ended = active.read().as_ref().cloned();
270                if let Some(interaction) = ended {
271                    if let Some(ref h) = on_layout_event {
272                        h.call(LayoutEvent::Interaction {
273                            phase: InteractionPhase::Stop,
274                            id: interaction.id.clone(),
275                            interaction: interaction.interaction_type,
276                            layout: layout.peek().clone(),
277                            compaction,
278                            collision: collision_strategy,
279                        });
280                    }
281                    let pid = e.data.pointer_id();
282                    let _ = document::eval(&format!(r#"
283                        const container = document.querySelector(".hadrone-container[data-active='true']");
284                        if (container) container.releasePointerCapture({});
285                    "#, pid));
286                    active.set(None);
287                    visual_delta.set(None);
288                }
289            },
290            onpointerleave: move |_| {
291                let ended = active.read().as_ref().cloned();
292                if let Some(interaction) = ended {
293                    if let Some(ref h) = on_layout_event {
294                        h.call(LayoutEvent::Interaction {
295                            phase: InteractionPhase::Cancel,
296                            id: interaction.id.clone(),
297                            interaction: interaction.interaction_type,
298                            layout: layout.peek().clone(),
299                            compaction,
300                            collision: collision_strategy,
301                        });
302                    }
303                    active.set(None);
304                    visual_delta.set(None);
305                }
306            },
307            onpointercancel: move |_| {
308                let ended = active.read().as_ref().cloned();
309                if let Some(interaction) = ended {
310                    if let Some(ref h) = on_layout_event {
311                        h.call(LayoutEvent::Interaction {
312                            phase: InteractionPhase::Cancel,
313                            id: interaction.id.clone(),
314                            interaction: interaction.interaction_type,
315                            layout: layout.peek().clone(),
316                            compaction,
317                            collision: collision_strategy,
318                        });
319                    }
320                    active.set(None);
321                    visual_delta.set(None);
322                }
323            },
324
325            // Global styles for handle hover and transitions
326            style {
327                r#"
328                .resize-handle {{ opacity: 0; pointer-events: none; transition: opacity 0.15s ease-in-out; }}
329                .grid-item:hover .resize-handle {{ opacity: 1; pointer-events: auto; }}
330                .hadrone-container[data-active="true"] {{ cursor: grabbing !important; }}
331                .hadrone-container[data-active="true"] .grid-item:not([data-active="true"]) .resize-handle {{ opacity: 0 !important; pointer-events: none !important; }}
332                .grid-item[data-active="true"] .resize-handle {{ opacity: 1 !important; pointer-events: auto !important; }}
333                .grid-item-inner:focus-visible {{ outline: 2px solid #2563eb; outline-offset: 2px; }}
334                .resize-handle:focus-visible {{ opacity: 1 !important; pointer-events: auto !important; outline: 2px solid #2563eb; outline-offset: 2px; }}
335                "#
336            }
337
338            for item in current_layout {
339                {
340                    let item_drag = item.clone();
341                    let item_resize = item.clone();
342
343                    let active_ref = active.read();
344                    let is_active = active_ref.as_ref().is_some_and(|a| a.id == item.id);
345
346                    rsx! {
347                        GridItem {
348                            key: "{item.id}",
349                            item: item.clone(),
350                            config,
351                            is_active,
352                            start_rect: if is_active { active_ref.as_ref().map(|a| a.start_rect) } else { None },
353                            visual_delta: if is_active { visual_delta() } else { None },
354                            render_item: props.render_item,
355                            layout,
356                            keyboard_cell_nudge,
357                            compaction,
358                            interaction_active,
359                            on_drag_start: move |e: Event<PointerData>| {
360                                if !item_drag.can_drag() {
361                                    return;
362                                }
363                                let pid = e.data.pointer_id();
364                                let _ = document::eval(&format!(r#"
365                                    const container = document.querySelector(".hadrone-container");
366                                    if (container) container.setPointerCapture({});
367                                "#, pid));
368
369                                let start_mouse = (e.data.client_coordinates().x as f32, e.data.client_coordinates().y as f32);
370                                let session = InteractionSession {
371                                    id: item_drag.id.clone(),
372                                    start_mouse,
373                                    start_rect: (item_drag.x, item_drag.y, item_drag.w, item_drag.h),
374                                    interaction_type: InteractionType::Drag,
375                                    handle: ResizeHandle::SouthEast,
376                                    col_width_px: container_width() / config.cols as f32,
377                                    row_height_px: config.row_height,
378                                    margin: config.margin,
379                                    container_padding: config.container_padding,
380                                    compaction,
381                                    collision: collision_strategy,
382                                };
383
384                                visual_delta.set(Some(session.get_visual_delta(start_mouse)));
385                                active.set(Some(session));
386                                if let Some(ref h) = on_layout_event {
387                                    h.call(LayoutEvent::Interaction {
388                                        phase: InteractionPhase::Start,
389                                        id: item_drag.id.clone(),
390                                        interaction: InteractionType::Drag,
391                                        layout: layout.peek().clone(),
392                                        compaction,
393                                        collision: collision_strategy,
394                                    });
395                                }
396                            },
397                            on_resize_start: move |(e, handle): (Event<PointerData>, ResizeHandle)| {
398                                if !item_resize.can_resize() {
399                                    return;
400                                }
401                                let pid = e.data.pointer_id();
402                                let _ = document::eval(&format!(r#"
403                                    const container = document.querySelector(".hadrone-container");
404                                    if (container) container.setPointerCapture({});
405                                "#, pid));
406
407                                let start_mouse = (e.data.client_coordinates().x as f32, e.data.client_coordinates().y as f32);
408                                let session = InteractionSession {
409                                    id: item_resize.id.clone(),
410                                    start_mouse,
411                                    start_rect: (item_resize.x, item_resize.y, item_resize.w, item_resize.h),
412                                    interaction_type: InteractionType::Resize,
413                                    handle,
414                                    col_width_px: container_width() / config.cols as f32,
415                                    row_height_px: config.row_height,
416                                    margin: config.margin,
417                                    container_padding: config.container_padding,
418                                    compaction,
419                                    collision: collision_strategy,
420                                };
421
422                                visual_delta.set(Some(session.get_visual_delta(start_mouse)));
423                                active.set(Some(session));
424                                if let Some(ref h) = on_layout_event {
425                                    h.call(LayoutEvent::Interaction {
426                                        phase: InteractionPhase::Start,
427                                        id: item_resize.id.clone(),
428                                        interaction: InteractionType::Resize,
429                                        layout: layout.peek().clone(),
430                                        compaction,
431                                        collision: collision_strategy,
432                                    });
433                                }
434                            }
435                        }
436                    }
437                }
438            }
439        }
440    }
441}
442
443pub type PointerEvent = Event<PointerData>;
444
445#[derive(Props, Clone, PartialEq)]
446#[allow(unpredictable_function_pointer_comparisons)]
447pub struct GridItemProps {
448    pub item: LayoutItem,
449    pub config: GridConfig,
450    pub is_active: bool,
451    pub start_rect: Option<(i32, i32, i32, i32)>,
452    pub visual_delta: Option<(f32, f32, f32, f32)>,
453    pub layout: Signal<Vec<LayoutItem>>,
454    pub keyboard_cell_nudge: bool,
455    pub compaction: CompactionType,
456    pub interaction_active: bool,
457    pub render_item: fn(LayoutItem) -> Element,
458    pub on_drag_start: EventHandler<PointerEvent>,
459    pub on_resize_start: EventHandler<(PointerEvent, ResizeHandle)>,
460}
461
462/// A single interactive unit within the grid.
463#[component]
464pub fn GridItem(props: GridItemProps) -> Element {
465    let item = props.item.clone();
466    let item_id = item.id.clone();
467    let config = props.config;
468    let layout_sig = props.layout;
469    let keyboard_cell_nudge = props.keyboard_cell_nudge;
470    let compaction = props.compaction;
471    let interaction_active = props.interaction_active;
472
473    let mut x_anim = use_animation(item.x as f32, Duration::from_millis(200));
474    let mut y_anim = use_animation(item.y as f32, Duration::from_millis(200));
475    let mut w_anim = use_animation(item.w as f32, Duration::from_millis(200));
476    let mut h_anim = use_animation(item.h as f32, Duration::from_millis(200));
477
478    use_effect(move || {
479        x_anim.set(item.x as f32);
480        y_anim.set(item.y as f32);
481        w_anim.set(item.w as f32);
482        h_anim.set(item.h as f32);
483    });
484
485    let col_width_pct = 100.0 / config.cols as f32;
486
487    let (left_str, top_str, width_str, height_str) = if let (
488        Some((dx, dy, dw, dh)),
489        Some(start_rect),
490    ) = (props.visual_delta, props.start_rect)
491    {
492        let start_left_pct = start_rect.0 as f32 * col_width_pct;
493        let start_top_px = start_rect.1 as f32 * (config.row_height + config.margin.1 as f32);
494        let start_width_pct = start_rect.2 as f32 * col_width_pct;
495        let start_height_px = start_rect.3 as f32 * config.row_height
496            + (start_rect.3 as f32 - 1.0) * config.margin.1 as f32;
497
498        (
499            format!("calc({}% + {}px)", start_left_pct, dx),
500            format!("{}px", start_top_px + dy),
501            format!(
502                "calc({}% - {}px + {}px)",
503                start_width_pct, config.margin.0, dw
504            ),
505            format!("{}px", start_height_px + dh),
506        )
507    } else {
508        (
509            format!("{}%", x_anim.value() * col_width_pct),
510            format!(
511                "{}px",
512                y_anim.value() * (config.row_height + config.margin.1 as f32)
513            ),
514            format!(
515                "calc({}% - {}px)",
516                w_anim.value() * col_width_pct,
517                config.margin.0
518            ),
519            format!(
520                "{}px",
521                h_anim.value() * config.row_height
522                    + (h_anim.value() - 1.0) * config.margin.1 as f32
523            ),
524        )
525    };
526
527    let transform = if props.is_active {
528        "scale(1.025) translate3d(0, 0, 0)"
529    } else {
530        "scale(1.0) translate3d(0, 0, 0)"
531    };
532
533    let style = format!(
534        "position: absolute; \
535         left: {left_str}; \
536         top: {top_str}; \
537         width: {width_str}; \
538         height: {height_str}; \
539         z-index: {z}; \
540         pointer-events: auto; \
541         transform: {transform}; \
542         transition: transform 0.15s ease-out; \
543         touch-action: none; \
544         user-select: none;",
545        z = if props.is_active { 100 } else { 0 }
546    );
547
548    let grabbed = if props.is_active { "true" } else { "false" };
549    let aria_item = format!("Widget {}, draggable grid item", item.id);
550
551    rsx! {
552        div {
553            class: "grid-item",
554            style: "{style}",
555            "data-active": "{props.is_active}",
556            div {
557                class: "grid-item-inner",
558                style: "width: 100%; height: 100%; position: relative;",
559                tabindex: 0,
560                role: "group",
561                aria_label: "{aria_item}",
562                aria_grabbed: "{grabbed}",
563                onpointerdown: move |e| props.on_drag_start.call(e),
564                onkeydown: move |e: Event<KeyboardData>| {
565                    if !keyboard_cell_nudge || interaction_active {
566                        return;
567                    }
568                    let (dx, dy) = match e.key() {
569                        Key::ArrowLeft => (-1, 0),
570                        Key::ArrowRight => (1, 0),
571                        Key::ArrowUp => (0, -1),
572                        Key::ArrowDown => (0, 1),
573                        _ => return,
574                    };
575                    e.prevent_default();
576                    e.stop_propagation();
577                    apply_keyboard_cell_nudge(
578                        layout_sig,
579                        config.cols,
580                        compaction,
581                        &item_id,
582                        dx,
583                        dy,
584                    );
585                },
586
587                { (props.render_item)(item.clone()) }
588            }
589            for handle in item
590                .resize_handles
591                .iter()
592                .cloned()
593                .filter(|h| {
594                    item.can_resize()
595                        && matches!(
596                            h,
597                            ResizeHandle::SouthEast | ResizeHandle::South | ResizeHandle::East
598                        )
599                })
600            {
601                ResizeHandleComponent {
602                    handle,
603                    is_active: props.is_active,
604                    on_pointerdown: move |e| props.on_resize_start.call((e, handle)),
605                }
606            }
607        }
608    }
609}
610
611#[component]
612fn ResizeHandleComponent(
613    handle: ResizeHandle,
614    is_active: bool,
615    on_pointerdown: EventHandler<PointerEvent>,
616) -> Element {
617    let (style, content, z) = match handle {
618        ResizeHandle::SouthEast => (
619            "bottom: -8px; right: -8px; cursor: nwse-resize; width: 40px; height: 40px; display: flex; align-items: flex-end; justify-content: flex-end; padding: 12px;",
620            rsx! {
621                svg {
622                    width: "14",
623                    height: "14",
624                    view_box: "0 0 12 12",
625                    style: "opacity: 0.2; pointer-events: none;",
626                    path { d: "M10 2 L10 10 L2 10 Z", fill: "currentColor" }
627                }
628            },
629            20,
630        ),
631        ResizeHandle::South => (
632            "bottom: -8px; left: 10px; right: 30px; height: 16px; cursor: ns-resize; display: flex; justify-content: center; align-items: center;",
633            rsx! { div { style: "width: 40px; height: 4px; background: transparent; border-radius: 2px;" } },
634            10,
635        ),
636        ResizeHandle::East => (
637            "top: 10px; bottom: 30px; right: -8px; width: 16px; cursor: ew-resize; display: flex; align-items: center; justify-content: center;",
638            rsx! { div { style: "width: 4px; height: 40px; background: transparent; border-radius: 2px;" } },
639            10,
640        ),
641        _ => return rsx! {},
642    };
643
644    let active_style = if is_active {
645        "opacity: 1 !important; pointer-events: auto !important;"
646    } else {
647        ""
648    };
649    let label = resize_handle_aria_label(handle);
650
651    rsx! {
652        div {
653            class: "resize-handle",
654            style: "position: absolute; {style}; touch-action: none; z-index: {z}; {active_style}",
655            tabindex: 0,
656            role: "button",
657            aria_label: "{label}",
658            onpointerdown: move |e| on_pointerdown.call(e),
659            {content}
660        }
661    }
662}
663
664fn use_animation(target: f32, _duration: std::time::Duration) -> Animation {
665    let mut value = use_signal(|| target);
666    let mut last_target = use_signal(|| target);
667
668    if target != *last_target.read() {
669        value.set(target);
670        last_target.set(target);
671    }
672
673    Animation { value }
674}
675
676#[derive(Clone, Copy)]
677struct Animation {
678    value: Signal<f32>,
679}
680
681impl Animation {
682    fn value(&self) -> f32 {
683        *self.value.read()
684    }
685    fn set(&mut self, target: f32) {
686        self.value.set(target)
687    }
688}