1use 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#[derive(Clone, Copy, Debug, Default, Deserialize, Serialize, Eq, PartialEq, Hash)]
14#[serde(transparent)]
15pub struct WindowId(pub u64);
16
17impl WindowId {
18 pub const PRIMARY: Self = Self(1);
25}
26
27#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
29#[serde(tag = "type", rename_all = "kebab-case")]
30pub enum DesktopEvent {
31 Ready,
33 WindowResized {
35 window_id: WindowId,
36 width: u32,
37 height: u32,
38 },
39 WindowMoved { window_id: WindowId, x: i32, y: i32 },
41 WindowMaximized { window_id: WindowId },
43 WindowUnmaximized { window_id: WindowId },
45 WindowMinimized { window_id: WindowId },
47 WindowRestored { window_id: WindowId },
49 WindowEnteredFullscreen { window_id: WindowId },
51 WindowLeftFullscreen { window_id: WindowId },
53 WindowFocused { window_id: WindowId },
55 WindowBlurred { window_id: WindowId },
57 WindowCloseRequested { window_id: WindowId },
59 WindowClosed { window_id: WindowId },
61 ThemeChanged { dark: bool },
63 ScaleFactorChanged { scale: f64 },
65 NavigationRequested { window_id: WindowId, url: String },
67 NavigationCompleted { window_id: WindowId, url: String },
69 Exiting,
71}
72
73#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
75pub enum EventResponse {
76 #[default]
78 Continue,
79 PreventDefault,
81}
82pub type EventHandler = dyn Fn(&DesktopEvent) -> EventResponse + Send + Sync + 'static;
84
85pub const MAX_EVENT_HANDLERS: usize = 256;
87
88#[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#[derive(Clone, Default)]
118pub struct EventRegistry {
119 inner: Arc<Mutex<EventRegistryState>>,
120}
121
122#[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 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 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 #[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 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#[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 #[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 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 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#[derive(Clone, Debug, PartialEq)]
327pub enum WindowCommand {
328 SetTitle(String),
330 SetBackground(Rgba),
332 SetSize { width: u32, height: u32 },
334 Minimize,
336 Maximize,
338 Unmaximize,
340 SetFullscreen(bool),
342 Center,
344 Focus,
346 Close,
348 StartDrag,
350 SetAlwaysOnTop(bool),
352}
353pub const MAX_QUEUED_WINDOW_COMMANDS: usize = 256;
356pub const MAX_WINDOW_TITLE_BYTES: usize = 16 * 1024;
358pub const MAX_QUEUED_WINDOW_TITLE_BYTES: usize = 64 * 1024;
360
361#[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#[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 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 #[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 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 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 pub fn set_title(&self, title: impl Into<String>) -> Result<(), WindowCommandError> {
520 self.send(WindowCommand::SetTitle(title.into()))
521 }
522 pub fn set_background(&self, color: Rgba) -> Result<(), WindowCommandError> {
524 self.send(WindowCommand::SetBackground(color))
525 }
526 pub fn set_size(&self, width: u32, height: u32) -> Result<(), WindowCommandError> {
528 self.send(WindowCommand::SetSize { width, height })
529 }
530 pub fn minimize(&self) -> Result<(), WindowCommandError> {
532 self.send(WindowCommand::Minimize)
533 }
534 pub fn maximize(&self) -> Result<(), WindowCommandError> {
536 self.send(WindowCommand::Maximize)
537 }
538 pub fn unmaximize(&self) -> Result<(), WindowCommandError> {
540 self.send(WindowCommand::Unmaximize)
541 }
542 pub fn fullscreen(&self, value: bool) -> Result<(), WindowCommandError> {
544 self.send(WindowCommand::SetFullscreen(value))
545 }
546 pub fn center(&self) -> Result<(), WindowCommandError> {
548 self.send(WindowCommand::Center)
549 }
550 pub fn focus(&self) -> Result<(), WindowCommandError> {
552 self.send(WindowCommand::Focus)
553 }
554 pub fn request_close(&self) -> Result<(), WindowCommandError> {
556 self.send(WindowCommand::Close)
557 }
558 pub fn start_drag(&self) -> Result<(), WindowCommandError> {
560 self.send(WindowCommand::StartDrag)
561 }
562 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#[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}
592pub const MAX_HOST_MESSAGE_BYTES: usize = 256;
594#[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 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}
611pub 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;