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::marker::PhantomData;
15use std::sync::Arc;
16use std::task::Waker;
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(all(feature = "menu", not(any(target_os = "android", target_os = "ios"))))]
23use crate::menu::init_menu;
24
25#[cfg(feature = "accessibility")]
26use crate::accessibility::AccessibilityState;
27
28pub struct WindowConfig<Rend: WindowRenderer> {
29    doc: Box<dyn Document>,
30    attributes: WindowAttributes,
31    rend: PhantomData<Rend>,
32}
33
34impl<Rend: WindowRenderer> WindowConfig<Rend> {
35    pub fn new(doc: Box<dyn Document>) -> Self {
36        Self::with_attributes(doc, Window::default_attributes())
37    }
38
39    pub fn with_attributes(doc: Box<dyn Document>, attributes: WindowAttributes) -> Self {
40        WindowConfig {
41            doc,
42            attributes,
43            rend: PhantomData,
44        }
45    }
46}
47
48pub struct View<Rend: WindowRenderer> {
49    pub doc: Box<dyn Document>,
50
51    pub(crate) renderer: Rend,
52    pub(crate) waker: Option<Waker>,
53
54    event_loop_proxy: EventLoopProxy<BlitzShellEvent>,
55    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    theme_override: Option<Theme>,
60    keyboard_modifiers: Modifiers,
61    buttons: MouseEventButtons,
62    mouse_pos: (f32, f32),
63
64    #[cfg(feature = "accessibility")]
65    /// Accessibility adapter for `accesskit`.
66    accessibility: AccessibilityState,
67
68    /// Main menu bar of this view's window.
69    /// Field is _ prefixed because it is never read. But it needs to be stored here to prevent it from dropping.
70    #[cfg(all(feature = "menu", not(any(target_os = "android", target_os = "ios"))))]
71    _menu: muda::Menu,
72}
73
74impl<Rend: WindowRenderer> View<Rend> {
75    pub fn init(
76        config: WindowConfig<Rend>,
77        event_loop: &ActiveEventLoop,
78        proxy: &EventLoopProxy<BlitzShellEvent>,
79    ) -> Self {
80        let winit_window = Arc::from(event_loop.create_window(config.attributes).unwrap());
81
82        // TODO: make this conditional on text input focus
83        winit_window.set_ime_allowed(true);
84
85        // Create viewport
86        let size = winit_window.inner_size();
87        let scale = winit_window.scale_factor() as f32;
88        let theme = winit_window.theme().unwrap_or(Theme::Light);
89        let color_scheme = theme_to_color_scheme(theme);
90        let viewport = Viewport::new(size.width, size.height, scale, color_scheme);
91
92        // Create shell provider
93        let shell_provider = BlitzShellProvider::new(winit_window.clone());
94
95        let mut doc = config.doc;
96        doc.set_viewport(viewport);
97        doc.set_shell_provider(Arc::new(shell_provider));
98
99        Self {
100            renderer: Rend::new(winit_window.clone()),
101            waker: None,
102            keyboard_modifiers: Default::default(),
103            event_loop_proxy: proxy.clone(),
104            window: winit_window.clone(),
105            doc,
106            theme_override: None,
107            buttons: MouseEventButtons::None,
108            mouse_pos: Default::default(),
109            #[cfg(feature = "accessibility")]
110            accessibility: AccessibilityState::new(&winit_window, proxy.clone()),
111
112            #[cfg(all(feature = "menu", not(any(target_os = "android", target_os = "ios"))))]
113            _menu: init_menu(&winit_window),
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(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
213    pub fn window_id(&self) -> WindowId {
214        self.window.id()
215    }
216
217    #[inline]
218    pub fn with_viewport(&mut self, cb: impl FnOnce(&mut Viewport)) {
219        let mut viewport = self.doc.viewport_mut();
220        cb(&mut viewport);
221        drop(viewport);
222        let (width, height) = self.doc.viewport().window_size;
223        if width > 0 && height > 0 {
224            self.renderer.set_size(width, height);
225            self.request_redraw();
226        }
227    }
228
229    #[cfg(feature = "accessibility")]
230    pub fn build_accessibility_tree(&mut self) {
231        self.accessibility.build_tree(&self.doc);
232    }
233
234    pub fn handle_winit_event(&mut self, event: WindowEvent) {
235        match event {
236            // Window lifecycle events
237            WindowEvent::Destroyed => {}
238            WindowEvent::ActivationTokenDone { .. } => {},
239            WindowEvent::CloseRequested => {
240                // Currently handled at the level above in application.rs
241            }
242            WindowEvent::RedrawRequested => {
243                self.redraw();
244            }
245
246            // Window size/position events
247            WindowEvent::Moved(_) => {}
248            WindowEvent::Occluded(_) => {},
249            WindowEvent::Resized(physical_size) => {
250                self.with_viewport(|v| v.window_size = (physical_size.width, physical_size.height));
251            }
252            WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
253                self.with_viewport(|v| v.set_hidpi_scale(scale_factor as f32));
254            }
255
256            // Theme events
257            WindowEvent::ThemeChanged(theme) => {
258                let color_scheme = theme_to_color_scheme(self.theme_override.unwrap_or(theme));
259                self.doc.viewport_mut().color_scheme = color_scheme;
260            }
261
262            // Text / keyboard events
263            WindowEvent::Ime(ime_event) => {
264                self.doc.handle_event(UiEvent::Ime(winit_ime_to_blitz(ime_event)));
265                self.request_redraw();
266            },
267            WindowEvent::ModifiersChanged(new_state) => {
268                // Store new keyboard modifier (ctrl, shift, etc) state for later use
269                self.keyboard_modifiers = new_state;
270            }
271            WindowEvent::KeyboardInput { event, .. } => {
272                let PhysicalKey::Code(key_code) = event.physical_key else {
273                    return;
274                };
275
276                if event.state.is_pressed() {
277                    let ctrl = self.keyboard_modifiers.state().control_key();
278                    let meta = self.keyboard_modifiers.state().super_key();
279                    let alt = self.keyboard_modifiers.state().alt_key();
280
281                    // Ctrl/Super keyboard shortcuts
282                    if ctrl | meta {
283                        match key_code {
284                            KeyCode::Equal => self.doc.viewport_mut().zoom_by(0.1),
285                            KeyCode::Minus => self.doc.viewport_mut().zoom_by(-0.1),
286                            KeyCode::Digit0 => self.doc.viewport_mut().set_zoom(1.0),
287                            _ => {}
288                        };
289                    }
290
291                    // Alt keyboard shortcuts
292                    if alt {
293                        match key_code {
294                            KeyCode::KeyD => {
295                                self.doc.devtools_mut().toggle_show_layout();
296                                self.request_redraw();
297                            }
298                            KeyCode::KeyH => {
299                                self.doc.devtools_mut().toggle_highlight_hover();
300                                self.request_redraw();
301                            }
302                            KeyCode::KeyT => self.doc.print_taffy_tree(),
303                            _ => {}
304                        };
305                    }
306
307                }
308
309                // Unmodified keypresses
310                let key_event_data = winit_key_event_to_blitz(&event, self.keyboard_modifiers.state());
311                let event = if event.state.is_pressed() {
312                    UiEvent::KeyDown(key_event_data)
313                } else {
314                    UiEvent::KeyUp(key_event_data)
315                };
316
317                self.doc.handle_event(event);
318                self.request_redraw();
319            }
320
321
322            // Mouse/pointer events
323            WindowEvent::CursorEntered { /*device_id*/.. } => {}
324            WindowEvent::CursorLeft { /*device_id*/.. } => {}
325            WindowEvent::CursorMoved { position, .. } => {
326                let winit::dpi::LogicalPosition::<f32> { x, y } = position.to_logical(self.window.scale_factor());
327                self.mouse_pos = (x, y);
328                let event = UiEvent::MouseMove(BlitzMouseButtonEvent {
329                    x,
330                    y,
331                    button: Default::default(),
332                    buttons: self.buttons,
333                    mods: winit_modifiers_to_kbt_modifiers(self.keyboard_modifiers.state()),
334                });
335                self.doc.handle_event(event);
336                self.request_redraw();
337            }
338            WindowEvent::MouseInput { button, state, .. } => {
339                let button = match button {
340                    MouseButton::Left => MouseEventButton::Main,
341                    MouseButton::Right => MouseEventButton::Secondary,
342                    _ => return,
343                };
344
345                match state {
346                    ElementState::Pressed => self.buttons |= button.into(),
347                    ElementState::Released => self.buttons ^= button.into(),
348                }
349
350                let event = BlitzMouseButtonEvent {
351                    x: self.mouse_pos.0,
352                    y: self.mouse_pos.1,
353                    button,
354                    buttons: self.buttons,
355                    mods: winit_modifiers_to_kbt_modifiers(self.keyboard_modifiers.state()),
356                };
357
358                let event = match state {
359                    ElementState::Pressed => UiEvent::MouseDown(event),
360                    ElementState::Released => UiEvent::MouseUp(event),
361                };
362                self.doc.handle_event(event);
363                self.request_redraw();
364            }
365            WindowEvent::MouseWheel { delta, .. } => {
366                let (scroll_x, scroll_y)= match delta {
367                    winit::event::MouseScrollDelta::LineDelta(x, y) => (x as f64 * 20.0, y as f64 * 20.0),
368                    winit::event::MouseScrollDelta::PixelDelta(offsets) => (offsets.x, offsets.y)
369                };
370
371                if let Some(hover_node_id) = self.doc.get_hover_node_id() {
372                    self.doc.scroll_node_by(hover_node_id, scroll_x, scroll_y);
373                } else {
374                    self.doc.scroll_viewport_by(scroll_x, scroll_y);
375                }
376                self.request_redraw();
377            }
378
379            // File events
380            WindowEvent::DroppedFile(_) => {}
381            WindowEvent::HoveredFile(_) => {}
382            WindowEvent::HoveredFileCancelled => {}
383            WindowEvent::Focused(_) => {}
384
385            // Touch and motion events
386            // Todo implement touch scrolling
387            WindowEvent::Touch(_) => {}
388            WindowEvent::TouchpadPressure { .. } => {}
389            WindowEvent::AxisMotion { .. } => {}
390            WindowEvent::PinchGesture { .. } => {},
391            WindowEvent::PanGesture { .. } => {},
392            WindowEvent::DoubleTapGesture { .. } => {},
393            WindowEvent::RotationGesture { .. } => {},
394        }
395    }
396}