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