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