blitz_shell/
window.rs

1use crate::BlitzShellProvider;
2use crate::convert_events::{
3    color_scheme_to_theme, theme_to_color_scheme, winit_ime_to_blitz, winit_key_event_to_blitz,
4    winit_modifiers_to_kbt_modifiers,
5};
6use crate::event::{BlitzShellEvent, create_waker};
7use anyrender::WindowRenderer;
8use blitz_dom::Document;
9use blitz_paint::paint_scene;
10use blitz_traits::events::{BlitzMouseButtonEvent, MouseEventButton, MouseEventButtons, UiEvent};
11use blitz_traits::shell::Viewport;
12use winit::keyboard::PhysicalKey;
13
14use std::sync::Arc;
15use std::task::Waker;
16use std::time::Instant;
17use winit::event::{ElementState, MouseButton};
18use winit::event_loop::{ActiveEventLoop, EventLoopProxy};
19use winit::window::{Theme, WindowAttributes, WindowId};
20use winit::{event::Modifiers, event::WindowEvent, keyboard::KeyCode, window::Window};
21
22#[cfg(feature = "accessibility")]
23use crate::accessibility::AccessibilityState;
24
25pub struct WindowConfig<Rend: WindowRenderer> {
26    doc: Box<dyn Document>,
27    attributes: WindowAttributes,
28    renderer: Rend,
29}
30
31impl<Rend: WindowRenderer> WindowConfig<Rend> {
32    pub fn new(doc: Box<dyn Document>, renderer: Rend) -> Self {
33        Self::with_attributes(doc, renderer, Window::default_attributes())
34    }
35
36    pub fn with_attributes(
37        doc: Box<dyn Document>,
38        renderer: Rend,
39        attributes: WindowAttributes,
40    ) -> Self {
41        WindowConfig {
42            doc,
43            attributes,
44            renderer,
45        }
46    }
47}
48
49pub struct View<Rend: WindowRenderer> {
50    pub doc: Box<dyn Document>,
51
52    pub renderer: Rend,
53    pub waker: Option<Waker>,
54
55    pub event_loop_proxy: EventLoopProxy<BlitzShellEvent>,
56    pub window: Arc<Window>,
57
58    /// The state of the keyboard modifiers (ctrl, shift, etc). Winit/Tao don't track these for us so we
59    /// need to store them in order to have access to them when processing keypress events
60    pub theme_override: Option<Theme>,
61    pub keyboard_modifiers: Modifiers,
62    pub buttons: MouseEventButtons,
63    pub mouse_pos: (f32, f32),
64    pub animation_timer: Option<Instant>,
65    pub is_visible: bool,
66
67    #[cfg(feature = "accessibility")]
68    /// Accessibility adapter for `accesskit`.
69    pub accessibility: AccessibilityState,
70}
71
72impl<Rend: WindowRenderer> View<Rend> {
73    pub fn init(
74        config: WindowConfig<Rend>,
75        event_loop: &ActiveEventLoop,
76        proxy: &EventLoopProxy<BlitzShellEvent>,
77    ) -> Self {
78        let winit_window = Arc::from(event_loop.create_window(config.attributes).unwrap());
79
80        // TODO: make this conditional on text input focus
81        winit_window.set_ime_allowed(true);
82
83        // Create viewport
84        let size = winit_window.inner_size();
85        let scale = winit_window.scale_factor() as f32;
86        let theme = winit_window.theme().unwrap_or(Theme::Light);
87        let color_scheme = theme_to_color_scheme(theme);
88        let viewport = Viewport::new(size.width, size.height, scale, color_scheme);
89
90        // Create shell provider
91        let shell_provider = BlitzShellProvider::new(winit_window.clone());
92
93        let mut doc = config.doc;
94        doc.set_viewport(viewport);
95        doc.set_shell_provider(Arc::new(shell_provider));
96
97        // If the document title is set prior to the window being created then it will
98        // have been sent to a dummy ShellProvider and won't get picked up.
99        // So we look for it here and set it if present.
100        let title = doc.find_title_node().map(|node| node.text_content());
101        if let Some(title) = title {
102            winit_window.set_title(&title);
103        }
104
105        Self {
106            renderer: config.renderer,
107            waker: None,
108            animation_timer: None,
109            keyboard_modifiers: Default::default(),
110            event_loop_proxy: proxy.clone(),
111            window: winit_window.clone(),
112            doc,
113            theme_override: None,
114            buttons: MouseEventButtons::None,
115            mouse_pos: Default::default(),
116            is_visible: winit_window.is_visible().unwrap_or(true),
117            #[cfg(feature = "accessibility")]
118            accessibility: AccessibilityState::new(&winit_window, proxy.clone()),
119        }
120    }
121
122    pub fn replace_document(&mut self, new_doc: Box<dyn Document>, retain_scroll_position: bool) {
123        let scroll = self.doc.viewport_scroll();
124        let viewport = self.doc.viewport().clone();
125        let shell_provider = self.doc.shell_provider.clone();
126
127        self.doc = new_doc;
128        self.doc.set_viewport(viewport);
129        self.doc.set_shell_provider(shell_provider);
130        self.poll();
131        self.request_redraw();
132
133        if retain_scroll_position {
134            self.doc.set_viewport_scroll(scroll);
135        }
136    }
137
138    pub fn theme_override(&self) -> Option<Theme> {
139        self.theme_override
140    }
141
142    pub fn current_theme(&self) -> Theme {
143        color_scheme_to_theme(self.doc.viewport().color_scheme)
144    }
145
146    pub fn set_theme_override(&mut self, theme: Option<Theme>) {
147        self.theme_override = theme;
148        let theme = theme.or(self.window.theme()).unwrap_or(Theme::Light);
149        self.with_viewport(|v| v.color_scheme = theme_to_color_scheme(theme));
150    }
151
152    pub fn downcast_doc_mut<T: 'static>(&mut self) -> &mut T {
153        self.doc.as_any_mut().downcast_mut::<T>().unwrap()
154    }
155
156    pub fn current_animation_time(&mut self) -> f64 {
157        match &self.animation_timer {
158            Some(start) => Instant::now().duration_since(*start).as_secs_f64(),
159            None => {
160                self.animation_timer = Some(Instant::now());
161                0.0
162            }
163        }
164    }
165}
166
167impl<Rend: WindowRenderer> View<Rend> {
168    pub fn resume(&mut self) {
169        // Resolve dom
170        let animation_time = self.current_animation_time();
171        self.doc.resolve(animation_time);
172
173        // Resume renderer
174        let (width, height) = self.doc.viewport().window_size;
175        let scale = self.doc.viewport().scale_f64();
176        self.renderer.resume(self.window.clone(), width, height);
177        if !self.renderer.is_active() {
178            panic!("Renderer failed to resume");
179        };
180
181        // Render
182        self.renderer
183            .render(|scene| paint_scene(scene, &self.doc, scale, width, height));
184
185        // Set waker
186        self.waker = Some(create_waker(&self.event_loop_proxy, self.window_id()));
187    }
188
189    pub fn suspend(&mut self) {
190        self.waker = None;
191        self.renderer.suspend();
192    }
193
194    pub fn poll(&mut self) -> bool {
195        if let Some(waker) = &self.waker {
196            let cx = std::task::Context::from_waker(waker);
197            if self.doc.poll(Some(cx)) {
198                #[cfg(feature = "accessibility")]
199                {
200                    if self.doc.has_changes() {
201                        self.accessibility.update_tree(&self.doc);
202                    }
203                }
204
205                self.request_redraw();
206                return true;
207            }
208        }
209
210        false
211    }
212
213    pub fn request_redraw(&self) {
214        if self.renderer.is_active() {
215            self.window.request_redraw();
216        }
217    }
218
219    pub fn redraw(&mut self) {
220        let animation_time = self.current_animation_time();
221        self.doc.resolve(animation_time);
222        let (width, height) = self.doc.viewport().window_size;
223        let scale = self.doc.viewport().scale_f64();
224        self.renderer
225            .render(|scene| paint_scene(scene, &self.doc, scale, width, height));
226
227        if self.is_visible && self.doc.is_animating() {
228            self.request_redraw();
229        }
230    }
231
232    pub fn window_id(&self) -> WindowId {
233        self.window.id()
234    }
235
236    #[inline]
237    pub fn with_viewport(&mut self, cb: impl FnOnce(&mut Viewport)) {
238        let mut viewport = self.doc.viewport_mut();
239        cb(&mut viewport);
240        drop(viewport);
241        let (width, height) = self.doc.viewport().window_size;
242        if width > 0 && height > 0 {
243            self.renderer.set_size(width, height);
244            self.request_redraw();
245        }
246    }
247
248    #[cfg(feature = "accessibility")]
249    pub fn build_accessibility_tree(&mut self) {
250        self.accessibility.update_tree(&self.doc);
251    }
252
253    pub fn handle_winit_event(&mut self, event: WindowEvent) {
254        match event {
255            // Window lifecycle events
256            WindowEvent::Destroyed => {}
257            WindowEvent::ActivationTokenDone { .. } => {},
258            WindowEvent::CloseRequested => {
259                // Currently handled at the level above in application.rs
260            }
261            WindowEvent::RedrawRequested => {
262                self.redraw();
263            }
264
265            // Window size/position events
266            WindowEvent::Moved(_) => {}
267            WindowEvent::Occluded(is_occluded) => {
268                self.is_visible = !is_occluded;
269                if self.is_visible {
270                    self.request_redraw();
271                }
272            },
273            WindowEvent::Resized(physical_size) => {
274                self.with_viewport(|v| v.window_size = (physical_size.width, physical_size.height));
275            }
276            WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
277                self.with_viewport(|v| v.set_hidpi_scale(scale_factor as f32));
278            }
279
280            // Theme events
281            WindowEvent::ThemeChanged(theme) => {
282                let color_scheme = theme_to_color_scheme(self.theme_override.unwrap_or(theme));
283                self.doc.viewport_mut().color_scheme = color_scheme;
284            }
285
286            // Text / keyboard events
287            WindowEvent::Ime(ime_event) => {
288                self.doc.handle_ui_event(UiEvent::Ime(winit_ime_to_blitz(ime_event)));
289                self.request_redraw();
290            },
291            WindowEvent::ModifiersChanged(new_state) => {
292                // Store new keyboard modifier (ctrl, shift, etc) state for later use
293                self.keyboard_modifiers = new_state;
294            }
295            WindowEvent::KeyboardInput { event, .. } => {
296                let PhysicalKey::Code(key_code) = event.physical_key else {
297                    return;
298                };
299
300                if event.state.is_pressed() {
301                    let ctrl = self.keyboard_modifiers.state().control_key();
302                    let meta = self.keyboard_modifiers.state().super_key();
303                    let alt = self.keyboard_modifiers.state().alt_key();
304
305                    // Ctrl/Super keyboard shortcuts
306                    if ctrl | meta {
307                        match key_code {
308                            KeyCode::Equal => self.doc.viewport_mut().zoom_by(0.1),
309                            KeyCode::Minus => self.doc.viewport_mut().zoom_by(-0.1),
310                            KeyCode::Digit0 => self.doc.viewport_mut().set_zoom(1.0),
311                            _ => {}
312                        };
313                    }
314
315                    // Alt keyboard shortcuts
316                    if alt {
317                        match key_code {
318                            KeyCode::KeyD => {
319                                self.doc.devtools_mut().toggle_show_layout();
320                                self.request_redraw();
321                            }
322                            KeyCode::KeyH => {
323                                self.doc.devtools_mut().toggle_highlight_hover();
324                                self.request_redraw();
325                            }
326                            KeyCode::KeyT => self.doc.print_taffy_tree(),
327                            _ => {}
328                        };
329                    }
330
331                }
332
333                // Unmodified keypresses
334                let key_event_data = winit_key_event_to_blitz(&event, self.keyboard_modifiers.state());
335                let event = if event.state.is_pressed() {
336                    UiEvent::KeyDown(key_event_data)
337                } else {
338                    UiEvent::KeyUp(key_event_data)
339                };
340
341                self.doc.handle_ui_event(event);
342                self.request_redraw();
343            }
344
345
346            // Mouse/pointer events
347            WindowEvent::CursorEntered { /*device_id*/.. } => {}
348            WindowEvent::CursorLeft { /*device_id*/.. } => {}
349            WindowEvent::CursorMoved { position, .. } => {
350                let winit::dpi::LogicalPosition::<f32> { x, y } = position.to_logical(self.window.scale_factor());
351                self.mouse_pos = (x, y);
352                let event = UiEvent::MouseMove(BlitzMouseButtonEvent {
353                    x,
354                    y,
355                    button: Default::default(),
356                    buttons: self.buttons,
357                    mods: winit_modifiers_to_kbt_modifiers(self.keyboard_modifiers.state()),
358                });
359                self.doc.handle_ui_event(event);
360            }
361            WindowEvent::MouseInput { button, state, .. } => {
362                let button = match button {
363                    MouseButton::Left => MouseEventButton::Main,
364                    MouseButton::Right => MouseEventButton::Secondary,
365                    _ => return,
366                };
367
368                match state {
369                    ElementState::Pressed => self.buttons |= button.into(),
370                    ElementState::Released => self.buttons ^= button.into(),
371                }
372
373                let event = BlitzMouseButtonEvent {
374                    x: self.mouse_pos.0,
375                    y: self.mouse_pos.1,
376                    button,
377                    buttons: self.buttons,
378                    mods: winit_modifiers_to_kbt_modifiers(self.keyboard_modifiers.state()),
379                };
380
381                let event = match state {
382                    ElementState::Pressed => UiEvent::MouseDown(event),
383                    ElementState::Released => UiEvent::MouseUp(event),
384                };
385                self.doc.handle_ui_event(event);
386                self.request_redraw();
387            }
388            WindowEvent::MouseWheel { delta, .. } => {
389                let (scroll_x, scroll_y)= match delta {
390                    winit::event::MouseScrollDelta::LineDelta(x, y) => (x as f64 * 20.0, y as f64 * 20.0),
391                    winit::event::MouseScrollDelta::PixelDelta(offsets) => (offsets.x, offsets.y)
392                };
393
394                let has_changed = if let Some(hover_node_id) = self.doc.get_hover_node_id() {
395                    self.doc.scroll_node_by_has_changed(hover_node_id, scroll_x, scroll_y)
396                } else {
397                    self.doc.scroll_viewport_by_has_changed(scroll_x, scroll_y)
398                };
399
400                if has_changed {
401                    self.request_redraw();
402                }
403            }
404
405            // File events
406            WindowEvent::DroppedFile(_) => {}
407            WindowEvent::HoveredFile(_) => {}
408            WindowEvent::HoveredFileCancelled => {}
409            WindowEvent::Focused(_) => {}
410
411            // Touch and motion events
412            // Todo implement touch scrolling
413            WindowEvent::Touch(_) => {}
414            WindowEvent::TouchpadPressure { .. } => {}
415            WindowEvent::AxisMotion { .. } => {}
416            WindowEvent::PinchGesture { .. } => {},
417            WindowEvent::PanGesture { .. } => {},
418            WindowEvent::DoubleTapGesture { .. } => {},
419            WindowEvent::RotationGesture { .. } => {},
420        }
421    }
422}