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::{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 std::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
31pub struct WindowConfig<Rend: WindowRenderer> {
32    doc: Box<dyn Document>,
33    attributes: WindowAttributes,
34    renderer: Rend,
35}
36
37impl<Rend: WindowRenderer> WindowConfig<Rend> {
38    pub fn new(doc: Box<dyn Document>, renderer: Rend) -> Self {
39        Self::with_attributes(doc, renderer, WindowAttributes::default())
40    }
41
42    pub fn with_attributes(
43        doc: Box<dyn Document>,
44        renderer: Rend,
45        attributes: WindowAttributes,
46    ) -> Self {
47        WindowConfig {
48            doc,
49            attributes,
50            renderer,
51        }
52    }
53}
54
55pub struct View<Rend: WindowRenderer> {
56    pub doc: Box<dyn Document>,
57
58    pub renderer: Rend,
59    pub waker: Option<Waker>,
60
61    pub proxy: BlitzShellProxy,
62    pub window: Arc<dyn Window>,
63
64    /// The state of the keyboard modifiers (ctrl, shift, etc). Winit/Tao don't track these for us so we
65    /// need to store them in order to have access to them when processing keypress events
66    pub theme_override: Option<Theme>,
67    pub keyboard_modifiers: Modifiers,
68    pub buttons: MouseEventButtons,
69    pub pointer_pos: PhysicalPosition<f64>,
70    pub animation_timer: Option<Instant>,
71    pub is_visible: bool,
72    pub safe_area_insets: PhysicalInsets<u32>,
73
74    #[cfg(feature = "accessibility")]
75    /// Accessibility adapter for `accesskit`.
76    pub accessibility: AccessibilityState,
77
78    // Calling request_redraw within a WindowEvent doesn't work on iOS. So on iOS we track the state
79    // with a boolean and call request_redraw in about_to_wait
80    //
81    // See https://github.com/rust-windowing/winit/issues/3406
82    #[cfg(target_os = "ios")]
83    pub ios_request_redraw: std::cell::Cell<bool>,
84}
85
86impl<Rend: WindowRenderer> View<Rend> {
87    pub fn init(
88        config: WindowConfig<Rend>,
89        event_loop: &dyn ActiveEventLoop,
90        proxy: &BlitzShellProxy,
91    ) -> Self {
92        // We create window as invisble and then later make window visible
93        // after AccessKit has initialised to avoid AccessKit panics
94        let is_visible = config.attributes.visible;
95        let attrs = config.attributes.with_visible(false);
96
97        let winit_window: Arc<dyn Window> = Arc::from(event_loop.create_window(attrs).unwrap());
98        #[cfg(feature = "accessibility")]
99        let accessibility = AccessibilityState::new(&*winit_window, proxy.clone());
100
101        if is_visible {
102            winit_window.set_visible(true);
103        }
104
105        // Create viewport
106        // TODO: account for the "safe area"
107        let size = winit_window.surface_size();
108        let scale = winit_window.scale_factor() as f32;
109        let safe_area_insets = winit_window.safe_area();
110        let theme = winit_window.theme().unwrap_or(Theme::Light);
111        let color_scheme = theme_to_color_scheme(theme);
112        let viewport = Viewport::new(size.width, size.height, scale, color_scheme);
113
114        // Create shell provider
115        let shell_provider = BlitzShellProvider::new(winit_window.clone());
116
117        let mut doc = config.doc;
118        let mut inner = doc.inner_mut();
119        inner.set_viewport(viewport);
120        inner.set_shell_provider(Arc::new(shell_provider));
121
122        // If the document title is set prior to the window being created then it will
123        // have been sent to a dummy ShellProvider and won't get picked up.
124        // So we look for it here and set it if present.
125        let title = inner.find_title_node().map(|node| node.text_content());
126        if let Some(title) = title {
127            winit_window.set_title(&title);
128        }
129
130        drop(inner);
131
132        Self {
133            renderer: config.renderer,
134            waker: None,
135            animation_timer: None,
136            keyboard_modifiers: Default::default(),
137            proxy: proxy.clone(),
138            window: winit_window.clone(),
139            doc,
140            theme_override: None,
141            buttons: MouseEventButtons::None,
142            safe_area_insets,
143            pointer_pos: Default::default(),
144            is_visible: winit_window.is_visible().unwrap_or(true),
145            #[cfg(feature = "accessibility")]
146            accessibility,
147
148            #[cfg(target_os = "ios")]
149            ios_request_redraw: std::cell::Cell::new(false),
150        }
151    }
152
153    pub fn replace_document(&mut self, new_doc: Box<dyn Document>, retain_scroll_position: bool) {
154        let inner = self.doc.inner();
155        let scroll = inner.viewport_scroll();
156        let viewport = inner.viewport().clone();
157        let shell_provider = inner.shell_provider.clone();
158        drop(inner);
159
160        self.doc = new_doc;
161
162        let mut inner = self.doc.inner_mut();
163        inner.set_viewport(viewport);
164        inner.set_shell_provider(shell_provider);
165        drop(inner);
166
167        self.poll();
168        self.request_redraw();
169
170        if retain_scroll_position {
171            self.doc.inner_mut().set_viewport_scroll(scroll);
172        }
173    }
174
175    pub fn theme_override(&self) -> Option<Theme> {
176        self.theme_override
177    }
178
179    pub fn current_theme(&self) -> Theme {
180        color_scheme_to_theme(self.doc.inner().viewport().color_scheme)
181    }
182
183    pub fn set_theme_override(&mut self, theme: Option<Theme>) {
184        self.theme_override = theme;
185        let theme = theme.or(self.window.theme()).unwrap_or(Theme::Light);
186        self.with_viewport(|v| v.color_scheme = theme_to_color_scheme(theme));
187    }
188
189    pub fn downcast_doc_mut<T: 'static>(&mut self) -> &mut T {
190        (&mut *self.doc as &mut dyn Any)
191            .downcast_mut::<T>()
192            .unwrap()
193    }
194
195    pub fn current_animation_time(&mut self) -> f64 {
196        match &self.animation_timer {
197            Some(start) => Instant::now().duration_since(*start).as_secs_f64(),
198            None => {
199                self.animation_timer = Some(Instant::now());
200                0.0
201            }
202        }
203    }
204}
205
206impl<Rend: WindowRenderer> View<Rend> {
207    pub fn resume(&mut self) {
208        let window_id = self.window_id();
209        let animation_time = self.current_animation_time();
210
211        let mut inner = self.doc.inner_mut();
212
213        // Resolve dom
214        inner.resolve(animation_time);
215
216        // Resume renderer
217        let (width, height) = inner.viewport().window_size;
218        let scale = inner.viewport().scale_f64();
219        self.renderer
220            .resume(Arc::new(self.window.clone()), width, height);
221        if !self.renderer.is_active() {
222            panic!("Renderer failed to resume");
223        };
224
225        // Render
226        let insets = self.safe_area_insets.to_logical(scale);
227        self.renderer.render(|scene| {
228            paint_scene(scene, &inner, scale, width, height, insets.left, insets.top)
229        });
230
231        // Set waker
232        self.waker = Some(create_waker(&self.proxy, window_id));
233    }
234
235    pub fn suspend(&mut self) {
236        self.waker = None;
237        self.renderer.suspend();
238    }
239
240    pub fn poll(&mut self) -> bool {
241        if let Some(waker) = &self.waker {
242            let cx = std::task::Context::from_waker(waker);
243            if self.doc.poll(Some(cx)) {
244                #[cfg(feature = "accessibility")]
245                {
246                    let inner = self.doc.inner();
247                    if inner.has_changes() {
248                        self.accessibility.update_tree(&inner);
249                    }
250                }
251
252                self.request_redraw();
253                return true;
254            }
255        }
256
257        false
258    }
259
260    pub fn request_redraw(&self) {
261        if self.renderer.is_active() {
262            self.window.request_redraw();
263            #[cfg(target_os = "ios")]
264            self.ios_request_redraw.set(true);
265        }
266    }
267
268    pub fn redraw(&mut self) {
269        #[cfg(target_os = "ios")]
270        self.ios_request_redraw.set(false);
271        let animation_time = self.current_animation_time();
272        let is_visible = self.is_visible;
273
274        let mut inner = self.doc.inner_mut();
275        inner.resolve(animation_time);
276
277        let (width, height) = inner.viewport().window_size;
278        let scale = inner.viewport().scale_f64();
279        let is_animating = inner.is_animating();
280        let is_blocked = inner.has_pending_critical_resources();
281        let insets = self.safe_area_insets.to_logical(scale);
282
283        if !is_blocked && is_visible {
284            self.renderer.render(|scene| {
285                paint_scene(scene, &inner, scale, width, height, insets.left, insets.top)
286            });
287        }
288
289        drop(inner);
290
291        if !is_blocked && is_visible && is_animating {
292            self.request_redraw();
293        }
294    }
295
296    pub fn pointer_coords(&self, position: PhysicalPosition<f64>) -> PointerCoords {
297        let inner = self.doc.inner();
298        let scale = inner.viewport().scale_f64();
299        let LogicalPosition::<f32> {
300            x: screen_x,
301            y: screen_y,
302        } = position.to_logical(scale);
303        let viewport_scroll_offset = inner.viewport_scroll();
304        let client_x = screen_x - (self.safe_area_insets.left as f64 / scale) as f32;
305        let client_y = screen_y - (self.safe_area_insets.top as f64 / scale) as f32;
306        let page_x = client_x + viewport_scroll_offset.x as f32;
307        let page_y = client_y + viewport_scroll_offset.y as f32;
308
309        PointerCoords {
310            screen_x,
311            screen_y,
312            client_x,
313            client_y,
314            page_x,
315            page_y,
316        }
317    }
318
319    pub fn window_id(&self) -> WindowId {
320        self.window.id()
321    }
322
323    #[inline]
324    pub fn with_viewport(&mut self, cb: impl FnOnce(&mut Viewport)) {
325        let mut inner = self.doc.inner_mut();
326        let mut viewport = inner.viewport_mut();
327        cb(&mut viewport);
328        let (width, height) = viewport.window_size;
329        drop(viewport);
330        drop(inner);
331        if width > 0 && height > 0 {
332            let insets = self.safe_area_insets;
333            self.renderer.set_size(
334                width + insets.left + insets.right,
335                height + insets.top + insets.bottom,
336            );
337            self.request_redraw();
338        }
339    }
340
341    #[cfg(feature = "accessibility")]
342    pub fn build_accessibility_tree(&mut self) {
343        let inner = self.doc.inner();
344        self.accessibility.update_tree(&inner);
345    }
346
347    #[cfg(target_os = "macos")]
348    pub fn handle_apple_standard_keybinding(&mut self, command: &str) {
349        use blitz_traits::SmolStr;
350        let event = UiEvent::AppleStandardKeybinding(SmolStr::new(command));
351        self.doc.handle_ui_event(event);
352    }
353
354    pub fn handle_winit_event(&mut self, event: WindowEvent) {
355        // Update accessibility focus and window size state in response to a Winit WindowEvent
356        #[cfg(feature = "accessibility")]
357        self.accessibility
358            .process_window_event(&*self.window, &event);
359
360        match event {
361            WindowEvent::Destroyed => {}
362            WindowEvent::ActivationTokenDone { .. } => {},
363            WindowEvent::CloseRequested => {
364                // Currently handled at the level above in application.rs
365            }
366            WindowEvent::RedrawRequested => {
367                self.redraw();
368            }
369            WindowEvent::Moved(_) => {}
370            WindowEvent::Occluded(is_occluded) => {
371                self.is_visible = !is_occluded;
372                if self.is_visible {
373                    self.request_redraw();
374                }
375            },
376            WindowEvent::SurfaceResized(physical_size) => {
377                self.safe_area_insets = self.window.safe_area();
378                let insets = self.safe_area_insets;
379                let width = physical_size.width - insets.left - insets.right;
380                let height = physical_size.height - insets.top - insets.bottom;
381                self.with_viewport(|v| v.window_size = (width, height));
382                self.request_redraw();
383            }
384            WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
385                self.with_viewport(|v| v.set_hidpi_scale(scale_factor as f32));
386                self.request_redraw();
387            }
388            WindowEvent::ThemeChanged(theme) => {
389                let color_scheme = theme_to_color_scheme(self.theme_override.unwrap_or(theme));
390                let mut inner = self.doc.inner_mut();
391                inner.viewport_mut().color_scheme = color_scheme;
392            }
393            WindowEvent::Ime(ime_event) => {
394                self.doc.handle_ui_event(UiEvent::Ime(winit_ime_to_blitz(ime_event)));
395                self.request_redraw();
396            },
397            WindowEvent::ModifiersChanged(new_state) => {
398                // Store new keyboard modifier (ctrl, shift, etc) state for later use
399                self.keyboard_modifiers = new_state;
400            }
401            WindowEvent::KeyboardInput { event, .. } => {
402                if let PhysicalKey::Code(key_code) = event.physical_key && event.state.is_pressed() {
403                        let ctrl = self.keyboard_modifiers.state().control_key();
404                        let meta = self.keyboard_modifiers.state().meta_key();
405                        let alt = self.keyboard_modifiers.state().alt_key();
406
407                        // Ctrl/Super keyboard shortcuts
408                        if ctrl | meta {
409                            match key_code {
410                                KeyCode::Equal => {
411                                    self.doc.inner_mut().viewport_mut().zoom_by(0.1);
412                                },
413                                KeyCode::Minus => {
414                                    self.doc.inner_mut().viewport_mut().zoom_by(-0.1);
415                                },
416                                KeyCode::Digit0 => {
417                                    self.doc.inner_mut().viewport_mut().set_zoom(1.0);
418                                }
419                                _ => {}
420                            };
421                        }
422
423                        // Alt keyboard shortcuts
424                        if alt {
425                            match key_code {
426                                KeyCode::KeyD => {
427                                    let mut inner = self.doc.inner_mut();
428                                    inner.devtools_mut().toggle_show_layout();
429                                    drop(inner);
430                                    self.request_redraw();
431                                }
432                                KeyCode::KeyH => {
433                                    let mut inner = self.doc.inner_mut();
434                                    inner.devtools_mut().toggle_highlight_hover();
435                                    drop(inner);
436                                    self.request_redraw();
437                                }
438                                KeyCode::KeyT => self.doc.inner().print_taffy_tree(),
439                                _ => {}
440                            };
441                        }
442
443                }
444
445                // Unmodified keypresses
446                let key_event_data = winit_key_event_to_blitz(&event, self.keyboard_modifiers.state());
447                let event = if event.state.is_pressed() {
448                    UiEvent::KeyDown(key_event_data)
449                } else {
450                    UiEvent::KeyUp(key_event_data)
451                };
452
453                self.doc.handle_ui_event(event);
454            }
455            WindowEvent::PointerEntered { /*device_id*/.. } => {}
456            WindowEvent::PointerLeft { /*device_id*/.. } => {}
457            WindowEvent::PointerMoved { position, source, primary, .. } => {
458                self.pointer_pos = position;
459                let event = UiEvent::PointerMove(BlitzPointerEvent {
460                    id: pointer_source_to_blitz(&source),
461                    is_primary: primary,
462                    coords: self.pointer_coords(position),
463                    button: Default::default(),
464                    buttons: self.buttons,
465                    mods: winit_modifiers_to_kbt_modifiers(self.keyboard_modifiers.state()),
466                    details: pointer_source_to_blitz_details(&source)
467                });
468                self.doc.handle_ui_event(event);
469            }
470            WindowEvent::PointerButton { button, state, primary, position, .. } => {
471                let id = button_source_to_blitz(&button);
472                let coords = self.pointer_coords(position);
473                self.pointer_pos = position;
474                let button = match &button {
475                    ButtonSource::Mouse(mouse_button) => match mouse_button {
476                        MouseButton::Left => MouseEventButton::Main,
477                        MouseButton::Right => MouseEventButton::Secondary,
478                        MouseButton::Middle => MouseEventButton::Auxiliary,
479                        // TODO: handle other button types
480                        _ => MouseEventButton::Auxiliary,
481                    }
482                    _ => MouseEventButton::Main,
483                };
484
485                match state {
486                    ElementState::Pressed => self.buttons |= button.into(),
487                    ElementState::Released => self.buttons ^= button.into(),
488                }
489
490                if id != BlitzPointerId::Mouse {
491                    let event = UiEvent::PointerMove(BlitzPointerEvent {
492                        id,
493                        is_primary: primary,
494                        coords,
495                        button: Default::default(),
496                        buttons: self.buttons,
497                        mods: winit_modifiers_to_kbt_modifiers(self.keyboard_modifiers.state()),
498                        details: PointerDetails::default()
499                    });
500                    self.doc.handle_ui_event(event);
501                }
502
503                let event = BlitzPointerEvent {
504                    id,
505                    is_primary: primary,
506                    coords,
507                    button,
508                    buttons: self.buttons,
509                    mods: winit_modifiers_to_kbt_modifiers(self.keyboard_modifiers.state()),
510
511                    // TODO: details for pointer up/down events
512                    details: PointerDetails::default(),
513                };
514
515                let event = match state {
516                    ElementState::Pressed => UiEvent::PointerDown(event),
517                    ElementState::Released => UiEvent::PointerUp(event),
518                };
519
520                self.doc.handle_ui_event(event);
521                self.request_redraw();
522            }
523            WindowEvent::MouseWheel { delta, .. } => {
524                let blitz_delta = match delta {
525                    winit::event::MouseScrollDelta::LineDelta(x, y) => BlitzWheelDelta::Lines(x as f64, y as f64),
526                    winit::event::MouseScrollDelta::PixelDelta(pos) => BlitzWheelDelta::Pixels(pos.x, pos.y),
527                };
528
529                let event = BlitzWheelEvent {
530                    delta: blitz_delta,
531                    coords: self.pointer_coords(self.pointer_pos),
532                    buttons: self.buttons,
533                    mods: winit_modifiers_to_kbt_modifiers(self.keyboard_modifiers.state()),
534                };
535
536                self.doc.handle_ui_event(UiEvent::Wheel(event));
537            }
538            WindowEvent::Focused(_) => {}
539            WindowEvent::TouchpadPressure { .. } => {}
540            WindowEvent::PinchGesture { .. } => {},
541            WindowEvent::PanGesture { .. } => {},
542            WindowEvent::DoubleTapGesture { .. } => {},
543            WindowEvent::RotationGesture { .. } => {},
544            WindowEvent::DragEntered { .. } => {},
545            WindowEvent::DragMoved { .. } => {},
546            WindowEvent::DragDropped { .. } => {},
547            WindowEvent::DragLeft { .. } => {},
548        }
549    }
550}