blitz_shell/
window.rs

1use crate::convert_events::{
2    winit_ime_to_blitz, winit_key_event_to_blitz, winit_modifiers_to_kbt_modifiers,
3};
4use crate::event::{BlitzShellEvent, create_waker};
5use blitz_dom::BaseDocument;
6use blitz_traits::{
7    BlitzMouseButtonEvent, ColorScheme, Devtools, MouseEventButton, MouseEventButtons, Viewport,
8};
9use blitz_traits::{Document, DocumentRenderer, DomEvent, DomEventData};
10use winit::keyboard::PhysicalKey;
11
12use std::marker::PhantomData;
13use std::sync::Arc;
14use std::task::Waker;
15use winit::event::{ElementState, MouseButton};
16use winit::event_loop::{ActiveEventLoop, EventLoopProxy};
17use winit::window::{Theme, WindowAttributes, WindowId};
18use winit::{event::Modifiers, event::WindowEvent, keyboard::KeyCode, window::Window};
19
20#[cfg(all(feature = "menu", not(any(target_os = "android", target_os = "ios"))))]
21use crate::menu::init_menu;
22
23#[cfg(feature = "accessibility")]
24use crate::accessibility::AccessibilityState;
25
26// TODO: make generic
27type D = BaseDocument;
28
29pub struct WindowConfig<
30    Doc: Document<Doc = BaseDocument>,
31    Rend: DocumentRenderer<Doc = BaseDocument>,
32> {
33    doc: Doc,
34    attributes: WindowAttributes,
35    rend: PhantomData<Rend>,
36}
37
38impl<Doc: Document<Doc = D>, Rend: DocumentRenderer<Doc = D>> WindowConfig<Doc, Rend> {
39    pub fn new(doc: Doc) -> Self {
40        Self::with_attributes(doc, Window::default_attributes())
41    }
42
43    pub fn with_attributes(doc: Doc, attributes: WindowAttributes) -> Self {
44        WindowConfig {
45            doc,
46            attributes,
47            rend: PhantomData,
48        }
49    }
50}
51
52pub struct View<Doc: Document<Doc = D>, Rend: DocumentRenderer<Doc = D>> {
53    pub doc: Doc,
54
55    pub(crate) renderer: Rend,
56    pub(crate) waker: Option<Waker>,
57
58    event_loop_proxy: EventLoopProxy<BlitzShellEvent>,
59    window: Arc<Window>,
60
61    /// The actual viewport of the page that we're getting a glimpse of.
62    /// We need this since the part of the page that's being viewed might not be the page in its entirety.
63    /// This will let us opt of rendering some stuff
64    viewport: Viewport,
65
66    /// The state of the keyboard modifiers (ctrl, shift, etc). Winit/Tao don't track these for us so we
67    /// need to store them in order to have access to them when processing keypress events
68    pub devtools: Devtools,
69    theme_override: Option<Theme>,
70    keyboard_modifiers: Modifiers,
71    buttons: MouseEventButtons,
72    mouse_pos: (f32, f32),
73    dom_mouse_pos: (f32, f32),
74    mouse_down_node: Option<usize>,
75
76    #[cfg(feature = "accessibility")]
77    /// Accessibility adapter for `accesskit`.
78    accessibility: AccessibilityState,
79
80    /// Main menu bar of this view's window.
81    /// Field is _ prefixed because it is never read. But it needs to be stored here to prevent it from dropping.
82    #[cfg(all(feature = "menu", not(any(target_os = "android", target_os = "ios"))))]
83    _menu: muda::Menu,
84}
85
86impl<Doc: Document<Doc = D>, Rend: DocumentRenderer<Doc = D>> View<Doc, Rend> {
87    pub(crate) fn init(
88        config: WindowConfig<Doc, Rend>,
89        event_loop: &ActiveEventLoop,
90        proxy: &EventLoopProxy<BlitzShellEvent>,
91    ) -> Self {
92        let winit_window = Arc::from(event_loop.create_window(config.attributes).unwrap());
93
94        // TODO: make this conditional on text input focus
95        winit_window.set_ime_allowed(true);
96
97        // Create viewport
98        let size = winit_window.inner_size();
99        let scale = winit_window.scale_factor() as f32;
100        let theme = winit_window.theme().unwrap_or(Theme::Light);
101        let color_scheme = theme_to_color_scheme(theme);
102        let viewport = Viewport::new(size.width, size.height, scale, color_scheme);
103
104        Self {
105            renderer: Rend::new(winit_window.clone()),
106            waker: None,
107            keyboard_modifiers: Default::default(),
108
109            event_loop_proxy: proxy.clone(),
110            window: winit_window.clone(),
111            doc: config.doc,
112            viewport,
113            devtools: Default::default(),
114            theme_override: None,
115            buttons: MouseEventButtons::None,
116            mouse_pos: Default::default(),
117            dom_mouse_pos: Default::default(),
118            mouse_down_node: None,
119
120            #[cfg(feature = "accessibility")]
121            accessibility: AccessibilityState::new(&winit_window, proxy.clone()),
122
123            #[cfg(all(feature = "menu", not(any(target_os = "android", target_os = "ios"))))]
124            _menu: init_menu(&winit_window),
125        }
126    }
127
128    pub fn replace_document(&mut self, new_doc: Doc, retain_scroll_position: bool) {
129        let scroll = self.doc.as_ref().viewport_scroll();
130
131        self.doc = new_doc;
132        self.kick_viewport();
133        self.poll();
134        self.request_redraw();
135
136        if retain_scroll_position {
137            self.doc.as_mut().set_viewport_scroll(scroll);
138        }
139    }
140
141    pub fn theme_override(&self) -> Option<Theme> {
142        self.theme_override
143    }
144
145    pub fn current_theme(&self) -> Theme {
146        color_scheme_to_theme(self.viewport.color_scheme)
147    }
148
149    pub fn set_theme_override(&mut self, theme: Option<Theme>) {
150        self.theme_override = theme;
151        let theme = theme.or(self.window.theme()).unwrap_or(Theme::Light);
152        self.viewport.color_scheme = theme_to_color_scheme(theme);
153        self.kick_viewport();
154        self.request_redraw();
155    }
156}
157
158impl<Doc: Document<Doc = D>, Rend: DocumentRenderer<Doc = D>> View<Doc, Rend> {
159    pub fn resume(&mut self) {
160        // Resolve dom
161        self.doc.as_mut().set_viewport(self.viewport.clone());
162        self.doc.as_mut().resolve();
163
164        // Resume renderer
165        self.renderer.resume(&self.viewport);
166        if !self.renderer.is_active() {
167            panic!("Renderer failed to resume");
168        };
169
170        // Render
171        let (width, height) = self.viewport.window_size;
172        self.renderer.render(
173            self.doc.as_ref(),
174            self.viewport.scale_f64(),
175            width,
176            height,
177            self.devtools,
178        );
179
180        // Set waker
181        self.waker = Some(create_waker(&self.event_loop_proxy, self.window_id()));
182    }
183
184    pub fn suspend(&mut self) {
185        self.waker = None;
186        self.renderer.suspend();
187    }
188
189    pub fn poll(&mut self) -> bool {
190        if let Some(waker) = &self.waker {
191            let cx = std::task::Context::from_waker(waker);
192            if self.doc.poll(cx) {
193                #[cfg(feature = "accessibility")]
194                {
195                    // TODO send fine grained accessibility tree updates.
196                    let changed = std::mem::take(&mut self.doc.as_mut().changed);
197                    if !changed.is_empty() {
198                        self.accessibility.build_tree(self.doc.as_ref());
199                    }
200                }
201
202                self.request_redraw();
203                return true;
204            }
205        }
206
207        false
208    }
209
210    pub fn request_redraw(&self) {
211        if self.renderer.is_active() {
212            self.window.request_redraw();
213        }
214    }
215
216    pub fn redraw(&mut self) {
217        self.doc.as_mut().resolve();
218        let (width, height) = self.viewport.window_size;
219        self.renderer.render(
220            self.doc.as_ref(),
221            self.viewport.scale_f64(),
222            width,
223            height,
224            self.devtools,
225        );
226    }
227
228    pub fn window_id(&self) -> WindowId {
229        self.window.id()
230    }
231
232    pub fn kick_viewport(&mut self) {
233        self.kick_dom_viewport();
234        self.doc.as_mut().scroll_viewport_by(0.0, 0.0); // Clamp scroll offset
235        self.kick_renderer_viewport();
236    }
237
238    pub fn kick_dom_viewport(&mut self) {
239        let (width, height) = self.viewport.window_size;
240        if width > 0 && height > 0 {
241            self.doc.as_mut().set_viewport(self.viewport.clone());
242            self.request_redraw();
243        }
244    }
245
246    pub fn kick_renderer_viewport(&mut self) {
247        let (width, height) = self.viewport.window_size;
248        if width > 0 && height > 0 {
249            self.renderer.set_size(width, height);
250            self.request_redraw();
251        }
252    }
253
254    pub fn mouse_move(&mut self, x: f32, y: f32) -> bool {
255        let viewport_scroll = self.doc.as_ref().viewport_scroll();
256        let dom_x = x + viewport_scroll.x as f32 / self.viewport.zoom();
257        let dom_y = y + viewport_scroll.y as f32 / self.viewport.zoom();
258
259        // println!("Mouse move: ({}, {})", x, y);
260        // println!("Unscaled: ({}, {})",);
261
262        self.mouse_pos = (x, y);
263        self.dom_mouse_pos = (dom_x, dom_y);
264        let mut changed = self.doc.as_mut().set_hover_to(dom_x, dom_y);
265
266        if let Some(node_id) = self.doc.as_ref().get_hover_node_id() {
267            let mut event = DomEvent::new(
268                node_id,
269                DomEventData::MouseMove(BlitzMouseButtonEvent {
270                    x: self.dom_mouse_pos.0,
271                    y: self.dom_mouse_pos.1,
272                    button: Default::default(),
273                    buttons: self.buttons,
274                    mods: winit_modifiers_to_kbt_modifiers(self.keyboard_modifiers.state()),
275                }),
276            );
277            self.doc.handle_event(&mut event);
278            if event.request_redraw {
279                changed = true;
280            }
281        }
282
283        changed
284    }
285
286    pub fn mouse_down(&mut self, button: MouseEventButton) {
287        let Some(node_id) = self.doc.as_ref().get_hover_node_id() else {
288            return;
289        };
290
291        self.doc.as_mut().active_node();
292        self.buttons |= button.into();
293
294        self.doc.handle_event(&mut DomEvent::new(
295            node_id,
296            DomEventData::MouseDown(BlitzMouseButtonEvent {
297                x: self.dom_mouse_pos.0,
298                y: self.dom_mouse_pos.1,
299                button,
300                buttons: self.buttons,
301                mods: winit_modifiers_to_kbt_modifiers(self.keyboard_modifiers.state()),
302            }),
303        ));
304
305        self.mouse_down_node = Some(node_id);
306    }
307
308    pub fn mouse_up(&mut self, button: MouseEventButton) {
309        self.doc.as_mut().unactive_node();
310
311        let Some(node_id) = self.doc.as_ref().get_hover_node_id() else {
312            return;
313        };
314
315        self.buttons ^= button.into();
316
317        self.doc.handle_event(&mut DomEvent::new(
318            node_id,
319            DomEventData::MouseUp(BlitzMouseButtonEvent {
320                x: self.dom_mouse_pos.0,
321                y: self.dom_mouse_pos.1,
322                button,
323                buttons: self.buttons,
324                mods: winit_modifiers_to_kbt_modifiers(self.keyboard_modifiers.state()),
325            }),
326        ));
327
328        if self.mouse_down_node == Some(node_id) {
329            self.click(button);
330        } else if let Some(mouse_down_id) = self.mouse_down_node {
331            // Anonymous node ids are unstable due to tree reconstruction. So we compare the id
332            // of the first non-anonymous ancestor.
333            if self.doc.as_ref().non_anon_ancestor_if_anon(mouse_down_id)
334                == self.doc.as_ref().non_anon_ancestor_if_anon(node_id)
335            {
336                self.click(button);
337            }
338        }
339    }
340
341    pub fn click(&mut self, button: MouseEventButton) {
342        let Some(node_id) = self.doc.as_ref().get_hover_node_id() else {
343            return;
344        };
345
346        if self.devtools.highlight_hover {
347            let mut node = self.doc.as_ref().get_node(node_id).unwrap();
348            if button == MouseEventButton::Secondary {
349                if let Some(parent_id) = node.layout_parent.get() {
350                    node = self.doc.as_ref().get_node(parent_id).unwrap();
351                }
352            }
353            self.doc.as_ref().debug_log_node(node.id);
354            self.devtools.highlight_hover = false;
355        } else {
356            // Not debug mode. Handle click as usual
357            if button == MouseEventButton::Main {
358                // If we hit a node, then we collect the node to its parents, check for listeners, and then
359                // call those listeners
360                self.doc.handle_event(&mut DomEvent::new(
361                    node_id,
362                    DomEventData::Click(BlitzMouseButtonEvent {
363                        x: self.dom_mouse_pos.0,
364                        y: self.dom_mouse_pos.1,
365                        button,
366                        buttons: self.buttons,
367                        mods: winit_modifiers_to_kbt_modifiers(self.keyboard_modifiers.state()),
368                    }),
369                ));
370            }
371        }
372    }
373
374    #[cfg(feature = "accessibility")]
375    pub fn build_accessibility_tree(&mut self) {
376        self.accessibility.build_tree(self.doc.as_ref());
377    }
378
379    pub fn handle_winit_event(&mut self, event: WindowEvent) {
380        match event {
381            // Window lifecycle events
382            WindowEvent::Destroyed => {}
383            WindowEvent::ActivationTokenDone { .. } => {},
384            WindowEvent::CloseRequested => {
385                // Currently handled at the level above in application.rs
386            }
387            WindowEvent::RedrawRequested => {
388                self.redraw();
389            }
390
391            // Window size/position events
392            WindowEvent::Moved(_) => {}
393            WindowEvent::Occluded(_) => {},
394            WindowEvent::Resized(physical_size) => {
395                self.viewport.window_size = (physical_size.width, physical_size.height);
396                self.kick_viewport();
397            }
398            WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
399                self.viewport.set_hidpi_scale(scale_factor as f32);
400                self.kick_viewport();
401            }
402
403            // Theme events
404            WindowEvent::ThemeChanged(theme) => {
405                self.viewport.color_scheme = theme_to_color_scheme(self.theme_override.unwrap_or(theme));
406                self.kick_viewport();
407            }
408
409            // Text / keyboard events
410            WindowEvent::Ime(ime_event) => {
411                if let Some(target) = self.doc.as_ref().get_focussed_node_id() {
412                    self.doc.handle_event(&mut DomEvent::new(target, DomEventData::Ime(winit_ime_to_blitz(ime_event))));
413                    self.request_redraw();
414                }
415            },
416            WindowEvent::ModifiersChanged(new_state) => {
417                // Store new keyboard modifier (ctrl, shift, etc) state for later use
418                self.keyboard_modifiers = new_state;
419            }
420            WindowEvent::KeyboardInput { event, .. } => {
421                let PhysicalKey::Code(key_code) = event.physical_key else {
422                    return;
423                };
424                if !event.state.is_pressed() {
425                    return;
426                }
427
428                let ctrl = self.keyboard_modifiers.state().control_key();
429                let meta = self.keyboard_modifiers.state().super_key();
430                let alt = self.keyboard_modifiers.state().alt_key();
431
432                // Ctrl/Super keyboard shortcuts
433                if ctrl | meta {
434                    match key_code {
435                        KeyCode::Equal => {
436                            *self.viewport.zoom_mut() += 0.1;
437                            self.kick_dom_viewport();
438                        }
439                        KeyCode::Minus => {
440                            *self.viewport.zoom_mut() -= 0.1;
441                            self.kick_dom_viewport();
442                        }
443                        KeyCode::Digit0 => {
444                            *self.viewport.zoom_mut() = 1.0;
445                            self.kick_dom_viewport();
446                        }
447                        _ => {}
448                    };
449                }
450
451                // Alt keyboard shortcuts
452                if alt {
453                    match key_code {
454                        KeyCode::KeyD => {
455                            self.devtools.show_layout = !self.devtools.show_layout;
456                            self.request_redraw();
457                        }
458                        KeyCode::KeyH => {
459                            self.devtools.highlight_hover = !self.devtools.highlight_hover;
460                            self.request_redraw();
461                        }
462                        KeyCode::KeyT => {
463                            self.doc.as_ref().print_taffy_tree();
464                        }
465                        _ => {}
466                    };
467                }
468
469                // Unmodified keypresses
470                match key_code {
471                    KeyCode::Tab if event.state.is_pressed() => {
472                        self.doc.as_mut().focus_next_node();
473                        self.request_redraw();
474                    }
475                    _ => {
476                        if let Some(focus_node_id) = self.doc.as_ref().get_focussed_node_id() {
477                            self.doc.handle_event(&mut DomEvent::new(
478                                focus_node_id,
479                                DomEventData::KeyPress(winit_key_event_to_blitz(&event, self.keyboard_modifiers.state()))
480                            ));
481                            self.request_redraw();
482                        }
483                    }
484                }
485            }
486
487
488            // Mouse/pointer events
489            WindowEvent::CursorEntered { /*device_id*/.. } => {}
490            WindowEvent::CursorLeft { /*device_id*/.. } => {}
491            WindowEvent::CursorMoved { position, .. } => {
492                let winit::dpi::LogicalPosition::<f32> { x, y } = position.to_logical(self.window.scale_factor());
493                let changed = self.mouse_move(x, y);
494
495                if changed {
496                    let cursor = self.doc.as_ref().get_cursor();
497                    if let Some(cursor) = cursor {
498                            self.window.set_cursor(cursor);
499                            self.request_redraw();
500                    }
501                }
502            }
503            WindowEvent::MouseInput { button, state, .. } => {
504                if matches!(button, MouseButton::Left | MouseButton::Right) {
505                    let button = match button {
506                        MouseButton::Left => MouseEventButton::Main,
507                        MouseButton::Right => MouseEventButton::Secondary,
508                        _ => unreachable!(),
509                    };
510
511                    match state {
512                        ElementState::Pressed => self.mouse_down(button),
513                        ElementState::Released => self.mouse_up(button)
514                    }
515
516                    self.request_redraw();
517                }
518            }
519            WindowEvent::MouseWheel { delta, .. } => {
520                let (scroll_x, scroll_y)= match delta {
521                    winit::event::MouseScrollDelta::LineDelta(x, y) => (x as f64 * 20.0, y as f64 * 20.0),
522                    winit::event::MouseScrollDelta::PixelDelta(offsets) => (offsets.x, offsets.y)
523                };
524
525                if let Some(hover_node_id)= self.doc.as_ref().get_hover_node_id() {
526                    self.doc.as_mut().scroll_node_by(hover_node_id, scroll_x, scroll_y);
527                } else {
528                    self.doc.as_mut().scroll_viewport_by(scroll_x, scroll_y);
529                }
530                self.request_redraw();
531            }
532
533            // File events
534            WindowEvent::DroppedFile(_) => {}
535            WindowEvent::HoveredFile(_) => {}
536            WindowEvent::HoveredFileCancelled => {}
537            WindowEvent::Focused(_) => {}
538
539            // Touch and motion events
540            // Todo implement touch scrolling
541            WindowEvent::Touch(_) => {}
542            WindowEvent::TouchpadPressure { .. } => {}
543            WindowEvent::AxisMotion { .. } => {}
544            WindowEvent::PinchGesture { .. } => {},
545            WindowEvent::PanGesture { .. } => {},
546            WindowEvent::DoubleTapGesture { .. } => {},
547            WindowEvent::RotationGesture { .. } => {},
548        }
549    }
550}
551
552fn theme_to_color_scheme(theme: Theme) -> ColorScheme {
553    match theme {
554        Theme::Light => ColorScheme::Light,
555        Theme::Dark => ColorScheme::Dark,
556    }
557}
558
559fn color_scheme_to_theme(scheme: ColorScheme) -> Theme {
560    match scheme {
561        ColorScheme::Light => Theme::Light,
562        ColorScheme::Dark => Theme::Dark,
563    }
564}