Skip to main content

blitz_shell/
window.rs

1use crate::BlitzShellProvider;
2use crate::convert_events::{
3    button_source_to_blitz, color_scheme_to_theme, pointer_kind_to_blitz, pointer_source_to_blitz,
4    pointer_source_to_blitz_details, theme_to_color_scheme, winit_ime_to_blitz,
5    winit_key_event_to_blitz, winit_modifiers_to_kbt_modifiers,
6};
7use crate::event::{BlitzShellEvent, BlitzShellProxy, create_waker};
8use anyrender::WindowRenderer;
9use blitz_dom::Document;
10use blitz_paint::paint_scene;
11use blitz_traits::events::{
12    BlitzPointerEvent, BlitzPointerId, BlitzWheelDelta, BlitzWheelEvent, MouseEventButton,
13    MouseEventButtons, PointerCoords, PointerDetails, UiEvent,
14};
15use blitz_traits::shell::Viewport;
16use winit::dpi::{LogicalPosition, PhysicalInsets, PhysicalPosition};
17use winit::keyboard::PhysicalKey;
18
19use atomic_refcell::AtomicRefCell;
20use std::any::Any;
21use std::sync::Arc;
22use std::task::Waker;
23use web_time::Instant;
24use winit::event::{ButtonSource, ElementState, MouseButton};
25use winit::event_loop::ActiveEventLoop;
26use winit::window::{Theme, WindowAttributes, WindowId};
27use winit::{event::Modifiers, event::WindowEvent, keyboard::KeyCode, window::Window};
28
29#[cfg(feature = "accessibility")]
30use crate::accessibility::AccessibilityState;
31
32// Ignore safe_area_insets on macOS because we don't want to avoid
33// drawing in the titlebar.
34#[cfg(target_os = "macos")]
35fn get_safe_area_insets(_window: &dyn Window) -> PhysicalInsets<u32> {
36    Default::default()
37}
38#[cfg(not(target_os = "macos"))]
39fn get_safe_area_insets(window: &dyn Window) -> PhysicalInsets<u32> {
40    window.safe_area()
41}
42
43pub struct WindowConfig<Rend: WindowRenderer> {
44    doc: Box<dyn Document>,
45    pub(crate) attributes: WindowAttributes,
46    renderer: Rend,
47}
48
49impl<Rend: WindowRenderer> WindowConfig<Rend> {
50    pub fn new(doc: Box<dyn Document>, renderer: Rend) -> Self {
51        Self::with_attributes(doc, renderer, WindowAttributes::default())
52    }
53
54    pub fn with_attributes(
55        doc: Box<dyn Document>,
56        renderer: Rend,
57        attributes: WindowAttributes,
58    ) -> Self {
59        WindowConfig {
60            doc,
61            attributes,
62            renderer,
63        }
64    }
65}
66
67pub struct View<Rend: WindowRenderer> {
68    pub doc: Box<dyn Document>,
69
70    pub renderer: Rend,
71    pub waker: Option<Waker>,
72
73    pub proxy: BlitzShellProxy,
74    pub window: Arc<dyn Window>,
75
76    /// The state of the keyboard modifiers (ctrl, shift, etc). Winit/Tao don't track these for us so we
77    /// need to store them in order to have access to them when processing keypress events
78    pub theme_override: Option<Theme>,
79    pub keyboard_modifiers: Modifiers,
80    pub buttons: MouseEventButtons,
81    pub pointer_pos: PhysicalPosition<f64>,
82    /// The non-mouse pointers (touch/pen) that are currently pressed, in the
83    /// order they were pressed.
84    ///
85    /// This serves two purposes:
86    /// - Multi-touch: it is cloned (cheaply, via [`Arc`]) into every dispatched
87    ///   [`BlitzPointerEvent`] so that touch events can report all concurrent
88    ///   touches via their `touches` list.
89    /// - Cancellation detection: winit signals a cancelled touch with a
90    ///   [`WindowEvent::PointerLeft`] that is *not* preceded by a
91    ///   [`WindowEvent::PointerButton`] with [`ElementState::Released`]. If a
92    ///   pointer is still in this list when it leaves, it was cancelled.
93    ///
94    /// The events stored here always have an empty `active_pointers` list to
95    /// avoid a reference cycle.
96    pub active_events: Arc<AtomicRefCell<Vec<BlitzPointerEvent>>>,
97    pub animation_timer: Option<Instant>,
98    pub is_visible: bool,
99    pub safe_area_insets: PhysicalInsets<u32>,
100
101    #[cfg(target_arch = "wasm32")]
102    pending_resize: Option<winit::dpi::PhysicalSize<u32>>,
103    #[cfg(target_arch = "wasm32")]
104    last_resize_at: Option<web_time::Instant>,
105    /// True iff a setTimeout has been scheduled and not yet observed by
106    /// `apply_pending_resize_if_settled`. Prevents the timer storm that would
107    /// otherwise allocate a fresh `Closure` per resize event during a drag.
108    #[cfg(target_arch = "wasm32")]
109    resize_timer_scheduled: bool,
110
111    #[cfg(feature = "accessibility")]
112    /// Accessibility adapter for `accesskit`.
113    pub accessibility: AccessibilityState,
114
115    // Calling request_redraw within a WindowEvent doesn't work on iOS. So on iOS we track the state
116    // with a boolean and call request_redraw in about_to_wait
117    //
118    // See https://github.com/rust-windowing/winit/issues/3406
119    #[cfg(target_os = "ios")]
120    pub ios_request_redraw: std::cell::Cell<bool>,
121}
122
123impl<Rend: WindowRenderer> Drop for View<Rend> {
124    fn drop(&mut self) {
125        // Release the renderer's window surface before the window is dropped.
126        // The renderer may be shared (e.g. provided as a context to user code),
127        // in which case it can outlive the `View`. A GPU surface must not
128        // outlive the window/display it is attached to: dropping it after the
129        // event loop has shut down segfaults on Wayland.
130        self.renderer.suspend();
131    }
132}
133
134impl<Rend: WindowRenderer> View<Rend> {
135    pub fn init(
136        config: WindowConfig<Rend>,
137        event_loop: &dyn ActiveEventLoop,
138        proxy: &BlitzShellProxy,
139    ) -> Self {
140        // We create window as invisble and then later make window visible
141        // after AccessKit has initialised to avoid AccessKit panics
142        let is_visible = config.attributes.visible;
143        // Capture the requested surface size before consuming `attributes`, so we can
144        // seed the viewport on platforms (winit-web) that report `surface_size() == 0×0`
145        // until a layout pass fires.
146        let requested_surface_size = config.attributes.surface_size;
147        let attrs = config.attributes.with_visible(false);
148
149        let winit_window: Arc<dyn Window> = Arc::from(event_loop.create_window(attrs).unwrap());
150        #[cfg(feature = "accessibility")]
151        let accessibility = AccessibilityState::new(&*winit_window, proxy.clone());
152
153        if is_visible {
154            winit_window.set_visible(true);
155        }
156
157        // Create viewport
158        let scale = winit_window.scale_factor() as f32;
159        let mut size = winit_window.surface_size();
160        if (size.width == 0 || size.height == 0)
161            && let Some(requested) = requested_surface_size
162        {
163            size = requested.to_physical(scale as f64);
164        }
165        // On wasm, when the embedder didn't call `with_surface_size`, winit-web's
166        // initial `surface_size()` is 0×0 — its ResizeObserver hasn't fired yet.
167        // Resuming the renderer at 0×0 trips a wgpu swapchain-size-0 error, so
168        // seed from the canvas element's CSS layout box (host-stylesheet result).
169        #[cfg(target_arch = "wasm32")]
170        if size.width == 0 || size.height == 0 {
171            use winit::platform::web::WindowExtWeb;
172            if let Some(canvas) = winit_window.canvas() {
173                let css_w = canvas.offset_width().max(0) as u32;
174                let css_h = canvas.offset_height().max(0) as u32;
175                if css_w > 0 && css_h > 0 {
176                    size = winit::dpi::LogicalSize::new(css_w, css_h).to_physical(scale as f64);
177                }
178            }
179        }
180        let safe_area_insets = get_safe_area_insets(&*winit_window);
181        let theme = winit_window.theme().unwrap_or(Theme::Light);
182        let color_scheme = theme_to_color_scheme(theme);
183        // The viewport is the window surface minus the safe area insets
184        let viewport_width = size
185            .width
186            .saturating_sub(safe_area_insets.left + safe_area_insets.right);
187        let viewport_height = size
188            .height
189            .saturating_sub(safe_area_insets.top + safe_area_insets.bottom);
190        let viewport = Viewport::new(viewport_width, viewport_height, scale, color_scheme);
191
192        // Create shell provider
193        let shell_provider = BlitzShellProvider::new(winit_window.clone(), proxy.clone());
194
195        let mut doc = config.doc;
196        let mut inner = doc.inner_mut();
197        inner.set_viewport(viewport);
198        inner.set_shell_provider(Arc::new(shell_provider));
199
200        // If the document title is set prior to the window being created then it will
201        // have been sent to a dummy ShellProvider and won't get picked up.
202        // So we look for it here and set it if present.
203        let title = inner.find_title_node().map(|node| node.text_content());
204        if let Some(title) = title {
205            winit_window.set_title(&title);
206        }
207
208        drop(inner);
209
210        Self {
211            renderer: config.renderer,
212            waker: None,
213            animation_timer: None,
214            keyboard_modifiers: Default::default(),
215            proxy: proxy.clone(),
216            window: winit_window.clone(),
217            doc,
218            theme_override: None,
219            buttons: MouseEventButtons::None,
220            active_events: Arc::new(AtomicRefCell::new(Vec::new())),
221            safe_area_insets,
222            #[cfg(target_arch = "wasm32")]
223            pending_resize: None,
224            #[cfg(target_arch = "wasm32")]
225            last_resize_at: None,
226            #[cfg(target_arch = "wasm32")]
227            resize_timer_scheduled: false,
228            pointer_pos: Default::default(),
229            is_visible: winit_window.is_visible().unwrap_or(true),
230            #[cfg(feature = "accessibility")]
231            accessibility,
232
233            #[cfg(target_os = "ios")]
234            ios_request_redraw: std::cell::Cell::new(false),
235        }
236    }
237
238    pub fn replace_document(&mut self, new_doc: Box<dyn Document>, retain_scroll_position: bool) {
239        let inner = self.doc.inner();
240        let scroll = inner.viewport_scroll();
241        let viewport = inner.viewport().clone();
242        let shell_provider = inner.shell_provider.clone();
243        drop(inner);
244
245        self.doc = new_doc;
246
247        let mut inner = self.doc.inner_mut();
248        inner.set_viewport(viewport);
249        inner.set_shell_provider(shell_provider);
250        drop(inner);
251
252        self.poll();
253        self.request_redraw();
254
255        if retain_scroll_position {
256            self.doc.inner_mut().set_viewport_scroll(scroll);
257        }
258    }
259
260    pub fn theme_override(&self) -> Option<Theme> {
261        self.theme_override
262    }
263
264    pub fn current_theme(&self) -> Theme {
265        color_scheme_to_theme(self.doc.inner().viewport().color_scheme)
266    }
267
268    pub fn set_theme_override(&mut self, theme: Option<Theme>) {
269        self.theme_override = theme;
270        let theme = theme.or(self.window.theme()).unwrap_or(Theme::Light);
271        self.with_viewport(|v| v.color_scheme = theme_to_color_scheme(theme));
272    }
273
274    pub fn downcast_doc_mut<T: 'static>(&mut self) -> &mut T {
275        (&mut *self.doc as &mut dyn Any)
276            .downcast_mut::<T>()
277            .unwrap()
278    }
279
280    pub fn current_animation_time(&mut self) -> f64 {
281        match &self.animation_timer {
282            Some(start) => Instant::now().duration_since(*start).as_secs_f64(),
283            None => {
284                self.animation_timer = Some(Instant::now());
285                0.0
286            }
287        }
288    }
289}
290
291impl<Rend: WindowRenderer> View<Rend> {
292    /// Start resuming the renderer. Dispatches [`BlitzShellEvent::ResumeReady`]
293    /// when initialization completes — synchronously on native, asynchronously
294    /// on wasm32. The embedder must call [`complete_resume`](Self::complete_resume)
295    /// in response.
296    pub fn resume(&mut self) {
297        let window_id = self.window_id();
298        let animation_time = self.current_animation_time();
299
300        let (width, height) = {
301            let mut inner = self.doc.inner_mut();
302            inner.resolve(animation_time);
303            inner.viewport().window_size
304        };
305
306        // The render surface covers the entire window, including the safe area
307        let insets = self.safe_area_insets;
308        let width = width + insets.left + insets.right;
309        let height = height + insets.top + insets.bottom;
310
311        let proxy = self.proxy.clone();
312        self.renderer
313            .resume(Arc::new(self.window.clone()), width, height, move || {
314                proxy.send_event(BlitzShellEvent::ResumeReady { window_id });
315            });
316    }
317
318    /// Finalize a previously-started resume. Should be called in response to a
319    /// [`BlitzShellEvent::ResumeReady`] event. Paints the first frame and
320    /// installs the doc poll waker. Returns `true` if the renderer is now active.
321    pub fn complete_resume(&mut self) -> bool {
322        if !self.renderer.complete_resume() {
323            return false;
324        }
325
326        let window_id = self.window_id();
327
328        // Resync the renderer to the current viewport. Resize/scale events that
329        // arrived while the renderer was Pending were no-ops on the renderer
330        // (its `set_size` only matches Active), so the surface created during
331        // resume could be at a stale size by the time we get here.
332        let animation_time = self.current_animation_time();
333        let mut inner = self.doc.inner_mut();
334        inner.resolve(animation_time);
335        let (width, height) = inner.viewport().window_size;
336        let scale = inner.viewport().scale_f64();
337        let insets = self.safe_area_insets;
338
339        #[cfg(feature = "custom-widget")]
340        inner.can_create_surfaces(&mut self.renderer as _);
341
342        // The render surface covers the entire window, including the safe area
343        self.renderer.set_size(
344            width + insets.left + insets.right,
345            height + insets.top + insets.bottom,
346        );
347
348        self.renderer.render(|scene| {
349            paint_scene(
350                scene,
351                &mut inner,
352                scale,
353                width,
354                height,
355                insets.left,
356                insets.top,
357            )
358        });
359
360        self.waker = Some(create_waker(&self.proxy, window_id));
361        true
362    }
363
364    pub fn suspend(&mut self) {
365        self.waker = None;
366        self.renderer.suspend();
367
368        #[cfg(feature = "custom-widget")]
369        self.doc.inner_mut().destroy_surfaces();
370    }
371
372    pub fn poll(&mut self) -> bool {
373        if let Some(waker) = &self.waker {
374            let cx = std::task::Context::from_waker(waker);
375            if self.doc.poll(Some(cx)) {
376                #[cfg(feature = "accessibility")]
377                {
378                    let inner = self.doc.inner();
379                    if inner.has_changes() {
380                        self.accessibility.update_tree(&inner);
381                    }
382                }
383
384                self.request_redraw();
385                return true;
386            }
387        }
388
389        false
390    }
391
392    pub fn request_redraw(&self) {
393        if self.renderer.is_active() {
394            self.window.request_redraw();
395            #[cfg(target_os = "ios")]
396            self.ios_request_redraw.set(true);
397        }
398    }
399
400    pub fn redraw(&mut self) {
401        #[cfg(target_os = "ios")]
402        self.ios_request_redraw.set(false);
403        let animation_time = self.current_animation_time();
404        let is_visible = self.is_visible;
405
406        let mut inner = self.doc.inner_mut();
407        inner.resolve(animation_time);
408
409        // Unregister resources (e.g. textures) from dropped custom widget nodes
410        #[cfg(feature = "custom-widget")]
411        for id in inner.take_pending_resource_deallocations() {
412            self.renderer.unregister_resource(id);
413        }
414
415        let (width, height) = inner.viewport().window_size;
416        let scale = inner.viewport().scale_f64();
417        let is_animating = inner.is_animating();
418        let is_blocked = inner.has_pending_critical_resources();
419        let insets = self.safe_area_insets;
420
421        if !is_blocked && is_visible {
422            self.renderer.render(|scene| {
423                paint_scene(
424                    scene,
425                    &mut inner,
426                    scale,
427                    width,
428                    height,
429                    insets.left,
430                    insets.top,
431                )
432            });
433        }
434
435        drop(inner);
436
437        if !is_blocked && is_visible && is_animating {
438            self.request_redraw();
439        }
440    }
441
442    pub fn pointer_coords(&self, position: PhysicalPosition<f64>) -> PointerCoords {
443        let inner = self.doc.inner();
444        let scale = inner.viewport().scale_f64();
445        let LogicalPosition::<f32> {
446            x: screen_x,
447            y: screen_y,
448        } = position.to_logical(scale);
449        let viewport_scroll_offset = inner.viewport_scroll();
450        let client_x = screen_x - (self.safe_area_insets.left as f64 / scale) as f32;
451        let client_y = screen_y - (self.safe_area_insets.top as f64 / scale) as f32;
452        let page_x = client_x + viewport_scroll_offset.x as f32;
453        let page_y = client_y + viewport_scroll_offset.y as f32;
454
455        PointerCoords {
456            screen_x,
457            screen_y,
458            client_x,
459            client_y,
460            page_x,
461            page_y,
462        }
463    }
464
465    pub fn window_id(&self) -> WindowId {
466        self.window.id()
467    }
468
469    /// Store `event` as an active pointer, replacing any existing entry with the
470    /// same id. The stored event has an empty `active_pointers` list to avoid a
471    /// reference cycle.
472    fn set_active_pointer(&self, event: &BlitzPointerEvent) {
473        let mut stored = event.clone();
474        stored.active_pointers = Default::default();
475
476        let mut active = self.active_events.borrow_mut();
477        if let Some(existing) = active.iter_mut().find(|e| e.id == stored.id) {
478            *existing = stored;
479        } else {
480            active.push(stored);
481        }
482    }
483
484    /// Update the stored position/state of an already-active pointer. Does
485    /// nothing if the pointer is not currently active (e.g. a hovering pen).
486    fn update_active_pointer(&self, event: &BlitzPointerEvent) {
487        let mut active = self.active_events.borrow_mut();
488        if let Some(existing) = active.iter_mut().find(|e| e.id == event.id) {
489            let mut stored = event.clone();
490            stored.active_pointers = Default::default();
491            *existing = stored;
492        }
493    }
494
495    /// Remove an active pointer by id. Returns `true` if it was present.
496    fn remove_active_pointer(&self, id: BlitzPointerId) -> bool {
497        let mut active = self.active_events.borrow_mut();
498        let len_before = active.len();
499        active.retain(|e| e.id != id);
500        active.len() != len_before
501    }
502
503    #[inline]
504    pub fn with_viewport(&mut self, cb: impl FnOnce(&mut Viewport)) {
505        let mut inner = self.doc.inner_mut();
506        let mut viewport = inner.viewport_mut();
507        cb(&mut viewport);
508        let (width, height) = viewport.window_size;
509        drop(viewport);
510        drop(inner);
511        if width > 0 && height > 0 {
512            let insets = self.safe_area_insets;
513            self.renderer.set_size(
514                width + insets.left + insets.right,
515                height + insets.top + insets.bottom,
516            );
517            self.request_redraw();
518        }
519    }
520
521    #[cfg(feature = "accessibility")]
522    pub fn build_accessibility_tree(&mut self) {
523        let inner = self.doc.inner();
524        self.accessibility.update_tree(&inner);
525    }
526
527    #[cfg(target_arch = "wasm32")]
528    const RESIZE_DEBOUNCE_MS: u32 = 100;
529
530    #[cfg(target_arch = "wasm32")]
531    fn schedule_resize_settle_check(&mut self, delay_ms: u32) {
532        use wasm_bindgen::JsCast;
533        use wasm_bindgen::closure::Closure;
534
535        let proxy = self.proxy.clone();
536        let window_id = self.window_id();
537        let cb = Closure::once_into_js(move || {
538            proxy.send_event(BlitzShellEvent::ResizeSettleCheck { window_id });
539        });
540        if let Some(win) = web_sys::window() {
541            let _ = win.set_timeout_with_callback_and_timeout_and_arguments_0(
542                cb.unchecked_ref(),
543                delay_ms as i32,
544            );
545            self.resize_timer_scheduled = true;
546        }
547    }
548
549    /// Applies the pending resize iff motion has been quiet for the debounce
550    /// window; otherwise re-arms the timer for the remaining time. Called
551    /// when a previously scheduled timer fires.
552    #[cfg(target_arch = "wasm32")]
553    pub fn apply_pending_resize_if_settled(&mut self) {
554        self.resize_timer_scheduled = false;
555        let Some(last) = self.last_resize_at else {
556            return;
557        };
558        let debounce = std::time::Duration::from_millis(Self::RESIZE_DEBOUNCE_MS as u64);
559        let elapsed = web_time::Instant::now().saturating_duration_since(last);
560        if elapsed < debounce {
561            // Motion ongoing — wait out the rest of the window before re-checking.
562            let remaining_ms = (debounce - elapsed).as_millis() as u32;
563            self.schedule_resize_settle_check(remaining_ms);
564            return;
565        }
566        let Some(size) = self.pending_resize.take() else {
567            return;
568        };
569        self.last_resize_at = None;
570
571        let insets = self.safe_area_insets;
572        let width = size.width.saturating_sub(insets.left + insets.right);
573        let height = size.height.saturating_sub(insets.top + insets.bottom);
574        self.with_viewport(|v| v.window_size = (width, height));
575        self.request_redraw();
576    }
577
578    #[cfg(target_os = "macos")]
579    pub fn handle_apple_standard_keybinding(&mut self, command: &str) {
580        use blitz_traits::SmolStr;
581        let event = UiEvent::AppleStandardKeybinding(SmolStr::new(command));
582        self.doc.handle_ui_event(event);
583    }
584
585    pub fn handle_winit_event(&mut self, event: WindowEvent) {
586        // Update accessibility focus and window size state in response to a Winit WindowEvent
587        #[cfg(feature = "accessibility")]
588        self.accessibility
589            .process_window_event(&*self.window, &event);
590
591        match event {
592            WindowEvent::Destroyed => {}
593            WindowEvent::ActivationTokenDone { .. } => {},
594            WindowEvent::CloseRequested => {
595                // Currently handled at the level above in application.rs
596            }
597            WindowEvent::RedrawRequested => {
598                self.redraw();
599            }
600            WindowEvent::Moved(_) => {}
601            WindowEvent::Occluded(is_occluded) => {
602                self.is_visible = !is_occluded;
603                if self.is_visible {
604                    self.request_redraw();
605                }
606            },
607            WindowEvent::SurfaceResized(physical_size) => {
608                self.safe_area_insets = get_safe_area_insets(&*self.window);
609                // On WASM, defer the apply: wgpu's surface.configure clears the canvas,
610                // so running it every frame flickers during a drag. The browser stretches
611                // the stale backing store until the debounce timer settles.
612                #[cfg(target_arch = "wasm32")]
613                {
614                    self.pending_resize = Some(physical_size);
615                    self.last_resize_at = Some(web_time::Instant::now());
616                    if !self.resize_timer_scheduled {
617                        self.schedule_resize_settle_check(Self::RESIZE_DEBOUNCE_MS);
618                    }
619                }
620                #[cfg(not(target_arch = "wasm32"))]
621                {
622                    let insets = self.safe_area_insets;
623                    let width = physical_size.width - insets.left - insets.right;
624                    let height = physical_size.height - insets.top - insets.bottom;
625                    self.with_viewport(|v| v.window_size = (width, height));
626                    self.request_redraw();
627                }
628            }
629            WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
630                self.with_viewport(|v| v.set_hidpi_scale(scale_factor as f32));
631                self.request_redraw();
632            }
633            WindowEvent::ThemeChanged(theme) => {
634                let color_scheme = theme_to_color_scheme(self.theme_override.unwrap_or(theme));
635                let mut inner = self.doc.inner_mut();
636                inner.viewport_mut().color_scheme = color_scheme;
637            }
638            WindowEvent::Ime(ime_event) => {
639                self.doc.handle_ui_event(UiEvent::Ime(winit_ime_to_blitz(ime_event)));
640                self.request_redraw();
641            },
642            WindowEvent::ModifiersChanged(new_state) => {
643                // Store new keyboard modifier (ctrl, shift, etc) state for later use
644                self.keyboard_modifiers = new_state;
645            }
646            WindowEvent::KeyboardInput { event, .. } => {
647                if let PhysicalKey::Code(key_code) = event.physical_key && event.state.is_pressed() {
648                        let ctrl = self.keyboard_modifiers.state().control_key();
649                        let meta = self.keyboard_modifiers.state().meta_key();
650                        let alt = self.keyboard_modifiers.state().alt_key();
651
652                        // Ctrl/Super keyboard shortcuts
653                        if ctrl | meta {
654                            match key_code {
655                                KeyCode::Equal => {
656                                    self.doc.inner_mut().viewport_mut().zoom_by(0.1);
657                                },
658                                KeyCode::Minus => {
659                                    self.doc.inner_mut().viewport_mut().zoom_by(-0.1);
660                                },
661                                KeyCode::Digit0 => {
662                                    self.doc.inner_mut().viewport_mut().set_zoom(1.0);
663                                }
664                                _ => {}
665                            };
666                        }
667
668                        // Alt keyboard shortcuts
669                        if alt {
670                            match key_code {
671                                KeyCode::KeyD => {
672                                    let mut inner = self.doc.inner_mut();
673                                    inner.devtools_mut().toggle_show_layout();
674                                    drop(inner);
675                                    self.request_redraw();
676                                }
677                                KeyCode::KeyH => {
678                                    let mut inner = self.doc.inner_mut();
679                                    inner.devtools_mut().toggle_highlight_hover();
680                                    drop(inner);
681                                    self.request_redraw();
682                                }
683                                KeyCode::KeyT => self.doc.inner().print_taffy_tree(),
684                                _ => {}
685                            };
686                        }
687
688                }
689
690                // Unmodified keypresses
691                let key_event_data = winit_key_event_to_blitz(&event, self.keyboard_modifiers.state());
692                let event = if event.state.is_pressed() {
693                    UiEvent::KeyDown(key_event_data)
694                } else {
695                    UiEvent::KeyUp(key_event_data)
696                };
697
698                self.doc.handle_ui_event(event);
699            }
700            WindowEvent::PointerEntered { /*device_id*/.. } => {}
701            WindowEvent::PointerLeft { position, primary, kind, .. } => {
702                let id = pointer_kind_to_blitz(&kind);
703
704                // A `PointerLeft` for a non-mouse pointer that is still pressed
705                // (i.e. we never saw a `PointerButton` with `Released` for it)
706                // means the system cancelled tracking of this touch/pen. Emit a
707                // pointercancel in that case. A mouse simply leaving the window,
708                // or a touch that was already released, is not a cancellation.
709                // Remove from the active list first so the cancelled pointer is
710                // excluded from this event's `touches`. `remove_active_pointer`
711                // reports whether the pointer was actually active.
712                if id != BlitzPointerId::Mouse && self.remove_active_pointer(id) {
713                    let position = position.unwrap_or(self.pointer_pos);
714                    self.pointer_pos = position;
715
716                    // The pointer is no longer pressed.
717                    self.buttons ^= MouseEventButton::Main.into();
718
719                    let event = BlitzPointerEvent {
720                        id,
721                        is_primary: primary,
722                        coords: self.pointer_coords(position),
723                        button: MouseEventButton::Main,
724                        buttons: self.buttons,
725                        mods: winit_modifiers_to_kbt_modifiers(self.keyboard_modifiers.state()),
726                        details: PointerDetails::default(),
727                        element: Default::default(),
728                        active_pointers: Arc::clone(&self.active_events),
729                    };
730
731                    self.doc.handle_ui_event(UiEvent::PointerCancel(event));
732                    self.request_redraw();
733                }
734            }
735            WindowEvent::PointerMoved { position, source, primary, .. } => {
736                self.pointer_pos = position;
737                let id = pointer_source_to_blitz(&source);
738                let event = BlitzPointerEvent {
739                    id,
740                    is_primary: primary,
741                    coords: self.pointer_coords(position),
742                    button: Default::default(),
743                    buttons: self.buttons,
744                    mods: winit_modifiers_to_kbt_modifiers(self.keyboard_modifiers.state()),
745                    details: pointer_source_to_blitz_details(&source),
746                    element: Default::default(),
747                    active_pointers: Arc::clone(&self.active_events),
748                };
749                // Keep multi-touch positions current (no-op for non-active pointers).
750                if id != BlitzPointerId::Mouse {
751                    self.update_active_pointer(&event);
752                }
753                self.doc.handle_ui_event(UiEvent::PointerMove(event));
754            }
755            WindowEvent::PointerButton { button, state, primary, position, .. } => {
756                let id = button_source_to_blitz(&button);
757                let coords = self.pointer_coords(position);
758                self.pointer_pos = position;
759                let button = match &button {
760                    ButtonSource::Mouse(mouse_button) => match mouse_button {
761                        MouseButton::Left => MouseEventButton::Main,
762                        MouseButton::Right => MouseEventButton::Secondary,
763                        MouseButton::Middle => MouseEventButton::Auxiliary,
764                        // TODO: handle other button types
765                        _ => MouseEventButton::Auxiliary,
766                    }
767                    _ => MouseEventButton::Main,
768                };
769
770                match state {
771                    ElementState::Pressed => self.buttons |= button.into(),
772                    ElementState::Released => self.buttons ^= button.into(),
773                }
774
775                let pointer_event = BlitzPointerEvent {
776                    id,
777                    is_primary: primary,
778                    coords,
779                    button,
780                    buttons: self.buttons,
781                    mods: winit_modifiers_to_kbt_modifiers(self.keyboard_modifiers.state()),
782
783                    // TODO: details for pointer up/down events
784                    details: PointerDetails::default(),
785                    element: Default::default(),
786                    active_pointers: Arc::clone(&self.active_events),
787                };
788
789                // Maintain the list of active (pressed) non-mouse pointers. A
790                // press adds the pointer *before* dispatch (so touchstart's
791                // `touches` includes it). A release is handled after the
792                // synthetic move below so the move still sees it, but before the
793                // pointerup so touchend's `touches` excludes it.
794                if id != BlitzPointerId::Mouse && state == ElementState::Pressed {
795                    self.set_active_pointer(&pointer_event);
796                }
797
798                // Touch input doesn't emit a `PointerMoved` before the button
799                // event the way a mouse does, so synthesise a move to update the
800                // hover/hit position to the touch location.
801                if id != BlitzPointerId::Mouse {
802                    let event = BlitzPointerEvent {
803                        id,
804                        is_primary: primary,
805                        coords,
806                        button: Default::default(),
807                        buttons: self.buttons,
808                        mods: winit_modifiers_to_kbt_modifiers(self.keyboard_modifiers.state()),
809                        details: PointerDetails::default(),
810                        element: Default::default(),
811                        active_pointers: Arc::clone(&self.active_events),
812                    };
813                    self.doc.handle_ui_event(UiEvent::PointerMove(event));
814                }
815
816                if id != BlitzPointerId::Mouse && state == ElementState::Released {
817                    self.remove_active_pointer(id);
818                }
819
820                let event = pointer_event;
821
822                let event = match state {
823                    ElementState::Pressed => UiEvent::PointerDown(event),
824                    ElementState::Released => UiEvent::PointerUp(event),
825                };
826
827                self.doc.handle_ui_event(event);
828                self.request_redraw();
829            }
830            WindowEvent::MouseWheel { delta, .. } => {
831                let blitz_delta = match delta {
832                    winit::event::MouseScrollDelta::LineDelta(x, y) => BlitzWheelDelta::Lines(x as f64, y as f64),
833                    winit::event::MouseScrollDelta::PixelDelta(pos) => BlitzWheelDelta::Pixels(pos.x, pos.y),
834                };
835
836                let event = BlitzWheelEvent {
837                    delta: blitz_delta,
838                    coords: self.pointer_coords(self.pointer_pos),
839                    buttons: self.buttons,
840                    mods: winit_modifiers_to_kbt_modifiers(self.keyboard_modifiers.state()),
841                    element: Default::default()
842                };
843
844                self.doc.handle_ui_event(UiEvent::Wheel(event));
845            }
846            WindowEvent::Focused(_) => {}
847            WindowEvent::TouchpadPressure { .. } => {}
848            WindowEvent::PinchGesture { .. } => {},
849            WindowEvent::PanGesture { .. } => {},
850            WindowEvent::DoubleTapGesture { .. } => {},
851            WindowEvent::RotationGesture { .. } => {},
852            WindowEvent::DragEntered { .. } => {},
853            WindowEvent::DragMoved { .. } => {},
854            WindowEvent::DragDropped { .. } => {},
855            WindowEvent::DragLeft { .. } => {},
856        }
857    }
858}