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::UiEvent;
11use blitz_traits::{BlitzMouseButtonEvent, MouseEventButton, MouseEventButtons, 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(cx) {
182                #[cfg(feature = "accessibility")]
183                {
184                    // TODO send fine grained accessibility tree updates.
185                    let changed = std::mem::take(&mut self.doc.changed);
186                    if !changed.is_empty() {
187                        self.accessibility.build_tree(&self.doc);
188                    }
189                }
190
191                self.request_redraw();
192                return true;
193            }
194        }
195
196        false
197    }
198
199    pub fn request_redraw(&self) {
200        if self.renderer.is_active() {
201            self.window.request_redraw();
202        }
203    }
204
205    pub fn redraw(&mut self) {
206        self.doc.resolve();
207        let (width, height) = self.doc.viewport().window_size;
208        let scale = self.doc.viewport().scale_f64();
209        self.renderer
210            .render(|scene| paint_scene(scene, &self.doc, scale, width, height));
211
212        if self.doc.is_animating() {
213            self.request_redraw();
214        }
215    }
216
217    pub fn window_id(&self) -> WindowId {
218        self.window.id()
219    }
220
221    #[inline]
222    pub fn with_viewport(&mut self, cb: impl FnOnce(&mut Viewport)) {
223        let mut viewport = self.doc.viewport_mut();
224        cb(&mut viewport);
225        drop(viewport);
226        let (width, height) = self.doc.viewport().window_size;
227        if width > 0 && height > 0 {
228            self.renderer.set_size(width, height);
229            self.request_redraw();
230        }
231    }
232
233    #[cfg(feature = "accessibility")]
234    pub fn build_accessibility_tree(&mut self) {
235        self.accessibility.build_tree(&self.doc);
236    }
237
238    pub fn handle_winit_event(&mut self, event: WindowEvent) {
239        match event {
240            // Window lifecycle events
241            WindowEvent::Destroyed => {}
242            WindowEvent::ActivationTokenDone { .. } => {},
243            WindowEvent::CloseRequested => {
244                // Currently handled at the level above in application.rs
245            }
246            WindowEvent::RedrawRequested => {
247                self.redraw();
248            }
249
250            // Window size/position events
251            WindowEvent::Moved(_) => {}
252            WindowEvent::Occluded(_) => {},
253            WindowEvent::Resized(physical_size) => {
254                self.with_viewport(|v| v.window_size = (physical_size.width, physical_size.height));
255            }
256            WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
257                self.with_viewport(|v| v.set_hidpi_scale(scale_factor as f32));
258            }
259
260            // Theme events
261            WindowEvent::ThemeChanged(theme) => {
262                let color_scheme = theme_to_color_scheme(self.theme_override.unwrap_or(theme));
263                self.doc.viewport_mut().color_scheme = color_scheme;
264            }
265
266            // Text / keyboard events
267            WindowEvent::Ime(ime_event) => {
268                self.doc.handle_event(UiEvent::Ime(winit_ime_to_blitz(ime_event)));
269                self.request_redraw();
270            },
271            WindowEvent::ModifiersChanged(new_state) => {
272                // Store new keyboard modifier (ctrl, shift, etc) state for later use
273                self.keyboard_modifiers = new_state;
274            }
275            WindowEvent::KeyboardInput { event, .. } => {
276                let PhysicalKey::Code(key_code) = event.physical_key else {
277                    return;
278                };
279
280                if event.state.is_pressed() {
281                    let ctrl = self.keyboard_modifiers.state().control_key();
282                    let meta = self.keyboard_modifiers.state().super_key();
283                    let alt = self.keyboard_modifiers.state().alt_key();
284
285                    // Ctrl/Super keyboard shortcuts
286                    if ctrl | meta {
287                        match key_code {
288                            KeyCode::Equal => self.doc.viewport_mut().zoom_by(0.1),
289                            KeyCode::Minus => self.doc.viewport_mut().zoom_by(-0.1),
290                            KeyCode::Digit0 => self.doc.viewport_mut().set_zoom(1.0),
291                            _ => {}
292                        };
293                    }
294
295                    // Alt keyboard shortcuts
296                    if alt {
297                        match key_code {
298                            KeyCode::KeyD => {
299                                self.doc.devtools_mut().toggle_show_layout();
300                                self.request_redraw();
301                            }
302                            KeyCode::KeyH => {
303                                self.doc.devtools_mut().toggle_highlight_hover();
304                                self.request_redraw();
305                            }
306                            KeyCode::KeyT => self.doc.print_taffy_tree(),
307                            _ => {}
308                        };
309                    }
310
311                }
312
313                // Unmodified keypresses
314                let key_event_data = winit_key_event_to_blitz(&event, self.keyboard_modifiers.state());
315                let event = if event.state.is_pressed() {
316                    UiEvent::KeyDown(key_event_data)
317                } else {
318                    UiEvent::KeyUp(key_event_data)
319                };
320
321                self.doc.handle_event(event);
322                self.request_redraw();
323            }
324
325
326            // Mouse/pointer events
327            WindowEvent::CursorEntered { /*device_id*/.. } => {}
328            WindowEvent::CursorLeft { /*device_id*/.. } => {}
329            WindowEvent::CursorMoved { position, .. } => {
330                let winit::dpi::LogicalPosition::<f32> { x, y } = position.to_logical(self.window.scale_factor());
331                self.mouse_pos = (x, y);
332                let event = UiEvent::MouseMove(BlitzMouseButtonEvent {
333                    x,
334                    y,
335                    button: Default::default(),
336                    buttons: self.buttons,
337                    mods: winit_modifiers_to_kbt_modifiers(self.keyboard_modifiers.state()),
338                });
339                self.doc.handle_event(event);
340                self.request_redraw();
341            }
342            WindowEvent::MouseInput { button, state, .. } => {
343                let button = match button {
344                    MouseButton::Left => MouseEventButton::Main,
345                    MouseButton::Right => MouseEventButton::Secondary,
346                    _ => return,
347                };
348
349                match state {
350                    ElementState::Pressed => self.buttons |= button.into(),
351                    ElementState::Released => self.buttons ^= button.into(),
352                }
353
354                let event = BlitzMouseButtonEvent {
355                    x: self.mouse_pos.0,
356                    y: self.mouse_pos.1,
357                    button,
358                    buttons: self.buttons,
359                    mods: winit_modifiers_to_kbt_modifiers(self.keyboard_modifiers.state()),
360                };
361
362                let event = match state {
363                    ElementState::Pressed => UiEvent::MouseDown(event),
364                    ElementState::Released => UiEvent::MouseUp(event),
365                };
366                self.doc.handle_event(event);
367                self.request_redraw();
368            }
369            WindowEvent::MouseWheel { delta, .. } => {
370                let (scroll_x, scroll_y)= match delta {
371                    winit::event::MouseScrollDelta::LineDelta(x, y) => (x as f64 * 20.0, y as f64 * 20.0),
372                    winit::event::MouseScrollDelta::PixelDelta(offsets) => (offsets.x, offsets.y)
373                };
374
375                if let Some(hover_node_id) = self.doc.get_hover_node_id() {
376                    self.doc.scroll_node_by(hover_node_id, scroll_x, scroll_y);
377                } else {
378                    self.doc.scroll_viewport_by(scroll_x, scroll_y);
379                }
380                self.request_redraw();
381            }
382
383            // File events
384            WindowEvent::DroppedFile(_) => {}
385            WindowEvent::HoveredFile(_) => {}
386            WindowEvent::HoveredFileCancelled => {}
387            WindowEvent::Focused(_) => {}
388
389            // Touch and motion events
390            // Todo implement touch scrolling
391            WindowEvent::Touch(_) => {}
392            WindowEvent::TouchpadPressure { .. } => {}
393            WindowEvent::AxisMotion { .. } => {}
394            WindowEvent::PinchGesture { .. } => {},
395            WindowEvent::PanGesture { .. } => {},
396            WindowEvent::DoubleTapGesture { .. } => {},
397            WindowEvent::RotationGesture { .. } => {},
398        }
399    }
400}