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