Skip to repository content718 lines · 24.7 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T04:04:41.017Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
events.rs
1use std::rc::Rc;
2
3use gpui::{
4 Capslock, DispatchEventResult, ExternalPaths, FileDropEvent, KeyDownEvent, KeyUpEvent,
5 Keystroke, Modifiers, ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseExitEvent,
6 MouseMoveEvent, MouseUpEvent, NavigationDirection, Pixels, PlatformInput, Point, ScrollDelta,
7 ScrollWheelEvent, TouchPhase, point, px,
8};
9use smallvec::smallvec;
10use wasm_bindgen::prelude::*;
11
12use crate::window::WebWindowInner;
13
14pub struct WebEventListeners {
15 #[allow(dead_code)]
16 closures: Vec<Closure<dyn FnMut(JsValue)>>,
17}
18
19pub(crate) struct ClickState {
20 last_position: Point<Pixels>,
21 last_time: f64,
22 current_count: usize,
23}
24
25impl Default for ClickState {
26 fn default() -> Self {
27 Self {
28 last_position: Point::default(),
29 last_time: 0.0,
30 current_count: 0,
31 }
32 }
33}
34
35impl ClickState {
36 fn register_click(&mut self, position: Point<Pixels>, time: f64) -> usize {
37 let distance = ((f32::from(position.x) - f32::from(self.last_position.x)).powi(2)
38 + (f32::from(position.y) - f32::from(self.last_position.y)).powi(2))
39 .sqrt();
40
41 if (time - self.last_time) < 400.0 && distance < 5.0 {
42 self.current_count += 1;
43 } else {
44 self.current_count = 1;
45 }
46
47 self.last_position = position;
48 self.last_time = time;
49 self.current_count
50 }
51}
52
53impl WebWindowInner {
54 pub fn register_event_listeners(self: &Rc<Self>) -> WebEventListeners {
55 let mut closures = vec![
56 self.register_pointer_down(),
57 self.register_pointer_up(),
58 self.register_pointer_move(),
59 self.register_pointer_leave(),
60 self.register_wheel(),
61 self.register_context_menu(),
62 self.register_dragover(),
63 self.register_drop(),
64 self.register_dragleave(),
65 self.register_key_down(),
66 self.register_key_up(),
67 self.register_paste(),
68 self.register_composition_start(),
69 self.register_composition_update(),
70 self.register_composition_end(),
71 self.register_focus(),
72 self.register_blur(),
73 self.register_pointer_enter(),
74 self.register_pointer_leave_hover(),
75 ];
76 closures.extend(self.register_visibility_change());
77 closures.extend(self.register_appearance_change());
78
79 WebEventListeners { closures }
80 }
81
82 fn listen(
83 self: &Rc<Self>,
84 event_name: &str,
85 handler: impl FnMut(JsValue) + 'static,
86 ) -> Closure<dyn FnMut(JsValue)> {
87 let closure = Closure::<dyn FnMut(JsValue)>::new(handler);
88 self.canvas
89 .add_event_listener_with_callback(event_name, closure.as_ref().unchecked_ref())
90 .ok();
91 closure
92 }
93
94 fn listen_input(
95 self: &Rc<Self>,
96 event_name: &str,
97 handler: impl FnMut(JsValue) + 'static,
98 ) -> Closure<dyn FnMut(JsValue)> {
99 let closure = Closure::<dyn FnMut(JsValue)>::new(handler);
100 self.input_element
101 .add_event_listener_with_callback(event_name, closure.as_ref().unchecked_ref())
102 .ok();
103 closure
104 }
105
106 /// Registers a listener with `{passive: false}` so that `preventDefault()` works.
107 /// Needed for events like `wheel` which are passive by default in modern browsers.
108 fn listen_non_passive(
109 self: &Rc<Self>,
110 event_name: &str,
111 handler: impl FnMut(JsValue) + 'static,
112 ) -> Closure<dyn FnMut(JsValue)> {
113 let closure = Closure::<dyn FnMut(JsValue)>::new(handler);
114 let canvas_js: &JsValue = self.canvas.as_ref();
115 let callback_js: &JsValue = closure.as_ref();
116 let options = js_sys::Object::new();
117 js_sys::Reflect::set(&options, &"passive".into(), &false.into()).ok();
118 if let Ok(add_fn_val) = js_sys::Reflect::get(canvas_js, &"addEventListener".into()) {
119 if let Ok(add_fn) = add_fn_val.dyn_into::<js_sys::Function>() {
120 add_fn
121 .call3(canvas_js, &event_name.into(), callback_js, &options)
122 .ok();
123 }
124 }
125 closure
126 }
127
128 fn dispatch_input(&self, input: PlatformInput) -> Option<DispatchEventResult> {
129 let mut borrowed = self.callbacks.borrow_mut();
130 borrowed.input.as_mut().map(|callback| callback(input))
131 }
132
133 fn register_pointer_down(self: &Rc<Self>) -> Closure<dyn FnMut(JsValue)> {
134 let this = Rc::clone(self);
135 self.listen("pointerdown", move |event: JsValue| {
136 let event: web_sys::PointerEvent = event.unchecked_into();
137 event.prevent_default();
138 this.input_element.focus().ok();
139
140 let button = dom_mouse_button_to_gpui(event.button());
141 let position = pointer_position_in_element(&event);
142 let modifiers = modifiers_from_mouse_event(&event, this.is_mac);
143 let time = js_sys::Date::now();
144
145 this.pressed_button.set(Some(button));
146 let click_count = this.click_state.borrow_mut().register_click(position, time);
147
148 {
149 let mut current_state = this.state.borrow_mut();
150 current_state.mouse_position = position;
151 current_state.modifiers = modifiers;
152 }
153
154 this.dispatch_input(PlatformInput::MouseDown(MouseDownEvent {
155 button,
156 position,
157 modifiers,
158 click_count,
159 first_mouse: false,
160 }));
161 })
162 }
163
164 fn register_pointer_up(self: &Rc<Self>) -> Closure<dyn FnMut(JsValue)> {
165 let this = Rc::clone(self);
166 self.listen("pointerup", move |event: JsValue| {
167 let event: web_sys::PointerEvent = event.unchecked_into();
168 event.prevent_default();
169
170 let button = dom_mouse_button_to_gpui(event.button());
171 let position = pointer_position_in_element(&event);
172 let modifiers = modifiers_from_mouse_event(&event, this.is_mac);
173
174 this.pressed_button.set(None);
175 let click_count = this.click_state.borrow().current_count;
176
177 {
178 let mut current_state = this.state.borrow_mut();
179 current_state.mouse_position = position;
180 current_state.modifiers = modifiers;
181 }
182
183 this.dispatch_input(PlatformInput::MouseUp(MouseUpEvent {
184 button,
185 position,
186 modifiers,
187 click_count,
188 }));
189 })
190 }
191
192 fn register_pointer_move(self: &Rc<Self>) -> Closure<dyn FnMut(JsValue)> {
193 let this = Rc::clone(self);
194 self.listen("pointermove", move |event: JsValue| {
195 let event: web_sys::PointerEvent = event.unchecked_into();
196 event.prevent_default();
197
198 let position = pointer_position_in_element(&event);
199 let modifiers = modifiers_from_mouse_event(&event, this.is_mac);
200 let current_pressed = this.pressed_button.get();
201
202 {
203 let mut current_state = this.state.borrow_mut();
204 current_state.mouse_position = position;
205 current_state.modifiers = modifiers;
206 }
207
208 this.dispatch_input(PlatformInput::MouseMove(MouseMoveEvent {
209 position,
210 pressed_button: current_pressed,
211 modifiers,
212 }));
213 })
214 }
215
216 fn register_pointer_leave(self: &Rc<Self>) -> Closure<dyn FnMut(JsValue)> {
217 let this = Rc::clone(self);
218 self.listen("pointerleave", move |event: JsValue| {
219 let event: web_sys::PointerEvent = event.unchecked_into();
220
221 let position = pointer_position_in_element(&event);
222 let modifiers = modifiers_from_mouse_event(&event, this.is_mac);
223 let current_pressed = this.pressed_button.get();
224
225 {
226 let mut current_state = this.state.borrow_mut();
227 current_state.mouse_position = position;
228 current_state.modifiers = modifiers;
229 }
230
231 this.dispatch_input(PlatformInput::MouseExited(MouseExitEvent {
232 position,
233 pressed_button: current_pressed,
234 modifiers,
235 }));
236 })
237 }
238
239 fn register_wheel(self: &Rc<Self>) -> Closure<dyn FnMut(JsValue)> {
240 let this = Rc::clone(self);
241 self.listen_non_passive("wheel", move |event: JsValue| {
242 let event: web_sys::WheelEvent = event.unchecked_into();
243 event.prevent_default();
244
245 let mouse_event: &web_sys::MouseEvent = event.as_ref();
246 let position = mouse_position_in_element(mouse_event);
247 let modifiers = modifiers_from_wheel_event(mouse_event, this.is_mac);
248
249 let delta_mode = event.delta_mode();
250 let delta = if delta_mode == 1 {
251 ScrollDelta::Lines(point(-event.delta_x() as f32, -event.delta_y() as f32))
252 } else {
253 ScrollDelta::Pixels(point(
254 px(-event.delta_x() as f32),
255 px(-event.delta_y() as f32),
256 ))
257 };
258
259 {
260 let mut current_state = this.state.borrow_mut();
261 current_state.modifiers = modifiers;
262 }
263
264 this.dispatch_input(PlatformInput::ScrollWheel(ScrollWheelEvent {
265 position,
266 delta,
267 modifiers,
268 touch_phase: TouchPhase::Moved,
269 }));
270 })
271 }
272
273 fn register_context_menu(self: &Rc<Self>) -> Closure<dyn FnMut(JsValue)> {
274 self.listen("contextmenu", move |event: JsValue| {
275 let event: web_sys::Event = event.unchecked_into();
276 event.prevent_default();
277 })
278 }
279
280 fn register_dragover(self: &Rc<Self>) -> Closure<dyn FnMut(JsValue)> {
281 let this = Rc::clone(self);
282 self.listen("dragover", move |event: JsValue| {
283 let event: web_sys::DragEvent = event.unchecked_into();
284 event.prevent_default();
285
286 let mouse_event: &web_sys::MouseEvent = event.as_ref();
287 let position = mouse_position_in_element(mouse_event);
288
289 {
290 let mut current_state = this.state.borrow_mut();
291 current_state.mouse_position = position;
292 }
293
294 this.dispatch_input(PlatformInput::FileDrop(FileDropEvent::Pending { position }));
295 })
296 }
297
298 fn register_drop(self: &Rc<Self>) -> Closure<dyn FnMut(JsValue)> {
299 let this = Rc::clone(self);
300 self.listen("drop", move |event: JsValue| {
301 let event: web_sys::DragEvent = event.unchecked_into();
302 event.prevent_default();
303
304 let mouse_event: &web_sys::MouseEvent = event.as_ref();
305 let position = mouse_position_in_element(mouse_event);
306
307 {
308 let mut current_state = this.state.borrow_mut();
309 current_state.mouse_position = position;
310 }
311
312 let paths = extract_file_paths_from_drag(&event);
313
314 this.dispatch_input(PlatformInput::FileDrop(FileDropEvent::Entered {
315 position,
316 paths: ExternalPaths(paths),
317 }));
318
319 this.dispatch_input(PlatformInput::FileDrop(FileDropEvent::Submit { position }));
320 })
321 }
322
323 fn register_dragleave(self: &Rc<Self>) -> Closure<dyn FnMut(JsValue)> {
324 let this = Rc::clone(self);
325 self.listen("dragleave", move |_event: JsValue| {
326 this.dispatch_input(PlatformInput::FileDrop(FileDropEvent::Exited));
327 })
328 }
329
330 fn register_key_down(self: &Rc<Self>) -> Closure<dyn FnMut(JsValue)> {
331 let this = Rc::clone(self);
332 self.listen_input("keydown", move |event: JsValue| {
333 let event: web_sys::KeyboardEvent = event.unchecked_into();
334
335 let modifiers = modifiers_from_keyboard_event(&event, this.is_mac);
336 let capslock = capslock_from_keyboard_event(&event);
337
338 {
339 let mut current_state = this.state.borrow_mut();
340 current_state.modifiers = modifiers;
341 current_state.capslock = capslock;
342 }
343
344 this.dispatch_input(PlatformInput::ModifiersChanged(ModifiersChangedEvent {
345 modifiers,
346 capslock,
347 }));
348
349 let key = dom_key_to_gpui_key(&event);
350
351 if is_modifier_only_key(&key) {
352 return;
353 }
354
355 let is_held = event.repeat();
356 let key_char = compute_key_char(&event, &key, &modifiers);
357
358 let keystroke = Keystroke {
359 modifiers,
360 key,
361 key_char: key_char.clone(),
362 };
363
364 let result = this.dispatch_input(PlatformInput::KeyDown(KeyDownEvent {
365 keystroke,
366 is_held,
367 prefer_character_input: false,
368 }));
369
370 if let Some(result) = result {
371 if !result.propagate {
372 event.prevent_default();
373 return;
374 }
375 }
376
377 if this.is_composing.get() || event.is_composing() {
378 event.prevent_default();
379 return;
380 }
381
382 if modifiers.is_subset_of(&Modifiers::shift()) {
383 if let Some(text) = key_char {
384 this.with_input_handler(|handler| {
385 handler.replace_text_in_range(None, &text);
386 });
387 // The character went into the input handler; suppress browser
388 // side-effects for the same keystroke (space scrolling the
389 // page, quick-find, etc.). Everything not handled above falls
390 // through so browser shortcuts keep their defaults.
391 event.prevent_default();
392 }
393 }
394 })
395 }
396
397 fn register_key_up(self: &Rc<Self>) -> Closure<dyn FnMut(JsValue)> {
398 let this = Rc::clone(self);
399 self.listen_input("keyup", move |event: JsValue| {
400 let event: web_sys::KeyboardEvent = event.unchecked_into();
401
402 let modifiers = modifiers_from_keyboard_event(&event, this.is_mac);
403 let capslock = capslock_from_keyboard_event(&event);
404
405 {
406 let mut current_state = this.state.borrow_mut();
407 current_state.modifiers = modifiers;
408 current_state.capslock = capslock;
409 }
410
411 this.dispatch_input(PlatformInput::ModifiersChanged(ModifiersChangedEvent {
412 modifiers,
413 capslock,
414 }));
415
416 let key = dom_key_to_gpui_key(&event);
417
418 if is_modifier_only_key(&key) {
419 return;
420 }
421
422 let key_char = compute_key_char(&event, &key, &modifiers);
423
424 let keystroke = Keystroke {
425 modifiers,
426 key,
427 key_char,
428 };
429
430 let result = this.dispatch_input(PlatformInput::KeyUp(KeyUpEvent { keystroke }));
431 if let Some(result) = result {
432 if !result.propagate {
433 event.prevent_default();
434 }
435 }
436 })
437 }
438
439 /// Paste is delivered through the DOM `paste` event rather than
440 /// `Platform::read_from_clipboard`: the browser's asynchronous clipboard
441 /// read API cannot fit that synchronous signature, while `ClipboardEvent`
442 /// exposes `clipboardData` synchronously inside the event. It fires for
443 /// any browser-initiated paste (keyboard, menu bar, context menu).
444 fn register_paste(self: &Rc<Self>) -> Closure<dyn FnMut(JsValue)> {
445 let this = Rc::clone(self);
446 self.listen_input("paste", move |event: JsValue| {
447 let event: web_sys::ClipboardEvent = event.unchecked_into();
448 let Some(clipboard_data) = event.clipboard_data() else {
449 return;
450 };
451 let Ok(text) = clipboard_data.get_data("text/plain") else {
452 return;
453 };
454 if text.is_empty() {
455 return;
456 }
457
458 event.prevent_default();
459 this.with_input_handler(|handler| {
460 handler.replace_text_in_range(None, &text);
461 });
462 })
463 }
464
465 fn register_composition_start(self: &Rc<Self>) -> Closure<dyn FnMut(JsValue)> {
466 let this = Rc::clone(self);
467 self.listen_input("compositionstart", move |_event: JsValue| {
468 this.is_composing.set(true);
469 })
470 }
471
472 fn register_composition_update(self: &Rc<Self>) -> Closure<dyn FnMut(JsValue)> {
473 let this = Rc::clone(self);
474 self.listen_input("compositionupdate", move |event: JsValue| {
475 let event: web_sys::CompositionEvent = event.unchecked_into();
476 let data = event.data().unwrap_or_default();
477 this.is_composing.set(true);
478 this.with_input_handler(|handler| {
479 handler.replace_and_mark_text_in_range(None, &data, None);
480 });
481 })
482 }
483
484 fn register_composition_end(self: &Rc<Self>) -> Closure<dyn FnMut(JsValue)> {
485 let this = Rc::clone(self);
486 self.listen_input("compositionend", move |event: JsValue| {
487 let event: web_sys::CompositionEvent = event.unchecked_into();
488 let data = event.data().unwrap_or_default();
489 this.is_composing.set(false);
490 this.with_input_handler(|handler| {
491 handler.replace_text_in_range(None, &data);
492 handler.unmark_text();
493 });
494 this.input_element.set_value("");
495 })
496 }
497
498 fn register_focus(self: &Rc<Self>) -> Closure<dyn FnMut(JsValue)> {
499 let this = Rc::clone(self);
500 self.listen_input("focus", move |_event: JsValue| {
501 {
502 let mut state = this.state.borrow_mut();
503 state.is_active = true;
504 }
505 let mut callbacks = this.callbacks.borrow_mut();
506 if let Some(ref mut callback) = callbacks.active_status_change {
507 callback(true);
508 }
509 })
510 }
511
512 fn register_blur(self: &Rc<Self>) -> Closure<dyn FnMut(JsValue)> {
513 let this = Rc::clone(self);
514 self.listen_input("blur", move |_event: JsValue| {
515 {
516 let mut state = this.state.borrow_mut();
517 state.is_active = false;
518 }
519 let mut callbacks = this.callbacks.borrow_mut();
520 if let Some(ref mut callback) = callbacks.active_status_change {
521 callback(false);
522 }
523 })
524 }
525
526 fn register_pointer_enter(self: &Rc<Self>) -> Closure<dyn FnMut(JsValue)> {
527 let this = Rc::clone(self);
528 self.listen("pointerenter", move |_event: JsValue| {
529 {
530 let mut state = this.state.borrow_mut();
531 state.is_hovered = true;
532 }
533 let mut callbacks = this.callbacks.borrow_mut();
534 if let Some(ref mut callback) = callbacks.hover_status_change {
535 callback(true);
536 }
537 })
538 }
539
540 fn register_pointer_leave_hover(self: &Rc<Self>) -> Closure<dyn FnMut(JsValue)> {
541 let this = Rc::clone(self);
542 self.listen("pointerleave", move |_event: JsValue| {
543 {
544 let mut state = this.state.borrow_mut();
545 state.is_hovered = false;
546 }
547 let mut callbacks = this.callbacks.borrow_mut();
548 if let Some(ref mut callback) = callbacks.hover_status_change {
549 callback(false);
550 }
551 })
552 }
553}
554
555fn dom_key_to_gpui_key(event: &web_sys::KeyboardEvent) -> String {
556 let key = event.key();
557 match key.as_str() {
558 "Enter" => "enter".to_string(),
559 "Backspace" => "backspace".to_string(),
560 "Tab" => "tab".to_string(),
561 "Escape" => "escape".to_string(),
562 "Delete" => "delete".to_string(),
563 " " => "space".to_string(),
564 "ArrowLeft" => "left".to_string(),
565 "ArrowRight" => "right".to_string(),
566 "ArrowUp" => "up".to_string(),
567 "ArrowDown" => "down".to_string(),
568 "Home" => "home".to_string(),
569 "End" => "end".to_string(),
570 "PageUp" => "pageup".to_string(),
571 "PageDown" => "pagedown".to_string(),
572 "Insert" => "insert".to_string(),
573 "Control" => "control".to_string(),
574 "Alt" => "alt".to_string(),
575 "Shift" => "shift".to_string(),
576 "Meta" => "platform".to_string(),
577 "CapsLock" => "capslock".to_string(),
578 other => {
579 if let Some(rest) = other.strip_prefix('F') {
580 if let Ok(number) = rest.parse::<u8>() {
581 if (1..=35).contains(&number) {
582 return format!("f{number}");
583 }
584 }
585 }
586 other.to_lowercase()
587 }
588 }
589}
590
591fn dom_mouse_button_to_gpui(button: i16) -> MouseButton {
592 match button {
593 0 => MouseButton::Left,
594 1 => MouseButton::Middle,
595 2 => MouseButton::Right,
596 3 => MouseButton::Navigate(NavigationDirection::Back),
597 4 => MouseButton::Navigate(NavigationDirection::Forward),
598 _ => MouseButton::Left,
599 }
600}
601
602fn modifiers_from_keyboard_event(event: &web_sys::KeyboardEvent, _is_mac: bool) -> Modifiers {
603 Modifiers {
604 control: event.ctrl_key(),
605 alt: event.alt_key(),
606 shift: event.shift_key(),
607 platform: event.meta_key(),
608 function: false,
609 }
610}
611
612fn modifiers_from_mouse_event(event: &web_sys::PointerEvent, _is_mac: bool) -> Modifiers {
613 let mouse_event: &web_sys::MouseEvent = event.as_ref();
614 Modifiers {
615 control: mouse_event.ctrl_key(),
616 alt: mouse_event.alt_key(),
617 shift: mouse_event.shift_key(),
618 platform: mouse_event.meta_key(),
619 function: false,
620 }
621}
622
623fn modifiers_from_wheel_event(event: &web_sys::MouseEvent, _is_mac: bool) -> Modifiers {
624 Modifiers {
625 control: event.ctrl_key(),
626 alt: event.alt_key(),
627 shift: event.shift_key(),
628 platform: event.meta_key(),
629 function: false,
630 }
631}
632
633fn capslock_from_keyboard_event(event: &web_sys::KeyboardEvent) -> Capslock {
634 Capslock {
635 on: event.get_modifier_state("CapsLock"),
636 }
637}
638
639pub(crate) fn is_mac_platform(browser_window: &web_sys::Window) -> bool {
640 let navigator = browser_window.navigator();
641
642 #[allow(deprecated)]
643 // navigator.platform() is deprecated but navigator.userAgentData is not widely available yet
644 if let Ok(platform) = navigator.platform() {
645 if platform.contains("Mac") {
646 return true;
647 }
648 }
649
650 if let Ok(user_agent) = navigator.user_agent() {
651 return user_agent.contains("Mac");
652 }
653
654 false
655}
656
657fn is_modifier_only_key(key: &str) -> bool {
658 matches!(
659 key,
660 "control" | "alt" | "shift" | "platform" | "capslock" | "compose" | "process"
661 )
662}
663
664fn compute_key_char(
665 event: &web_sys::KeyboardEvent,
666 gpui_key: &str,
667 modifiers: &Modifiers,
668) -> Option<String> {
669 if modifiers.platform || modifiers.control {
670 return None;
671 }
672
673 if is_modifier_only_key(gpui_key) {
674 return None;
675 }
676
677 if gpui_key == "space" {
678 return Some(" ".to_string());
679 }
680
681 let raw_key = event.key();
682
683 if raw_key.len() == 1 {
684 return Some(raw_key);
685 }
686
687 None
688}
689
690fn pointer_position_in_element(event: &web_sys::PointerEvent) -> Point<Pixels> {
691 let mouse_event: &web_sys::MouseEvent = event.as_ref();
692 mouse_position_in_element(mouse_event)
693}
694
695fn mouse_position_in_element(event: &web_sys::MouseEvent) -> Point<Pixels> {
696 // offset_x/offset_y give position relative to the target element's padding edge
697 point(px(event.offset_x() as f32), px(event.offset_y() as f32))
698}
699
700fn extract_file_paths_from_drag(
701 event: &web_sys::DragEvent,
702) -> smallvec::SmallVec<[std::path::PathBuf; 2]> {
703 let mut paths = smallvec![];
704 let Some(data_transfer) = event.data_transfer() else {
705 return paths;
706 };
707 let file_list = data_transfer.files();
708 let Some(files) = file_list else {
709 return paths;
710 };
711 for index in 0..files.length() {
712 if let Some(file) = files.get(index) {
713 paths.push(std::path::PathBuf::from(file.name()));
714 }
715 }
716 paths
717}
718