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