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