1use blockworx_canvas2d::input::{Edge, Phase, Sample, chord_of, dom};
9use blockworx_kernel::Event;
10use blockworx_paint::Button;
11use blockworx_tools::commands::CommandId;
12use blockworx_tools::tool::Action;
13use dioxus::prelude::*;
14use dioxus::web::WebEventExt as _;
15use wasm_bindgen::JsCast as _;
16use wasm_bindgen::prelude::Closure;
17use web_sys::{HtmlCanvasElement, KeyboardEvent, PointerEvent};
18
19use crate::exchange::Zone;
20use crate::shell::{Escaped, Shell, types_into};
21
22#[component]
28pub fn Canvas(shell: Shell) -> Element {
29 let shell = use_hook(|| CopyValue::new(shell));
32 rsx! {
33 canvas {
34 class: "bw-canvas absolute inset-0 h-full w-full touch-none select-none outline-none \
35 transition-[filter] group-data-[viewing=true]:saturate-50",
36 tabindex: "0",
37 onmounted: move |event| {
38 if let Some(canvas) = event
39 .try_as_web_event()
40 .and_then(|element| element.dyn_into::<HtmlCanvasElement>().ok())
41 {
42 shell.read().mounted(canvas);
43 }
44 },
45 onresize: move |_| shell.read().resized(),
46 oncontextmenu: move |event| event.prevent_default(),
49 ondragover: move |event| {
54 if let Some(native) = event.try_as_web_event() {
55 native.prevent_default();
56 dragged(&shell.read(), &native);
57 }
58 },
59 ondrop: move |event| {
60 let Some(native) = event.try_as_web_event() else {
61 return;
62 };
63 native.prevent_default();
64 dragged(&shell.read(), &native);
65 if let Some(file) = crate::exchange::carried_by(&native) {
66 crate::exchange::drops(&shell.read(), Zone::Diagram, &file);
67 }
68 },
69 onpointerdown: move |event| {
70 if let Some(native) = event.try_as_web_event() {
71 if dom::button_of(native.button()) == Some(Button::Middle) {
77 native.prevent_default();
78 shell.read().provoked();
79 }
80 shell.read().works();
84 shell.read().grabs(native.pointer_id());
85 pointed(&shell.read(), &native, Phase::Down);
86 }
87 },
88 onpointermove: move |event| pointed_by(&shell.read(), &event, Phase::Moved),
89 onpointerup: move |event| pointed_by(&shell.read(), &event, Phase::Up),
90 onpointerleave: move |event| pointed_by(&shell.read(), &event, Phase::Left),
91 onpointercancel: move |event| pointed_by(&shell.read(), &event, Phase::Cancelled),
92 onwheel: move |event| {
93 let Some(native) = event.try_as_web_event() else {
94 return;
95 };
96 native.prevent_default();
99 let shell = shell.read();
100 if let Some(origin) = shell.origin() {
101 shell.sampled(Sample::Wheel(dom::wheel(&native, origin)));
102 }
103 },
104 }
105 }
106}
107
108fn dragged(shell: &Shell, native: &web_sys::DragEvent) {
112 if let Some(origin) = shell.origin() {
113 shell.sampled(Sample::Pointer(dom::dragged(native, origin)));
114 }
115}
116
117fn pointed_by(shell: &Shell, event: &dioxus::core::Event<PointerData>, phase: Phase) {
118 if let Some(native) = event.try_as_web_event() {
119 pointed(shell, &native, phase);
120 }
121}
122
123fn pointed(shell: &Shell, native: &PointerEvent, phase: Phase) {
124 let Some(origin) = shell.origin() else {
125 return;
126 };
127 shell.sampled(Sample::Pointer(dom::pointer(native, phase, origin)));
128}
129
130pub fn listens(shell: &Shell) {
135 let Some(document) = crate::shell::window().and_then(|window| window.document()) else {
136 return;
137 };
138 let on = |kind: &str, heard: Box<dyn FnMut(web_sys::Event)>| {
139 let heard = Closure::wrap(heard);
140 if document
141 .add_event_listener_with_callback(kind, heard.as_ref().unchecked_ref())
142 .is_ok()
143 {
144 heard.forget();
145 }
146 };
147 for (kind, edge) in [("keydown", Edge::Down), ("keyup", Edge::Up)] {
148 let shell = shell.clone();
149 on(
150 kind,
151 Box::new(move |event| {
152 if let Some(native) = event.dyn_ref::<KeyboardEvent>() {
153 keyed_by(&shell, native, edge);
154 }
155 }),
156 );
157 }
158 let pasting = shell.clone();
159 on("paste", Box::new(move |event| pasted_on(&pasting, &event)));
160 for (kind, id) in [("copy", CommandId::Copy), ("cut", CommandId::Cut)] {
161 let shell = shell.clone();
162 on(kind, Box::new(move |event| clipped_on(&shell, &event, id)));
163 }
164}
165
166fn keyed_by(shell: &Shell, native: &KeyboardEvent, edge: Edge) {
169 if edge == Edge::Down {
173 shell.asked();
174 }
175 if edge == Edge::Down && (kept_by_a_control(native) || typed_into(native)) {
176 return;
177 }
178 keyed(shell, native, edge);
179}
180
181fn kept_by_a_control(native: &KeyboardEvent) -> bool {
185 native.default_prevented() && (native.key() != "Escape" || opened_around(native))
186}
187
188fn opened_around(native: &web_sys::Event) -> bool {
191 native
192 .target()
193 .and_then(|target| target.dyn_into::<web_sys::Element>().ok())
194 .and_then(|element| element.closest("[data-state=open]").ok().flatten())
195 .is_some()
196}
197
198fn keyed(shell: &Shell, native: &KeyboardEvent, edge: Edge) {
201 if edge == Edge::Down && native.key() == "Escape" && shell.escaped() == Escaped::Claimed {
204 native.prevent_default();
205 return;
206 }
207 if let Some(sample) = dom::key(native, edge) {
208 native.prevent_default();
211 shell.sampled(Sample::Key(sample));
212 return;
213 }
214 if edge != Edge::Down {
215 return;
216 }
217 let Some(id) = chord_of(native).and_then(|chord| shell.chrome().peek().commands.bound(chord))
218 else {
219 return;
220 };
221 native.prevent_default();
222 shell.say(Event::Command(id));
223}
224
225fn clipped_on(shell: &Shell, native: &web_sys::Event, id: CommandId) {
228 let offered = shell
229 .chrome()
230 .peek()
231 .commands
232 .face(id)
233 .is_some_and(|face| !face.availability.disabled());
234 if typed_into(native) || !offered {
235 return;
236 }
237 native.prevent_default();
238 shell.say(Event::Command(id));
239}
240
241fn pasted_on(shell: &Shell, native: &web_sys::Event) {
244 if typed_into(native) {
245 return;
246 }
247 let pasted = native
248 .dyn_ref::<web_sys::ClipboardEvent>()
249 .and_then(web_sys::ClipboardEvent::clipboard_data)
250 .and_then(|data| data.get_data("text/plain").ok())
251 .filter(|text| !text.is_empty());
252 let Some(text) = pasted else {
253 return;
254 };
255 if !shell.takes_a_paste() {
256 native.prevent_default();
260 return;
261 }
262 native.prevent_default();
263 shell.say(Event::Action(Action::Paste(text)));
264}
265
266fn typed_into(native: &web_sys::Event) -> bool {
267 native
268 .target()
269 .and_then(|target| target.dyn_into::<web_sys::Element>().ok())
270 .is_some_and(|target| types_into(&target))
271}