logicaffeine_web/ui/
drag.rs1#[derive(Clone, Copy, Debug, PartialEq, Eq)]
11pub enum Axis {
12 Columns,
14 Rows,
16}
17
18impl Axis {
19 pub fn root_class(self) -> &'static str {
21 match self {
22 Axis::Columns => "dragging-columns",
23 Axis::Rows => "dragging-rows",
24 }
25 }
26}
27
28pub const DRAG_STYLE: &str = r#"
31html.dragging, html.dragging * { user-select: none !important; -webkit-user-select: none !important; }
32html.dragging iframe { pointer-events: none !important; }
33html.dragging-columns, html.dragging-columns * { cursor: col-resize !important; }
34html.dragging-rows, html.dragging-rows * { cursor: row-resize !important; }
35"#;
36
37#[cfg(target_arch = "wasm32")]
38pub use live::begin;
39
40#[cfg(target_arch = "wasm32")]
41mod live {
42 use super::Axis;
43 use std::cell::RefCell;
44 use std::rc::Rc;
45 use wasm_bindgen::prelude::*;
46 use wasm_bindgen::JsCast;
47
48 const ENDS: [&str; 3] = ["pointerup", "pointercancel", "lostpointercapture"];
49
50 struct Live {
51 handle: web_sys::Element,
52 axis: Axis,
53 origin: (f64, f64),
54 latest: (f64, f64),
55 frame: Option<i32>,
56 on_frame: Box<dyn FnMut(f64, f64)>,
57 on_end: Option<Box<dyn FnOnce(f64, f64)>>,
58 on_move: Option<Closure<dyn FnMut(web_sys::PointerEvent)>>,
59 on_stop: Option<Closure<dyn FnMut(web_sys::PointerEvent)>>,
60 paint: Option<Closure<dyn FnMut()>>,
61 }
62
63 fn root() -> Option<web_sys::Element> {
64 web_sys::window()?.document()?.document_element()
65 }
66
67 pub fn begin(
71 handle: web_sys::Element,
72 pointer_id: i32,
73 origin: (f64, f64),
74 axis: Axis,
75 on_frame: impl FnMut(f64, f64) + 'static,
76 on_end: impl FnOnce(f64, f64) + 'static,
77 ) {
78 let _ = handle.set_pointer_capture(pointer_id);
79 if let Some(root) = root() {
80 let _ = root.class_list().add_2("dragging", axis.root_class());
81 }
82
83 let live = Rc::new(RefCell::new(Live {
84 handle: handle.clone(),
85 axis,
86 origin,
87 latest: origin,
88 frame: None,
89 on_frame: Box::new(on_frame),
90 on_end: Some(Box::new(on_end)),
91 on_move: None,
92 on_stop: None,
93 paint: None,
94 }));
95
96 let weak = Rc::downgrade(&live);
97 let paint = Closure::<dyn FnMut()>::new(move || {
98 let Some(live) = weak.upgrade() else { return };
99 let mut live = live.borrow_mut();
100 live.frame = None;
101 let (x, y) = live.latest;
102 let (ox, oy) = live.origin;
103 (live.on_frame)(x - ox, y - oy);
104 });
105
106 let weak = Rc::downgrade(&live);
107 let on_move = Closure::<dyn FnMut(web_sys::PointerEvent)>::new(move |e: web_sys::PointerEvent| {
108 let Some(live) = weak.upgrade() else { return };
109 let mut live = live.borrow_mut();
110 live.latest = (e.client_x() as f64, e.client_y() as f64);
111 if live.frame.is_none() {
112 let request = live
113 .paint
114 .as_ref()
115 .and_then(|paint| web_sys::window()?.request_animation_frame(paint.as_ref().unchecked_ref()).ok());
116 live.frame = request;
117 }
118 });
119
120 let weak = Rc::downgrade(&live);
121 let on_stop = Closure::<dyn FnMut(web_sys::PointerEvent)>::new(move |e: web_sys::PointerEvent| {
122 let Some(live) = weak.upgrade() else { return };
123 if e.type_() != "lostpointercapture" {
124 live.borrow_mut().latest = (e.client_x() as f64, e.client_y() as f64);
125 }
126 finish(live);
127 });
128
129 let _ = handle.add_event_listener_with_callback("pointermove", on_move.as_ref().unchecked_ref());
130 for end in ENDS {
131 let _ = handle.add_event_listener_with_callback(end, on_stop.as_ref().unchecked_ref());
132 }
133
134 let mut state = live.borrow_mut();
135 state.paint = Some(paint);
136 state.on_move = Some(on_move);
137 state.on_stop = Some(on_stop);
138 drop(state);
139 LIVE.with(|slot| *slot.borrow_mut() = Some(live));
142 }
143
144 thread_local! {
145 static LIVE: RefCell<Option<Rc<RefCell<Live>>>> = const { RefCell::new(None) };
146 }
147
148 fn finish(live: Rc<RefCell<Live>>) {
149 let (on_end, offset) = {
150 let mut state = live.borrow_mut();
151 let Some(on_end) = state.on_end.take() else { return };
152 if let (Some(id), Some(window)) = (state.frame.take(), web_sys::window()) {
153 let _ = window.cancel_animation_frame(id);
154 }
155 if let Some(on_move) = &state.on_move {
156 let _ = state.handle.remove_event_listener_with_callback("pointermove", on_move.as_ref().unchecked_ref());
157 }
158 if let Some(on_stop) = &state.on_stop {
159 for end in ENDS {
160 let _ = state.handle.remove_event_listener_with_callback(end, on_stop.as_ref().unchecked_ref());
161 }
162 }
163 if let Some(root) = root() {
164 let _ = root.class_list().remove_2("dragging", state.axis.root_class());
165 }
166 let (x, y) = state.latest;
167 let (ox, oy) = state.origin;
168 (on_end, (x - ox, y - oy))
169 };
170 on_end(offset.0, offset.1);
171 LIVE.with(|slot| {
174 let done = slot.borrow_mut().take();
175 wasm_bindgen_futures::spawn_local(async move { drop(done) });
176 });
177 drop(live);
178 }
179}
180
181#[cfg(test)]
182#[path = "../../../../crates/logicaffeine_tests/unit/logicaffeine_web/ui/drag/tests.rs"]
183mod tests;