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::{BlitzShellEvent, 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 web_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
31#[cfg(target_os = "macos")]
34fn get_safe_area_insets(_window: &dyn Window) -> PhysicalInsets<u32> {
35 Default::default()
36}
37#[cfg(not(target_os = "macos"))]
38fn get_safe_area_insets(window: &dyn Window) -> PhysicalInsets<u32> {
39 window.safe_area()
40}
41
42pub struct WindowConfig<Rend: WindowRenderer> {
43 doc: Box<dyn Document>,
44 pub(crate) attributes: WindowAttributes,
45 renderer: Rend,
46}
47
48impl<Rend: WindowRenderer> WindowConfig<Rend> {
49 pub fn new(doc: Box<dyn Document>, renderer: Rend) -> Self {
50 Self::with_attributes(doc, renderer, WindowAttributes::default())
51 }
52
53 pub fn with_attributes(
54 doc: Box<dyn Document>,
55 renderer: Rend,
56 attributes: WindowAttributes,
57 ) -> Self {
58 WindowConfig {
59 doc,
60 attributes,
61 renderer,
62 }
63 }
64}
65
66pub struct View<Rend: WindowRenderer> {
67 pub doc: Box<dyn Document>,
68
69 pub renderer: Rend,
70 pub waker: Option<Waker>,
71
72 pub proxy: BlitzShellProxy,
73 pub window: Arc<dyn Window>,
74
75 pub theme_override: Option<Theme>,
78 pub keyboard_modifiers: Modifiers,
79 pub buttons: MouseEventButtons,
80 pub pointer_pos: PhysicalPosition<f64>,
81 pub animation_timer: Option<Instant>,
82 pub is_visible: bool,
83 pub safe_area_insets: PhysicalInsets<u32>,
84
85 #[cfg(target_arch = "wasm32")]
86 pending_resize: Option<winit::dpi::PhysicalSize<u32>>,
87 #[cfg(target_arch = "wasm32")]
88 last_resize_at: Option<web_time::Instant>,
89 #[cfg(target_arch = "wasm32")]
93 resize_timer_scheduled: bool,
94
95 #[cfg(feature = "accessibility")]
96 pub accessibility: AccessibilityState,
98
99 #[cfg(target_os = "ios")]
104 pub ios_request_redraw: std::cell::Cell<bool>,
105}
106
107impl<Rend: WindowRenderer> View<Rend> {
108 pub fn init(
109 config: WindowConfig<Rend>,
110 event_loop: &dyn ActiveEventLoop,
111 proxy: &BlitzShellProxy,
112 ) -> Self {
113 let is_visible = config.attributes.visible;
116 let requested_surface_size = config.attributes.surface_size;
120 let attrs = config.attributes.with_visible(false);
121
122 let winit_window: Arc<dyn Window> = Arc::from(event_loop.create_window(attrs).unwrap());
123 #[cfg(feature = "accessibility")]
124 let accessibility = AccessibilityState::new(&*winit_window, proxy.clone());
125
126 if is_visible {
127 winit_window.set_visible(true);
128 }
129
130 let scale = winit_window.scale_factor() as f32;
133 let mut size = winit_window.surface_size();
134 if (size.width == 0 || size.height == 0)
135 && let Some(requested) = requested_surface_size
136 {
137 size = requested.to_physical(scale as f64);
138 }
139 #[cfg(target_arch = "wasm32")]
144 if size.width == 0 || size.height == 0 {
145 use winit::platform::web::WindowExtWeb;
146 if let Some(canvas) = winit_window.canvas() {
147 let css_w = canvas.offset_width().max(0) as u32;
148 let css_h = canvas.offset_height().max(0) as u32;
149 if css_w > 0 && css_h > 0 {
150 size = winit::dpi::LogicalSize::new(css_w, css_h).to_physical(scale as f64);
151 }
152 }
153 }
154 let safe_area_insets = get_safe_area_insets(&*winit_window);
155 let theme = winit_window.theme().unwrap_or(Theme::Light);
156 let color_scheme = theme_to_color_scheme(theme);
157 let viewport = Viewport::new(size.width, size.height, scale, color_scheme);
158
159 let shell_provider = BlitzShellProvider::new(winit_window.clone());
161
162 let mut doc = config.doc;
163 let mut inner = doc.inner_mut();
164 inner.set_viewport(viewport);
165 inner.set_shell_provider(Arc::new(shell_provider));
166
167 let title = inner.find_title_node().map(|node| node.text_content());
171 if let Some(title) = title {
172 winit_window.set_title(&title);
173 }
174
175 drop(inner);
176
177 Self {
178 renderer: config.renderer,
179 waker: None,
180 animation_timer: None,
181 keyboard_modifiers: Default::default(),
182 proxy: proxy.clone(),
183 window: winit_window.clone(),
184 doc,
185 theme_override: None,
186 buttons: MouseEventButtons::None,
187 safe_area_insets,
188 #[cfg(target_arch = "wasm32")]
189 pending_resize: None,
190 #[cfg(target_arch = "wasm32")]
191 last_resize_at: None,
192 #[cfg(target_arch = "wasm32")]
193 resize_timer_scheduled: false,
194 pointer_pos: Default::default(),
195 is_visible: winit_window.is_visible().unwrap_or(true),
196 #[cfg(feature = "accessibility")]
197 accessibility,
198
199 #[cfg(target_os = "ios")]
200 ios_request_redraw: std::cell::Cell::new(false),
201 }
202 }
203
204 pub fn replace_document(&mut self, new_doc: Box<dyn Document>, retain_scroll_position: bool) {
205 let inner = self.doc.inner();
206 let scroll = inner.viewport_scroll();
207 let viewport = inner.viewport().clone();
208 let shell_provider = inner.shell_provider.clone();
209 drop(inner);
210
211 self.doc = new_doc;
212
213 let mut inner = self.doc.inner_mut();
214 inner.set_viewport(viewport);
215 inner.set_shell_provider(shell_provider);
216 drop(inner);
217
218 self.poll();
219 self.request_redraw();
220
221 if retain_scroll_position {
222 self.doc.inner_mut().set_viewport_scroll(scroll);
223 }
224 }
225
226 pub fn theme_override(&self) -> Option<Theme> {
227 self.theme_override
228 }
229
230 pub fn current_theme(&self) -> Theme {
231 color_scheme_to_theme(self.doc.inner().viewport().color_scheme)
232 }
233
234 pub fn set_theme_override(&mut self, theme: Option<Theme>) {
235 self.theme_override = theme;
236 let theme = theme.or(self.window.theme()).unwrap_or(Theme::Light);
237 self.with_viewport(|v| v.color_scheme = theme_to_color_scheme(theme));
238 }
239
240 pub fn downcast_doc_mut<T: 'static>(&mut self) -> &mut T {
241 (&mut *self.doc as &mut dyn Any)
242 .downcast_mut::<T>()
243 .unwrap()
244 }
245
246 pub fn current_animation_time(&mut self) -> f64 {
247 match &self.animation_timer {
248 Some(start) => Instant::now().duration_since(*start).as_secs_f64(),
249 None => {
250 self.animation_timer = Some(Instant::now());
251 0.0
252 }
253 }
254 }
255}
256
257impl<Rend: WindowRenderer> View<Rend> {
258 pub fn resume(&mut self) {
263 let window_id = self.window_id();
264 let animation_time = self.current_animation_time();
265
266 let (width, height) = {
267 let mut inner = self.doc.inner_mut();
268 inner.resolve(animation_time);
269 inner.viewport().window_size
270 };
271
272 let proxy = self.proxy.clone();
273 self.renderer
274 .resume(Arc::new(self.window.clone()), width, height, move || {
275 proxy.send_event(BlitzShellEvent::ResumeReady { window_id });
276 });
277 }
278
279 pub fn complete_resume(&mut self) -> bool {
283 if !self.renderer.complete_resume() {
284 return false;
285 }
286
287 let window_id = self.window_id();
288
289 let animation_time = self.current_animation_time();
294 let mut inner = self.doc.inner_mut();
295 inner.resolve(animation_time);
296 let (width, height) = inner.viewport().window_size;
297 let scale = inner.viewport().scale_f64();
298 let insets = self.safe_area_insets.to_logical(scale);
299
300 #[cfg(feature = "custom-widget")]
301 inner.can_create_surfaces(&self.renderer as _);
302
303 self.renderer.set_size(width, height);
304
305 self.renderer.render(|scene| {
306 paint_scene(
307 scene,
308 &mut inner,
309 scale,
310 width,
311 height,
312 insets.left,
313 insets.top,
314 )
315 });
316
317 self.waker = Some(create_waker(&self.proxy, window_id));
318 true
319 }
320
321 pub fn suspend(&mut self) {
322 self.waker = None;
323 self.renderer.suspend();
324
325 #[cfg(feature = "custom-widget")]
326 self.doc.inner_mut().destroy_surfaces();
327 }
328
329 pub fn poll(&mut self) -> bool {
330 if let Some(waker) = &self.waker {
331 let cx = std::task::Context::from_waker(waker);
332 if self.doc.poll(Some(cx)) {
333 #[cfg(feature = "accessibility")]
334 {
335 let inner = self.doc.inner();
336 if inner.has_changes() {
337 self.accessibility.update_tree(&inner);
338 }
339 }
340
341 self.request_redraw();
342 return true;
343 }
344 }
345
346 false
347 }
348
349 pub fn request_redraw(&self) {
350 if self.renderer.is_active() {
351 self.window.request_redraw();
352 #[cfg(target_os = "ios")]
353 self.ios_request_redraw.set(true);
354 }
355 }
356
357 pub fn redraw(&mut self) {
358 #[cfg(target_os = "ios")]
359 self.ios_request_redraw.set(false);
360 let animation_time = self.current_animation_time();
361 let is_visible = self.is_visible;
362
363 let mut inner = self.doc.inner_mut();
364 inner.resolve(animation_time);
365
366 #[cfg(feature = "custom-widget")]
368 for id in inner.take_pending_resource_deallocations() {
369 self.renderer.unregister_resource(id);
370 }
371
372 let (width, height) = inner.viewport().window_size;
373 let scale = inner.viewport().scale_f64();
374 let is_animating = inner.is_animating();
375 let is_blocked = inner.has_pending_critical_resources();
376 let insets = self.safe_area_insets.to_logical(scale);
377
378 if !is_blocked && is_visible {
379 self.renderer.render(|scene| {
380 paint_scene(
381 scene,
382 &mut inner,
383 scale,
384 width,
385 height,
386 insets.left,
387 insets.top,
388 )
389 });
390 }
391
392 drop(inner);
393
394 if !is_blocked && is_visible && is_animating {
395 self.request_redraw();
396 }
397 }
398
399 pub fn pointer_coords(&self, position: PhysicalPosition<f64>) -> PointerCoords {
400 let inner = self.doc.inner();
401 let scale = inner.viewport().scale_f64();
402 let LogicalPosition::<f32> {
403 x: screen_x,
404 y: screen_y,
405 } = position.to_logical(scale);
406 let viewport_scroll_offset = inner.viewport_scroll();
407 let client_x = screen_x - (self.safe_area_insets.left as f64 / scale) as f32;
408 let client_y = screen_y - (self.safe_area_insets.top as f64 / scale) as f32;
409 let page_x = client_x + viewport_scroll_offset.x as f32;
410 let page_y = client_y + viewport_scroll_offset.y as f32;
411
412 PointerCoords {
413 screen_x,
414 screen_y,
415 client_x,
416 client_y,
417 page_x,
418 page_y,
419 }
420 }
421
422 pub fn window_id(&self) -> WindowId {
423 self.window.id()
424 }
425
426 #[inline]
427 pub fn with_viewport(&mut self, cb: impl FnOnce(&mut Viewport)) {
428 let mut inner = self.doc.inner_mut();
429 let mut viewport = inner.viewport_mut();
430 cb(&mut viewport);
431 let (width, height) = viewport.window_size;
432 drop(viewport);
433 drop(inner);
434 if width > 0 && height > 0 {
435 let insets = self.safe_area_insets;
436 self.renderer.set_size(
437 width + insets.left + insets.right,
438 height + insets.top + insets.bottom,
439 );
440 self.request_redraw();
441 }
442 }
443
444 #[cfg(feature = "accessibility")]
445 pub fn build_accessibility_tree(&mut self) {
446 let inner = self.doc.inner();
447 self.accessibility.update_tree(&inner);
448 }
449
450 #[cfg(target_arch = "wasm32")]
451 const RESIZE_DEBOUNCE_MS: u32 = 100;
452
453 #[cfg(target_arch = "wasm32")]
454 fn schedule_resize_settle_check(&mut self, delay_ms: u32) {
455 use wasm_bindgen::JsCast;
456 use wasm_bindgen::closure::Closure;
457
458 let proxy = self.proxy.clone();
459 let window_id = self.window_id();
460 let cb = Closure::once_into_js(move || {
461 proxy.send_event(BlitzShellEvent::ResizeSettleCheck { window_id });
462 });
463 if let Some(win) = web_sys::window() {
464 let _ = win.set_timeout_with_callback_and_timeout_and_arguments_0(
465 cb.unchecked_ref(),
466 delay_ms as i32,
467 );
468 self.resize_timer_scheduled = true;
469 }
470 }
471
472 #[cfg(target_arch = "wasm32")]
476 pub fn apply_pending_resize_if_settled(&mut self) {
477 self.resize_timer_scheduled = false;
478 let Some(last) = self.last_resize_at else {
479 return;
480 };
481 let debounce = std::time::Duration::from_millis(Self::RESIZE_DEBOUNCE_MS as u64);
482 let elapsed = web_time::Instant::now().saturating_duration_since(last);
483 if elapsed < debounce {
484 let remaining_ms = (debounce - elapsed).as_millis() as u32;
486 self.schedule_resize_settle_check(remaining_ms);
487 return;
488 }
489 let Some(size) = self.pending_resize.take() else {
490 return;
491 };
492 self.last_resize_at = None;
493
494 let insets = self.safe_area_insets;
495 let width = size.width.saturating_sub(insets.left + insets.right);
496 let height = size.height.saturating_sub(insets.top + insets.bottom);
497 self.with_viewport(|v| v.window_size = (width, height));
498 self.request_redraw();
499 }
500
501 #[cfg(target_os = "macos")]
502 pub fn handle_apple_standard_keybinding(&mut self, command: &str) {
503 use blitz_traits::SmolStr;
504 let event = UiEvent::AppleStandardKeybinding(SmolStr::new(command));
505 self.doc.handle_ui_event(event);
506 }
507
508 pub fn handle_winit_event(&mut self, event: WindowEvent) {
509 #[cfg(feature = "accessibility")]
511 self.accessibility
512 .process_window_event(&*self.window, &event);
513
514 match event {
515 WindowEvent::Destroyed => {}
516 WindowEvent::ActivationTokenDone { .. } => {},
517 WindowEvent::CloseRequested => {
518 }
520 WindowEvent::RedrawRequested => {
521 self.redraw();
522 }
523 WindowEvent::Moved(_) => {}
524 WindowEvent::Occluded(is_occluded) => {
525 self.is_visible = !is_occluded;
526 if self.is_visible {
527 self.request_redraw();
528 }
529 },
530 WindowEvent::SurfaceResized(physical_size) => {
531 self.safe_area_insets = get_safe_area_insets(&*self.window);
532 #[cfg(target_arch = "wasm32")]
536 {
537 self.pending_resize = Some(physical_size);
538 self.last_resize_at = Some(web_time::Instant::now());
539 if !self.resize_timer_scheduled {
540 self.schedule_resize_settle_check(Self::RESIZE_DEBOUNCE_MS);
541 }
542 }
543 #[cfg(not(target_arch = "wasm32"))]
544 {
545 let insets = self.safe_area_insets;
546 let width = physical_size.width - insets.left - insets.right;
547 let height = physical_size.height - insets.top - insets.bottom;
548 self.with_viewport(|v| v.window_size = (width, height));
549 self.request_redraw();
550 }
551 }
552 WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
553 self.with_viewport(|v| v.set_hidpi_scale(scale_factor as f32));
554 self.request_redraw();
555 }
556 WindowEvent::ThemeChanged(theme) => {
557 let color_scheme = theme_to_color_scheme(self.theme_override.unwrap_or(theme));
558 let mut inner = self.doc.inner_mut();
559 inner.viewport_mut().color_scheme = color_scheme;
560 }
561 WindowEvent::Ime(ime_event) => {
562 self.doc.handle_ui_event(UiEvent::Ime(winit_ime_to_blitz(ime_event)));
563 self.request_redraw();
564 },
565 WindowEvent::ModifiersChanged(new_state) => {
566 self.keyboard_modifiers = new_state;
568 }
569 WindowEvent::KeyboardInput { event, .. } => {
570 if let PhysicalKey::Code(key_code) = event.physical_key && event.state.is_pressed() {
571 let ctrl = self.keyboard_modifiers.state().control_key();
572 let meta = self.keyboard_modifiers.state().meta_key();
573 let alt = self.keyboard_modifiers.state().alt_key();
574
575 if ctrl | meta {
577 match key_code {
578 KeyCode::Equal => {
579 self.doc.inner_mut().viewport_mut().zoom_by(0.1);
580 },
581 KeyCode::Minus => {
582 self.doc.inner_mut().viewport_mut().zoom_by(-0.1);
583 },
584 KeyCode::Digit0 => {
585 self.doc.inner_mut().viewport_mut().set_zoom(1.0);
586 }
587 _ => {}
588 };
589 }
590
591 if alt {
593 match key_code {
594 KeyCode::KeyD => {
595 let mut inner = self.doc.inner_mut();
596 inner.devtools_mut().toggle_show_layout();
597 drop(inner);
598 self.request_redraw();
599 }
600 KeyCode::KeyH => {
601 let mut inner = self.doc.inner_mut();
602 inner.devtools_mut().toggle_highlight_hover();
603 drop(inner);
604 self.request_redraw();
605 }
606 KeyCode::KeyT => self.doc.inner().print_taffy_tree(),
607 _ => {}
608 };
609 }
610
611 }
612
613 let key_event_data = winit_key_event_to_blitz(&event, self.keyboard_modifiers.state());
615 let event = if event.state.is_pressed() {
616 UiEvent::KeyDown(key_event_data)
617 } else {
618 UiEvent::KeyUp(key_event_data)
619 };
620
621 self.doc.handle_ui_event(event);
622 }
623 WindowEvent::PointerEntered { .. } => {}
624 WindowEvent::PointerLeft { .. } => {}
625 WindowEvent::PointerMoved { position, source, primary, .. } => {
626 self.pointer_pos = position;
627 let event = UiEvent::PointerMove(BlitzPointerEvent {
628 id: pointer_source_to_blitz(&source),
629 is_primary: primary,
630 coords: self.pointer_coords(position),
631 button: Default::default(),
632 buttons: self.buttons,
633 mods: winit_modifiers_to_kbt_modifiers(self.keyboard_modifiers.state()),
634 details: pointer_source_to_blitz_details(&source)
635 });
636 self.doc.handle_ui_event(event);
637 }
638 WindowEvent::PointerButton { button, state, primary, position, .. } => {
639 let id = button_source_to_blitz(&button);
640 let coords = self.pointer_coords(position);
641 self.pointer_pos = position;
642 let button = match &button {
643 ButtonSource::Mouse(mouse_button) => match mouse_button {
644 MouseButton::Left => MouseEventButton::Main,
645 MouseButton::Right => MouseEventButton::Secondary,
646 MouseButton::Middle => MouseEventButton::Auxiliary,
647 _ => MouseEventButton::Auxiliary,
649 }
650 _ => MouseEventButton::Main,
651 };
652
653 match state {
654 ElementState::Pressed => self.buttons |= button.into(),
655 ElementState::Released => self.buttons ^= button.into(),
656 }
657
658 if id != BlitzPointerId::Mouse {
659 let event = UiEvent::PointerMove(BlitzPointerEvent {
660 id,
661 is_primary: primary,
662 coords,
663 button: Default::default(),
664 buttons: self.buttons,
665 mods: winit_modifiers_to_kbt_modifiers(self.keyboard_modifiers.state()),
666 details: PointerDetails::default()
667 });
668 self.doc.handle_ui_event(event);
669 }
670
671 let event = BlitzPointerEvent {
672 id,
673 is_primary: primary,
674 coords,
675 button,
676 buttons: self.buttons,
677 mods: winit_modifiers_to_kbt_modifiers(self.keyboard_modifiers.state()),
678
679 details: PointerDetails::default(),
681 };
682
683 let event = match state {
684 ElementState::Pressed => UiEvent::PointerDown(event),
685 ElementState::Released => UiEvent::PointerUp(event),
686 };
687
688 self.doc.handle_ui_event(event);
689 self.request_redraw();
690 }
691 WindowEvent::MouseWheel { delta, .. } => {
692 let blitz_delta = match delta {
693 winit::event::MouseScrollDelta::LineDelta(x, y) => BlitzWheelDelta::Lines(x as f64, y as f64),
694 winit::event::MouseScrollDelta::PixelDelta(pos) => BlitzWheelDelta::Pixels(pos.x, pos.y),
695 };
696
697 let event = BlitzWheelEvent {
698 delta: blitz_delta,
699 coords: self.pointer_coords(self.pointer_pos),
700 buttons: self.buttons,
701 mods: winit_modifiers_to_kbt_modifiers(self.keyboard_modifiers.state()),
702 };
703
704 self.doc.handle_ui_event(UiEvent::Wheel(event));
705 }
706 WindowEvent::Focused(_) => {}
707 WindowEvent::TouchpadPressure { .. } => {}
708 WindowEvent::PinchGesture { .. } => {},
709 WindowEvent::PanGesture { .. } => {},
710 WindowEvent::DoubleTapGesture { .. } => {},
711 WindowEvent::RotationGesture { .. } => {},
712 WindowEvent::DragEntered { .. } => {},
713 WindowEvent::DragMoved { .. } => {},
714 WindowEvent::DragDropped { .. } => {},
715 WindowEvent::DragLeft { .. } => {},
716 }
717 }
718}