1use crate::BlitzShellProvider;
2use crate::convert_events::{
3 button_source_to_blitz, color_scheme_to_theme, pointer_source_to_blitz,
4 pointer_source_to_blitz_details, theme_to_color_scheme, winit_ime_to_blitz,
5 winit_key_event_to_blitz, winit_modifiers_to_kbt_modifiers,
6};
7use crate::event::{BlitzShellProxy, create_waker};
8use anyrender::WindowRenderer;
9use blitz_dom::Document;
10use blitz_paint::paint_scene;
11use blitz_traits::events::{
12 BlitzPointerEvent, BlitzPointerId, BlitzWheelDelta, BlitzWheelEvent, MouseEventButton,
13 MouseEventButtons, PointerCoords, PointerDetails, UiEvent,
14};
15use blitz_traits::shell::Viewport;
16use winit::dpi::{LogicalPosition, PhysicalInsets, PhysicalPosition};
17use winit::keyboard::PhysicalKey;
18
19use std::any::Any;
20use std::sync::Arc;
21use std::task::Waker;
22use std::time::Instant;
23use winit::event::{ButtonSource, ElementState, MouseButton};
24use winit::event_loop::ActiveEventLoop;
25use winit::window::{Theme, WindowAttributes, WindowId};
26use winit::{event::Modifiers, event::WindowEvent, keyboard::KeyCode, window::Window};
27
28#[cfg(feature = "accessibility")]
29use crate::accessibility::AccessibilityState;
30
31pub struct WindowConfig<Rend: WindowRenderer> {
32 doc: Box<dyn Document>,
33 attributes: WindowAttributes,
34 renderer: Rend,
35}
36
37impl<Rend: WindowRenderer> WindowConfig<Rend> {
38 pub fn new(doc: Box<dyn Document>, renderer: Rend) -> Self {
39 Self::with_attributes(doc, renderer, WindowAttributes::default())
40 }
41
42 pub fn with_attributes(
43 doc: Box<dyn Document>,
44 renderer: Rend,
45 attributes: WindowAttributes,
46 ) -> Self {
47 WindowConfig {
48 doc,
49 attributes,
50 renderer,
51 }
52 }
53}
54
55pub struct View<Rend: WindowRenderer> {
56 pub doc: Box<dyn Document>,
57
58 pub renderer: Rend,
59 pub waker: Option<Waker>,
60
61 pub proxy: BlitzShellProxy,
62 pub window: Arc<dyn Window>,
63
64 pub theme_override: Option<Theme>,
67 pub keyboard_modifiers: Modifiers,
68 pub buttons: MouseEventButtons,
69 pub pointer_pos: PhysicalPosition<f64>,
70 pub animation_timer: Option<Instant>,
71 pub is_visible: bool,
72 pub safe_area_insets: PhysicalInsets<u32>,
73
74 #[cfg(feature = "accessibility")]
75 pub accessibility: AccessibilityState,
77
78 #[cfg(target_os = "ios")]
83 pub ios_request_redraw: std::cell::Cell<bool>,
84}
85
86impl<Rend: WindowRenderer> View<Rend> {
87 pub fn init(
88 config: WindowConfig<Rend>,
89 event_loop: &dyn ActiveEventLoop,
90 proxy: &BlitzShellProxy,
91 ) -> Self {
92 let is_visible = config.attributes.visible;
95 let attrs = config.attributes.with_visible(false);
96
97 let winit_window: Arc<dyn Window> = Arc::from(event_loop.create_window(attrs).unwrap());
98 #[cfg(feature = "accessibility")]
99 let accessibility = AccessibilityState::new(&*winit_window, proxy.clone());
100
101 if is_visible {
102 winit_window.set_visible(true);
103 }
104
105 let size = winit_window.surface_size();
108 let scale = winit_window.scale_factor() as f32;
109 let safe_area_insets = winit_window.safe_area();
110 let theme = winit_window.theme().unwrap_or(Theme::Light);
111 let color_scheme = theme_to_color_scheme(theme);
112 let viewport = Viewport::new(size.width, size.height, scale, color_scheme);
113
114 let shell_provider = BlitzShellProvider::new(winit_window.clone());
116
117 let mut doc = config.doc;
118 let mut inner = doc.inner_mut();
119 inner.set_viewport(viewport);
120 inner.set_shell_provider(Arc::new(shell_provider));
121
122 let title = inner.find_title_node().map(|node| node.text_content());
126 if let Some(title) = title {
127 winit_window.set_title(&title);
128 }
129
130 drop(inner);
131
132 Self {
133 renderer: config.renderer,
134 waker: None,
135 animation_timer: None,
136 keyboard_modifiers: Default::default(),
137 proxy: proxy.clone(),
138 window: winit_window.clone(),
139 doc,
140 theme_override: None,
141 buttons: MouseEventButtons::None,
142 safe_area_insets,
143 pointer_pos: Default::default(),
144 is_visible: winit_window.is_visible().unwrap_or(true),
145 #[cfg(feature = "accessibility")]
146 accessibility,
147
148 #[cfg(target_os = "ios")]
149 ios_request_redraw: std::cell::Cell::new(false),
150 }
151 }
152
153 pub fn replace_document(&mut self, new_doc: Box<dyn Document>, retain_scroll_position: bool) {
154 let inner = self.doc.inner();
155 let scroll = inner.viewport_scroll();
156 let viewport = inner.viewport().clone();
157 let shell_provider = inner.shell_provider.clone();
158 drop(inner);
159
160 self.doc = new_doc;
161
162 let mut inner = self.doc.inner_mut();
163 inner.set_viewport(viewport);
164 inner.set_shell_provider(shell_provider);
165 drop(inner);
166
167 self.poll();
168 self.request_redraw();
169
170 if retain_scroll_position {
171 self.doc.inner_mut().set_viewport_scroll(scroll);
172 }
173 }
174
175 pub fn theme_override(&self) -> Option<Theme> {
176 self.theme_override
177 }
178
179 pub fn current_theme(&self) -> Theme {
180 color_scheme_to_theme(self.doc.inner().viewport().color_scheme)
181 }
182
183 pub fn set_theme_override(&mut self, theme: Option<Theme>) {
184 self.theme_override = theme;
185 let theme = theme.or(self.window.theme()).unwrap_or(Theme::Light);
186 self.with_viewport(|v| v.color_scheme = theme_to_color_scheme(theme));
187 }
188
189 pub fn downcast_doc_mut<T: 'static>(&mut self) -> &mut T {
190 (&mut *self.doc as &mut dyn Any)
191 .downcast_mut::<T>()
192 .unwrap()
193 }
194
195 pub fn current_animation_time(&mut self) -> f64 {
196 match &self.animation_timer {
197 Some(start) => Instant::now().duration_since(*start).as_secs_f64(),
198 None => {
199 self.animation_timer = Some(Instant::now());
200 0.0
201 }
202 }
203 }
204}
205
206impl<Rend: WindowRenderer> View<Rend> {
207 pub fn resume(&mut self) {
208 let window_id = self.window_id();
209 let animation_time = self.current_animation_time();
210
211 let mut inner = self.doc.inner_mut();
212
213 inner.resolve(animation_time);
215
216 let (width, height) = inner.viewport().window_size;
218 let scale = inner.viewport().scale_f64();
219 self.renderer
220 .resume(Arc::new(self.window.clone()), width, height);
221 if !self.renderer.is_active() {
222 panic!("Renderer failed to resume");
223 };
224
225 let insets = self.safe_area_insets.to_logical(scale);
227 self.renderer.render(|scene| {
228 paint_scene(scene, &inner, scale, width, height, insets.left, insets.top)
229 });
230
231 self.waker = Some(create_waker(&self.proxy, window_id));
233 }
234
235 pub fn suspend(&mut self) {
236 self.waker = None;
237 self.renderer.suspend();
238 }
239
240 pub fn poll(&mut self) -> bool {
241 if let Some(waker) = &self.waker {
242 let cx = std::task::Context::from_waker(waker);
243 if self.doc.poll(Some(cx)) {
244 #[cfg(feature = "accessibility")]
245 {
246 let inner = self.doc.inner();
247 if inner.has_changes() {
248 self.accessibility.update_tree(&inner);
249 }
250 }
251
252 self.request_redraw();
253 return true;
254 }
255 }
256
257 false
258 }
259
260 pub fn request_redraw(&self) {
261 if self.renderer.is_active() {
262 self.window.request_redraw();
263 #[cfg(target_os = "ios")]
264 self.ios_request_redraw.set(true);
265 }
266 }
267
268 pub fn redraw(&mut self) {
269 #[cfg(target_os = "ios")]
270 self.ios_request_redraw.set(false);
271 let animation_time = self.current_animation_time();
272 let is_visible = self.is_visible;
273
274 let mut inner = self.doc.inner_mut();
275 inner.resolve(animation_time);
276
277 let (width, height) = inner.viewport().window_size;
278 let scale = inner.viewport().scale_f64();
279 let is_animating = inner.is_animating();
280 let is_blocked = inner.has_pending_critical_resources();
281 let insets = self.safe_area_insets.to_logical(scale);
282
283 if !is_blocked && is_visible {
284 self.renderer.render(|scene| {
285 paint_scene(scene, &inner, scale, width, height, insets.left, insets.top)
286 });
287 }
288
289 drop(inner);
290
291 if !is_blocked && is_visible && is_animating {
292 self.request_redraw();
293 }
294 }
295
296 pub fn pointer_coords(&self, position: PhysicalPosition<f64>) -> PointerCoords {
297 let inner = self.doc.inner();
298 let scale = inner.viewport().scale_f64();
299 let LogicalPosition::<f32> {
300 x: screen_x,
301 y: screen_y,
302 } = position.to_logical(scale);
303 let viewport_scroll_offset = inner.viewport_scroll();
304 let client_x = screen_x - (self.safe_area_insets.left as f64 / scale) as f32;
305 let client_y = screen_y - (self.safe_area_insets.top as f64 / scale) as f32;
306 let page_x = client_x + viewport_scroll_offset.x as f32;
307 let page_y = client_y + viewport_scroll_offset.y as f32;
308
309 PointerCoords {
310 screen_x,
311 screen_y,
312 client_x,
313 client_y,
314 page_x,
315 page_y,
316 }
317 }
318
319 pub fn window_id(&self) -> WindowId {
320 self.window.id()
321 }
322
323 #[inline]
324 pub fn with_viewport(&mut self, cb: impl FnOnce(&mut Viewport)) {
325 let mut inner = self.doc.inner_mut();
326 let mut viewport = inner.viewport_mut();
327 cb(&mut viewport);
328 let (width, height) = viewport.window_size;
329 drop(viewport);
330 drop(inner);
331 if width > 0 && height > 0 {
332 let insets = self.safe_area_insets;
333 self.renderer.set_size(
334 width + insets.left + insets.right,
335 height + insets.top + insets.bottom,
336 );
337 self.request_redraw();
338 }
339 }
340
341 #[cfg(feature = "accessibility")]
342 pub fn build_accessibility_tree(&mut self) {
343 let inner = self.doc.inner();
344 self.accessibility.update_tree(&inner);
345 }
346
347 #[cfg(target_os = "macos")]
348 pub fn handle_apple_standard_keybinding(&mut self, command: &str) {
349 use blitz_traits::SmolStr;
350 let event = UiEvent::AppleStandardKeybinding(SmolStr::new(command));
351 self.doc.handle_ui_event(event);
352 }
353
354 pub fn handle_winit_event(&mut self, event: WindowEvent) {
355 #[cfg(feature = "accessibility")]
357 self.accessibility
358 .process_window_event(&*self.window, &event);
359
360 match event {
361 WindowEvent::Destroyed => {}
362 WindowEvent::ActivationTokenDone { .. } => {},
363 WindowEvent::CloseRequested => {
364 }
366 WindowEvent::RedrawRequested => {
367 self.redraw();
368 }
369 WindowEvent::Moved(_) => {}
370 WindowEvent::Occluded(is_occluded) => {
371 self.is_visible = !is_occluded;
372 if self.is_visible {
373 self.request_redraw();
374 }
375 },
376 WindowEvent::SurfaceResized(physical_size) => {
377 self.safe_area_insets = self.window.safe_area();
378 let insets = self.safe_area_insets;
379 let width = physical_size.width - insets.left - insets.right;
380 let height = physical_size.height - insets.top - insets.bottom;
381 self.with_viewport(|v| v.window_size = (width, height));
382 self.request_redraw();
383 }
384 WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
385 self.with_viewport(|v| v.set_hidpi_scale(scale_factor as f32));
386 self.request_redraw();
387 }
388 WindowEvent::ThemeChanged(theme) => {
389 let color_scheme = theme_to_color_scheme(self.theme_override.unwrap_or(theme));
390 let mut inner = self.doc.inner_mut();
391 inner.viewport_mut().color_scheme = color_scheme;
392 }
393 WindowEvent::Ime(ime_event) => {
394 self.doc.handle_ui_event(UiEvent::Ime(winit_ime_to_blitz(ime_event)));
395 self.request_redraw();
396 },
397 WindowEvent::ModifiersChanged(new_state) => {
398 self.keyboard_modifiers = new_state;
400 }
401 WindowEvent::KeyboardInput { event, .. } => {
402 if let PhysicalKey::Code(key_code) = event.physical_key && event.state.is_pressed() {
403 let ctrl = self.keyboard_modifiers.state().control_key();
404 let meta = self.keyboard_modifiers.state().meta_key();
405 let alt = self.keyboard_modifiers.state().alt_key();
406
407 if ctrl | meta {
409 match key_code {
410 KeyCode::Equal => {
411 self.doc.inner_mut().viewport_mut().zoom_by(0.1);
412 },
413 KeyCode::Minus => {
414 self.doc.inner_mut().viewport_mut().zoom_by(-0.1);
415 },
416 KeyCode::Digit0 => {
417 self.doc.inner_mut().viewport_mut().set_zoom(1.0);
418 }
419 _ => {}
420 };
421 }
422
423 if alt {
425 match key_code {
426 KeyCode::KeyD => {
427 let mut inner = self.doc.inner_mut();
428 inner.devtools_mut().toggle_show_layout();
429 drop(inner);
430 self.request_redraw();
431 }
432 KeyCode::KeyH => {
433 let mut inner = self.doc.inner_mut();
434 inner.devtools_mut().toggle_highlight_hover();
435 drop(inner);
436 self.request_redraw();
437 }
438 KeyCode::KeyT => self.doc.inner().print_taffy_tree(),
439 _ => {}
440 };
441 }
442
443 }
444
445 let key_event_data = winit_key_event_to_blitz(&event, self.keyboard_modifiers.state());
447 let event = if event.state.is_pressed() {
448 UiEvent::KeyDown(key_event_data)
449 } else {
450 UiEvent::KeyUp(key_event_data)
451 };
452
453 self.doc.handle_ui_event(event);
454 }
455 WindowEvent::PointerEntered { .. } => {}
456 WindowEvent::PointerLeft { .. } => {}
457 WindowEvent::PointerMoved { position, source, primary, .. } => {
458 self.pointer_pos = position;
459 let event = UiEvent::PointerMove(BlitzPointerEvent {
460 id: pointer_source_to_blitz(&source),
461 is_primary: primary,
462 coords: self.pointer_coords(position),
463 button: Default::default(),
464 buttons: self.buttons,
465 mods: winit_modifiers_to_kbt_modifiers(self.keyboard_modifiers.state()),
466 details: pointer_source_to_blitz_details(&source)
467 });
468 self.doc.handle_ui_event(event);
469 }
470 WindowEvent::PointerButton { button, state, primary, position, .. } => {
471 let id = button_source_to_blitz(&button);
472 let coords = self.pointer_coords(position);
473 self.pointer_pos = position;
474 let button = match &button {
475 ButtonSource::Mouse(mouse_button) => match mouse_button {
476 MouseButton::Left => MouseEventButton::Main,
477 MouseButton::Right => MouseEventButton::Secondary,
478 MouseButton::Middle => MouseEventButton::Auxiliary,
479 _ => MouseEventButton::Auxiliary,
481 }
482 _ => MouseEventButton::Main,
483 };
484
485 match state {
486 ElementState::Pressed => self.buttons |= button.into(),
487 ElementState::Released => self.buttons ^= button.into(),
488 }
489
490 if id != BlitzPointerId::Mouse {
491 let event = UiEvent::PointerMove(BlitzPointerEvent {
492 id,
493 is_primary: primary,
494 coords,
495 button: Default::default(),
496 buttons: self.buttons,
497 mods: winit_modifiers_to_kbt_modifiers(self.keyboard_modifiers.state()),
498 details: PointerDetails::default()
499 });
500 self.doc.handle_ui_event(event);
501 }
502
503 let event = BlitzPointerEvent {
504 id,
505 is_primary: primary,
506 coords,
507 button,
508 buttons: self.buttons,
509 mods: winit_modifiers_to_kbt_modifiers(self.keyboard_modifiers.state()),
510
511 details: PointerDetails::default(),
513 };
514
515 let event = match state {
516 ElementState::Pressed => UiEvent::PointerDown(event),
517 ElementState::Released => UiEvent::PointerUp(event),
518 };
519
520 self.doc.handle_ui_event(event);
521 self.request_redraw();
522 }
523 WindowEvent::MouseWheel { delta, .. } => {
524 let blitz_delta = match delta {
525 winit::event::MouseScrollDelta::LineDelta(x, y) => BlitzWheelDelta::Lines(x as f64, y as f64),
526 winit::event::MouseScrollDelta::PixelDelta(pos) => BlitzWheelDelta::Pixels(pos.x, pos.y),
527 };
528
529 let event = BlitzWheelEvent {
530 delta: blitz_delta,
531 coords: self.pointer_coords(self.pointer_pos),
532 buttons: self.buttons,
533 mods: winit_modifiers_to_kbt_modifiers(self.keyboard_modifiers.state()),
534 };
535
536 self.doc.handle_ui_event(UiEvent::Wheel(event));
537 }
538 WindowEvent::Focused(_) => {}
539 WindowEvent::TouchpadPressure { .. } => {}
540 WindowEvent::PinchGesture { .. } => {},
541 WindowEvent::PanGesture { .. } => {},
542 WindowEvent::DoubleTapGesture { .. } => {},
543 WindowEvent::RotationGesture { .. } => {},
544 WindowEvent::DragEntered { .. } => {},
545 WindowEvent::DragMoved { .. } => {},
546 WindowEvent::DragDropped { .. } => {},
547 WindowEvent::DragLeft { .. } => {},
548 }
549 }
550}