Skip to main content

webui_desktop/
event.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT license.
3
4use serde::{Deserialize, Serialize};
5use std::collections::VecDeque;
6use std::sync::{Arc, Mutex};
7use thiserror::Error;
8
9use crate::window::LiveBackground;
10use crate::Rgba;
11
12/// Stable identity for a desktop window.
13#[derive(Clone, Copy, Debug, Default, Deserialize, Serialize, Eq, PartialEq, Hash)]
14#[serde(transparent)]
15pub struct WindowId(pub u64);
16
17impl WindowId {
18    /// Identity of the single window every backend opens at startup.
19    ///
20    /// Multi-window support is not implemented yet, so all three backends must
21    /// report this exact value. Handlers can therefore compare against it
22    /// portably, and a future multi-window backend can allocate subsequent ids
23    /// without renumbering the primary window.
24    pub const PRIMARY: Self = Self(1);
25}
26
27/// A native lifecycle event delivered on the backend UI thread.
28#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
29#[serde(tag = "type", rename_all = "kebab-case")]
30pub enum DesktopEvent {
31    /// The native application is ready.
32    Ready,
33    /// A window changed size.
34    WindowResized {
35        window_id: WindowId,
36        width: u32,
37        height: u32,
38    },
39    /// A window moved.
40    WindowMoved { window_id: WindowId, x: i32, y: i32 },
41    /// A window state changed.
42    WindowMaximized { window_id: WindowId },
43    /// A window left the maximized state.
44    WindowUnmaximized { window_id: WindowId },
45    /// A window was minimized to the dock or taskbar.
46    WindowMinimized { window_id: WindowId },
47    /// A window was restored from the minimized state.
48    WindowRestored { window_id: WindowId },
49    /// A window entered native fullscreen.
50    WindowEnteredFullscreen { window_id: WindowId },
51    /// A window left native fullscreen.
52    WindowLeftFullscreen { window_id: WindowId },
53    /// A window became the key or foreground window.
54    WindowFocused { window_id: WindowId },
55    /// A window stopped being the key or foreground window.
56    WindowBlurred { window_id: WindowId },
57    /// A close request that handlers may prevent.
58    WindowCloseRequested { window_id: WindowId },
59    /// A window closed.
60    WindowClosed { window_id: WindowId },
61    /// The native theme changed.
62    ThemeChanged { dark: bool },
63    /// The display scale factor changed.
64    ScaleFactorChanged { scale: f64 },
65    /// A navigation request that handlers may prevent.
66    NavigationRequested { window_id: WindowId, url: String },
67    /// Navigation completed.
68    NavigationCompleted { window_id: WindowId, url: String },
69    /// The native application is exiting.
70    Exiting,
71}
72
73/// Response returned by a lifecycle handler.
74#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
75pub enum EventResponse {
76    /// Allow the default native behavior to proceed.
77    #[default]
78    Continue,
79    /// Cancel the default native behavior, for cancellable events only.
80    PreventDefault,
81}
82/// UI-thread lifecycle callback. Handlers must not block the UI thread.
83pub type EventHandler = dyn Fn(&DesktopEvent) -> EventResponse + Send + Sync + 'static;
84
85/// Maximum number of persistent and subscribed lifecycle callbacks.
86pub const MAX_EVENT_HANDLERS: usize = 256;
87
88/// Lifecycle callback registration error.
89#[derive(Debug, Error, Eq, PartialEq)]
90pub enum EventRegistrationError {
91    #[error("desktop event handler registry is full (max {MAX_EVENT_HANDLERS}); help: drop unused subscriptions before registering more handlers")]
92    Capacity,
93    #[error("desktop event handler registry is closed; help: register handlers before the desktop frame shuts down")]
94    Closed,
95    #[error("desktop event handler registry is unavailable because a callback panicked; help: restart the application")]
96    Unavailable,
97}
98
99struct EventRegistration {
100    id: u64,
101    handler: Arc<EventHandler>,
102}
103
104#[derive(Default)]
105struct EventRegistryState {
106    handlers: Option<Arc<[EventRegistration]>>,
107    next_id: u64,
108    closed: bool,
109}
110
111/// Thread-safe registration and dispatch container for lifecycle callbacks.
112///
113/// Dispatch clones one `Arc` snapshot without allocating and releases the lock
114/// before invoking callbacks. A dispatch already in progress may finish with
115/// its original snapshot; registration and removal affect future dispatches.
116/// Nested dispatch observes the current snapshot at the time it starts.
117#[derive(Clone, Default)]
118pub struct EventRegistry {
119    inner: Arc<Mutex<EventRegistryState>>,
120}
121
122/// Removes a subscribed lifecycle callback when dropped.
123///
124/// The subscription holds a weak registry reference, so retaining it in state
125/// owned by its callback does not create an ownership cycle.
126#[must_use = "dropping the subscription immediately removes the event handler"]
127pub struct EventSubscription {
128    owner: std::sync::Weak<Mutex<EventRegistryState>>,
129    id: u64,
130}
131
132impl Drop for EventSubscription {
133    fn drop(&mut self) {
134        let Some(owner) = self.owner.upgrade() else {
135            return;
136        };
137        remove_event_handler(&owner, self.id);
138    }
139}
140
141impl EventRegistry {
142    /// Register a callback that remains installed until the registry closes.
143    ///
144    /// # Errors
145    ///
146    /// Returns [`EventRegistrationError`] when the registry reached its handler
147    /// cap, has closed, or its state is unavailable after a callback panic.
148    pub fn on_event<F>(&self, handler: F) -> Result<(), EventRegistrationError>
149    where
150        F: Fn(&DesktopEvent) -> EventResponse + Send + Sync + 'static,
151    {
152        self.register(Arc::new(handler)).map(|_| ())
153    }
154
155    /// Register a callback removed automatically when its subscription drops.
156    ///
157    /// # Errors
158    ///
159    /// Returns [`EventRegistrationError`] when the registry reached its handler
160    /// cap, has closed, or its state is unavailable after a callback panic.
161    pub fn subscribe<F>(&self, handler: F) -> Result<EventSubscription, EventRegistrationError>
162    where
163        F: Fn(&DesktopEvent) -> EventResponse + Send + Sync + 'static,
164    {
165        let id = self.register(Arc::new(handler))?;
166        Ok(EventSubscription {
167            owner: Arc::downgrade(&self.inner),
168            id,
169        })
170    }
171
172    fn register(&self, handler: Arc<EventHandler>) -> Result<u64, EventRegistrationError> {
173        let mut state = self
174            .inner
175            .lock()
176            .map_err(|_| EventRegistrationError::Unavailable)?;
177        if state.closed {
178            return Err(EventRegistrationError::Closed);
179        }
180        let len = state.handlers.as_ref().map_or(0, |handlers| handlers.len());
181        if len >= MAX_EVENT_HANDLERS {
182            return Err(EventRegistrationError::Capacity);
183        }
184        let id = state.next_id;
185        state.next_id = state.next_id.wrapping_add(1);
186        let mut next = Vec::with_capacity(len + 1);
187        if let Some(handlers) = state.handlers.as_ref() {
188            next.extend(handlers.iter().map(|entry| EventRegistration {
189                id: entry.id,
190                handler: Arc::clone(&entry.handler),
191            }));
192        }
193        next.push(EventRegistration { id, handler });
194        let previous = state.handlers.replace(next.into());
195        drop(state);
196        drop(previous);
197        Ok(id)
198    }
199
200    /// Dispatch an event and report cancellation.
201    ///
202    /// Every handler in the dispatch snapshot observes the event even after one
203    /// cancels it, so the outcome does not depend on registration order.
204    #[must_use]
205    pub fn dispatch(&self, event: &DesktopEvent) -> EventResponse {
206        let handlers = {
207            let Ok(state) = self.inner.lock() else {
208                return EventResponse::Continue;
209            };
210            if state.closed {
211                return EventResponse::Continue;
212            }
213            state.handlers.as_ref().map(Arc::clone)
214        };
215        let Some(handlers) = handlers else {
216            return EventResponse::Continue;
217        };
218        let mut response = EventResponse::Continue;
219        for registration in handlers.iter() {
220            if (registration.handler)(event) == EventResponse::PreventDefault {
221                response = EventResponse::PreventDefault;
222            }
223        }
224        response
225    }
226
227    /// Close the registry and release all callbacks.
228    ///
229    /// Dispatch after closure is inert. A dispatch that already cloned its
230    /// snapshot may finish before those callbacks are released.
231    pub(crate) fn close(&self) {
232        let handlers = {
233            let Ok(mut state) = self.inner.lock() else {
234                return;
235            };
236            state.closed = true;
237            state.handlers.take()
238        };
239        drop(handlers);
240    }
241}
242
243fn remove_event_handler(owner: &Mutex<EventRegistryState>, id: u64) {
244    let previous = {
245        let Ok(mut state) = owner.lock() else {
246            return;
247        };
248        let Some(handlers) = state.handlers.as_ref() else {
249            return;
250        };
251        let Some(index) = handlers.iter().position(|entry| entry.id == id) else {
252            return;
253        };
254        if handlers.len() == 1 {
255            state.handlers.take()
256        } else {
257            let mut next = Vec::with_capacity(handlers.len() - 1);
258            next.extend(
259                handlers
260                    .iter()
261                    .enumerate()
262                    .filter(|(entry_index, _)| *entry_index != index)
263                    .map(|(_, entry)| EventRegistration {
264                        id: entry.id,
265                        handler: Arc::clone(&entry.handler),
266                    }),
267            );
268            state.handlers.replace(next.into())
269        }
270    };
271    drop(previous);
272}
273/// Event JavaScript serialization error.
274#[derive(Debug, Error)]
275pub enum EventJavascriptError {
276    #[error("failed to serialize desktop event for JavaScript: {0}")]
277    Serialization(serde_json::Error),
278}
279impl DesktopEvent {
280    /// Return the `webui:*` custom event name.
281    #[must_use]
282    pub const fn javascript_name(&self) -> &'static str {
283        match self {
284            Self::Ready => "webui:ready",
285            Self::WindowResized { .. } => "webui:window-resized",
286            Self::WindowMoved { .. } => "webui:window-moved",
287            Self::WindowMaximized { .. } => "webui:window-maximized",
288            Self::WindowUnmaximized { .. } => "webui:window-unmaximized",
289            Self::WindowMinimized { .. } => "webui:window-minimized",
290            Self::WindowRestored { .. } => "webui:window-restored",
291            Self::WindowEnteredFullscreen { .. } => "webui:window-entered-fullscreen",
292            Self::WindowLeftFullscreen { .. } => "webui:window-left-fullscreen",
293            Self::WindowFocused { .. } => "webui:window-focused",
294            Self::WindowBlurred { .. } => "webui:window-blurred",
295            Self::WindowCloseRequested { .. } => "webui:window-close-requested",
296            Self::WindowClosed { .. } => "webui:window-closed",
297            Self::ThemeChanged { .. } => "webui:theme-changed",
298            Self::ScaleFactorChanged { .. } => "webui:scale-factor-changed",
299            Self::NavigationRequested { .. } => "webui:navigation-requested",
300            Self::NavigationCompleted { .. } => "webui:navigation-completed",
301            Self::Exiting => "webui:exiting",
302        }
303    }
304    /// Serialize exact JavaScript evaluated by every backend.
305    pub fn to_javascript(&self) -> Result<String, EventJavascriptError> {
306        let detail = serde_json::to_string(self).map_err(EventJavascriptError::Serialization)?;
307        let name = self.javascript_name();
308        // `javascript_name` returns a closed set of ASCII `webui:*` literals that
309        // need no escaping, so the quotes are emitted directly rather than paying
310        // for a formatter or a second serde pass.
311        debug_assert!(
312            name.bytes()
313                .all(|byte| byte.is_ascii_graphic() && byte != b'"' && byte != b'\\'),
314            "event name must be escape-free ASCII"
315        );
316        let mut script = String::with_capacity(detail.len() + name.len() + 54);
317        script.push_str("window.dispatchEvent(new CustomEvent(\"");
318        script.push_str(name);
319        script.push_str("\",{detail:");
320        script.push_str(&detail);
321        script.push_str("}));");
322        Ok(script)
323    }
324}
325/// A command queued for execution on the native UI thread.
326#[derive(Clone, Debug, PartialEq)]
327pub enum WindowCommand {
328    /// Replace the native window title.
329    SetTitle(String),
330    /// Replace the native pre-paint and web document background.
331    SetBackground(Rgba),
332    /// Resize the window's content area, in logical pixels.
333    SetSize { width: u32, height: u32 },
334    /// Minimize the window to the dock or taskbar.
335    Minimize,
336    /// Maximize the window to fill the work area.
337    Maximize,
338    /// Restore the window from the maximized state.
339    Unmaximize,
340    /// Enter or leave native fullscreen.
341    SetFullscreen(bool),
342    /// Center the window on its current display.
343    Center,
344    /// Raise the window and give it keyboard focus.
345    Focus,
346    /// Close the window, firing `WindowCloseRequested` first.
347    Close,
348    /// Begin a host-driven window drag, used by `webui-drag` regions.
349    StartDrag,
350    /// Pin the window above or below other windows.
351    SetAlwaysOnTop(bool),
352}
353/// Maximum number of commands buffered for the native UI thread before
354/// [`WindowHandle::send`] reports back-pressure instead of growing without bound.
355pub const MAX_QUEUED_WINDOW_COMMANDS: usize = 256;
356/// Maximum UTF-8 bytes accepted in one queued window title.
357pub const MAX_WINDOW_TITLE_BYTES: usize = 16 * 1024;
358/// Maximum aggregate UTF-8 title bytes buffered for the native UI thread.
359pub const MAX_QUEUED_WINDOW_TITLE_BYTES: usize = 64 * 1024;
360
361/// Command queue error.
362#[derive(Debug, Error, Eq, PartialEq)]
363pub enum WindowCommandError {
364    #[error(
365        "desktop window command queue is full; help: wait for the UI thread to drain commands"
366    )]
367    QueueFull,
368    #[error("desktop window title is too large: {size} bytes (max {MAX_WINDOW_TITLE_BYTES}); help: shorten the title before sending it")]
369    TitleTooLarge { size: usize },
370    #[error("desktop window title queue is full: {queued} of {MAX_QUEUED_WINDOW_TITLE_BYTES} bytes queued, {requested} more requested; help: wait for the UI thread to drain title updates")]
371    TitleQueueFull { queued: usize, requested: usize },
372    #[error("desktop window command queue is closed; help: stop sending commands after the desktop frame shuts down")]
373    Closed,
374    #[error(
375        "desktop window command queue is unavailable because the native UI thread panicked; help: restart the application"
376    )]
377    Unavailable,
378}
379
380/// Sendable handle that queues commands and wakes the native UI loop.
381#[derive(Clone, Default)]
382pub struct WindowHandle {
383    inner: Arc<Mutex<WindowHandleState>>,
384}
385
386#[derive(Default)]
387struct WindowHandleState {
388    queue: Option<VecDeque<WindowCommand>>,
389    background: Option<Arc<LiveBackground>>,
390    wakeup: Option<Arc<dyn Fn() + Send + Sync>>,
391    queued_title_bytes: usize,
392    wake_scheduled: bool,
393    closed: bool,
394}
395
396impl WindowHandle {
397    pub(crate) fn with_background(background: Arc<LiveBackground>) -> Self {
398        Self {
399            inner: Arc::new(Mutex::new(WindowHandleState {
400                background: Some(background),
401                ..WindowHandleState::default()
402            })),
403        }
404    }
405
406    /// Install the backend wakeup callback.
407    ///
408    /// Installing a callback while commands are queued schedules one wakeup.
409    /// The callback is always invoked after releasing the queue lock.
410    pub fn set_wakeup<F>(&self, wakeup: F)
411    where
412        F: Fn() + Send + Sync + 'static,
413    {
414        let wakeup: Arc<dyn Fn() + Send + Sync> = Arc::new(wakeup);
415        let (previous, notify) = {
416            let Ok(mut state) = self.inner.lock() else {
417                return;
418            };
419            if state.closed {
420                return;
421            }
422            let previous = state.wakeup.replace(Arc::clone(&wakeup));
423            let has_backlog = state.queue.as_ref().is_some_and(|queue| !queue.is_empty());
424            let notify = has_backlog.then(|| {
425                state.wake_scheduled = true;
426                wakeup
427            });
428            (previous, notify)
429        };
430        drop(previous);
431        if let Some(notify) = notify {
432            notify();
433        }
434    }
435
436    /// Drain queued commands on the UI thread.
437    #[must_use]
438    pub fn drain_commands(&self) -> Vec<WindowCommand> {
439        let queue = {
440            let Ok(mut state) = self.inner.lock() else {
441                return Vec::new();
442            };
443            state.wake_scheduled = false;
444            state.queued_title_bytes = 0;
445            state.queue.take()
446        };
447        queue.map_or_else(Vec::new, VecDeque::into)
448    }
449
450    /// Queue a native command.
451    ///
452    /// Success means the command was accepted into the bounded queue, not that
453    /// the native UI thread has already applied it.
454    pub fn send(&self, command: WindowCommand) -> Result<(), WindowCommandError> {
455        let command = normalize_command(command);
456        let notify = {
457            let mut state = self
458                .inner
459                .lock()
460                .map_err(|_| WindowCommandError::Unavailable)?;
461            if state.closed {
462                return Err(WindowCommandError::Closed);
463            }
464            let (command, title_bytes) = command?;
465            let queue_len = state.queue.as_ref().map_or(0, VecDeque::len);
466            if queue_len >= MAX_QUEUED_WINDOW_COMMANDS {
467                return Err(WindowCommandError::QueueFull);
468            }
469            if title_bytes > MAX_QUEUED_WINDOW_TITLE_BYTES - state.queued_title_bytes {
470                return Err(WindowCommandError::TitleQueueFull {
471                    queued: state.queued_title_bytes,
472                    requested: title_bytes,
473                });
474            }
475            state.queued_title_bytes += title_bytes;
476            if let WindowCommand::SetBackground(color) = command {
477                if let Some(background) = &state.background {
478                    background.set(color);
479                }
480            }
481            state
482                .queue
483                .get_or_insert_with(VecDeque::new)
484                .push_back(command);
485            if state.wake_scheduled {
486                None
487            } else if let Some(wakeup) = state.wakeup.as_ref().map(Arc::clone) {
488                state.wake_scheduled = true;
489                Some(wakeup)
490            } else {
491                None
492            }
493        };
494        if let Some(notify) = notify {
495            notify();
496        }
497        Ok(())
498    }
499
500    /// Close the command session and discard pending commands.
501    ///
502    /// The frame owner calls this during teardown. Wakeup callbacks and queued
503    /// command payloads are released only after the state lock is released.
504    pub(crate) fn close(&self) {
505        let (wakeup, queue) = {
506            let Ok(mut state) = self.inner.lock() else {
507                return;
508            };
509            state.closed = true;
510            state.wake_scheduled = false;
511            state.queued_title_bytes = 0;
512            (state.wakeup.take(), state.queue.take())
513        };
514        drop(wakeup);
515        drop(queue);
516    }
517
518    /// Queue title update.
519    pub fn set_title(&self, title: impl Into<String>) -> Result<(), WindowCommandError> {
520        self.send(WindowCommand::SetTitle(title.into()))
521    }
522    /// Queue a native and document background update without changing the bundle manifest.
523    pub fn set_background(&self, color: Rgba) -> Result<(), WindowCommandError> {
524        self.send(WindowCommand::SetBackground(color))
525    }
526    /// Queue size update.
527    pub fn set_size(&self, width: u32, height: u32) -> Result<(), WindowCommandError> {
528        self.send(WindowCommand::SetSize { width, height })
529    }
530    /// Queue minimization.
531    pub fn minimize(&self) -> Result<(), WindowCommandError> {
532        self.send(WindowCommand::Minimize)
533    }
534    /// Queue maximization.
535    pub fn maximize(&self) -> Result<(), WindowCommandError> {
536        self.send(WindowCommand::Maximize)
537    }
538    /// Queue unmaximization.
539    pub fn unmaximize(&self) -> Result<(), WindowCommandError> {
540        self.send(WindowCommand::Unmaximize)
541    }
542    /// Queue fullscreen change.
543    pub fn fullscreen(&self, value: bool) -> Result<(), WindowCommandError> {
544        self.send(WindowCommand::SetFullscreen(value))
545    }
546    /// Queue centering.
547    pub fn center(&self) -> Result<(), WindowCommandError> {
548        self.send(WindowCommand::Center)
549    }
550    /// Queue focus.
551    pub fn focus(&self) -> Result<(), WindowCommandError> {
552        self.send(WindowCommand::Focus)
553    }
554    /// Queue a native close request.
555    pub fn request_close(&self) -> Result<(), WindowCommandError> {
556        self.send(WindowCommand::Close)
557    }
558    /// Queue native drag.
559    pub fn start_drag(&self) -> Result<(), WindowCommandError> {
560        self.send(WindowCommand::StartDrag)
561    }
562    /// Queue always-on-top change.
563    pub fn set_always_on_top(&self, value: bool) -> Result<(), WindowCommandError> {
564        self.send(WindowCommand::SetAlwaysOnTop(value))
565    }
566}
567
568fn normalize_command(command: WindowCommand) -> Result<(WindowCommand, usize), WindowCommandError> {
569    let WindowCommand::SetTitle(title) = command else {
570        return Ok((command, 0));
571    };
572    let size = title.len();
573    if size > MAX_WINDOW_TITLE_BYTES {
574        return Err(WindowCommandError::TitleTooLarge { size });
575    }
576    let title = if title.capacity() == size {
577        title
578    } else {
579        title.into_boxed_str().into_string()
580    };
581    Ok((WindowCommand::SetTitle(title), size))
582}
583/// Strictly bounded host message sent from injected web content.
584#[derive(Clone, Copy, Debug, Deserialize, Serialize, Eq, PartialEq)]
585#[serde(rename_all = "kebab-case")]
586pub enum DesktopHostMessage {
587    StartDrag,
588    Minimize,
589    ToggleMaximize,
590    Close,
591}
592/// Maximum untrusted script-message payload size.
593pub const MAX_HOST_MESSAGE_BYTES: usize = 256;
594/// Host-message parsing error.
595#[derive(Debug, Error)]
596pub enum DesktopHostMessageError {
597    #[error("desktop host message is too large: {size} bytes (max {MAX_HOST_MESSAGE_BYTES}); help: send only a supported command")]
598    TooLarge { size: usize },
599    #[error("invalid desktop host message; help: use start-drag, minimize, toggle-maximize, or close: {0}")]
600    Invalid(serde_json::Error),
601}
602impl DesktopHostMessage {
603    /// Parse untrusted JSON.
604    pub fn from_json(input: &str) -> Result<Self, DesktopHostMessageError> {
605        if input.len() > MAX_HOST_MESSAGE_BYTES {
606            return Err(DesktopHostMessageError::TooLarge { size: input.len() });
607        }
608        serde_json::from_str(input).map_err(DesktopHostMessageError::Invalid)
609    }
610}
611/// Shared script for `[webui-drag]` and `[webui-no-drag]` regions. Backends expose `window.webuiHostPostMessage`.
612/// Script injected by every backend to translate `webui-drag` regions into host
613/// messages.
614///
615/// The walk uses `composedPath()` rather than `target.closest()` because WebUI
616/// renders through Web Components: a `document`-level listener sees shadow DOM
617/// events retargeted to the host element, so `closest()` would never observe a
618/// drag region declared inside a component's shadow root. The path is scanned
619/// from the innermost node outward, so the nearest `webui-drag` or
620/// `webui-no-drag` ancestor wins, and non-element entries such as `document`
621/// and `window` are skipped. WebView2 waits for pointer movement before
622/// starting a native drag, since its move loop would consume a double-click.
623pub const DRAG_REGION_SCRIPT: &str = concat!(
624    "(()=>{const p=m=>window.webuiHostPostMessage&&window.webuiHostPostMessage(JSON.stringify(m));",
625    "const d=e=>{const q=typeof e.composedPath==='function'?e.composedPath():[];",
626    "for(let i=0;i<q.length;i++){const n=q[i];if(!n||n.nodeType!==1)continue;",
627    "if(n.hasAttribute('webui-no-drag'))return false;if(n.hasAttribute('webui-drag'))return true}return false};",
628    "let a=null,w=!!window.chrome?.webview;document.addEventListener('pointerdown',e=>{a=null;",
629    "if(e.button!==0||!d(e))return;if(!w){p('start-drag');return}",
630    "a=[e.pointerId,e.screenX,e.screenY]});",
631    "if(w){document.addEventListener('pointermove',e=>{",
632    "if(!a||e.pointerId!==a[0])return;if(!(e.buttons&1)){a=null;return}",
633    "if(Math.abs(e.screenX-a[1])<4&&Math.abs(e.screenY-a[2])<4)return;",
634    "a=null;p('start-drag')});",
635    "const end=e=>{if(a&&e.pointerId===a[0])a=null};",
636    "document.addEventListener('pointerup',end);document.addEventListener('pointercancel',end);",
637    "window.addEventListener('blur',()=>{a=null})}",
638    "document.addEventListener('dblclick',e=>{if(e.button===0&&d(e))p('toggle-maximize')})})();"
639);
640#[cfg(test)]
641#[path = "event_tests.rs"]
642mod tests;