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
26type 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 viewport: Viewport,
65
66 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: AccessibilityState,
79
80 #[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 winit_window.set_ime_allowed(true);
96
97 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 self.doc.as_mut().set_viewport(self.viewport.clone());
162 self.doc.as_mut().resolve();
163
164 self.renderer.resume(&self.viewport);
166 if !self.renderer.is_active() {
167 panic!("Renderer failed to resume");
168 };
169
170 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 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 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); 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 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 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 if button == MouseEventButton::Main {
358 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 WindowEvent::Destroyed => {}
383 WindowEvent::ActivationTokenDone { .. } => {},
384 WindowEvent::CloseRequested => {
385 }
387 WindowEvent::RedrawRequested => {
388 self.redraw();
389 }
390
391 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 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 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 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 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 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 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 WindowEvent::CursorEntered { .. } => {}
490 WindowEvent::CursorLeft { .. } => {}
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 WindowEvent::DroppedFile(_) => {}
535 WindowEvent::HoveredFile(_) => {}
536 WindowEvent::HoveredFileCancelled => {}
537 WindowEvent::Focused(_) => {}
538
539 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}