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