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::UiEvent;
11use blitz_traits::{BlitzMouseButtonEvent, MouseEventButton, MouseEventButtons, Viewport};
12use winit::keyboard::PhysicalKey;
13
14use std::marker::PhantomData;
15use std::sync::Arc;
16use std::task::Waker;
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(all(feature = "menu", not(any(target_os = "android", target_os = "ios"))))]
23use crate::menu::init_menu;
24
25#[cfg(feature = "accessibility")]
26use crate::accessibility::AccessibilityState;
27
28pub struct WindowConfig<Rend: WindowRenderer> {
29 doc: Box<dyn Document>,
30 attributes: WindowAttributes,
31 rend: PhantomData<Rend>,
32}
33
34impl<Rend: WindowRenderer> WindowConfig<Rend> {
35 pub fn new(doc: Box<dyn Document>) -> Self {
36 Self::with_attributes(doc, Window::default_attributes())
37 }
38
39 pub fn with_attributes(doc: Box<dyn Document>, attributes: WindowAttributes) -> Self {
40 WindowConfig {
41 doc,
42 attributes,
43 rend: PhantomData,
44 }
45 }
46}
47
48pub struct View<Rend: WindowRenderer> {
49 pub doc: Box<dyn Document>,
50
51 pub(crate) renderer: Rend,
52 pub(crate) waker: Option<Waker>,
53
54 event_loop_proxy: EventLoopProxy<BlitzShellEvent>,
55 window: Arc<Window>,
56
57 theme_override: Option<Theme>,
60 keyboard_modifiers: Modifiers,
61 buttons: MouseEventButtons,
62 mouse_pos: (f32, f32),
63
64 #[cfg(feature = "accessibility")]
65 accessibility: AccessibilityState,
67
68 #[cfg(all(feature = "menu", not(any(target_os = "android", target_os = "ios"))))]
71 _menu: muda::Menu,
72}
73
74impl<Rend: WindowRenderer> View<Rend> {
75 pub fn init(
76 config: WindowConfig<Rend>,
77 event_loop: &ActiveEventLoop,
78 proxy: &EventLoopProxy<BlitzShellEvent>,
79 ) -> Self {
80 let winit_window = Arc::from(event_loop.create_window(config.attributes).unwrap());
81
82 winit_window.set_ime_allowed(true);
84
85 let size = winit_window.inner_size();
87 let scale = winit_window.scale_factor() as f32;
88 let theme = winit_window.theme().unwrap_or(Theme::Light);
89 let color_scheme = theme_to_color_scheme(theme);
90 let viewport = Viewport::new(size.width, size.height, scale, color_scheme);
91
92 let shell_provider = BlitzShellProvider::new(winit_window.clone());
94
95 let mut doc = config.doc;
96 doc.set_viewport(viewport);
97 doc.set_shell_provider(Arc::new(shell_provider));
98
99 Self {
100 renderer: Rend::new(winit_window.clone()),
101 waker: None,
102 keyboard_modifiers: Default::default(),
103 event_loop_proxy: proxy.clone(),
104 window: winit_window.clone(),
105 doc,
106 theme_override: None,
107 buttons: MouseEventButtons::None,
108 mouse_pos: Default::default(),
109 #[cfg(feature = "accessibility")]
110 accessibility: AccessibilityState::new(&winit_window, proxy.clone()),
111
112 #[cfg(all(feature = "menu", not(any(target_os = "android", target_os = "ios"))))]
113 _menu: init_menu(&winit_window),
114 }
115 }
116
117 pub fn replace_document(&mut self, new_doc: Box<dyn Document>, retain_scroll_position: bool) {
118 let scroll = self.doc.viewport_scroll();
119 let viewport = self.doc.viewport().clone();
120 let shell_provider = self.doc.shell_provider.clone();
121
122 self.doc = new_doc;
123 self.doc.set_viewport(viewport);
124 self.doc.set_shell_provider(shell_provider);
125 self.poll();
126 self.request_redraw();
127
128 if retain_scroll_position {
129 self.doc.set_viewport_scroll(scroll);
130 }
131 }
132
133 pub fn theme_override(&self) -> Option<Theme> {
134 self.theme_override
135 }
136
137 pub fn current_theme(&self) -> Theme {
138 color_scheme_to_theme(self.doc.viewport().color_scheme)
139 }
140
141 pub fn set_theme_override(&mut self, theme: Option<Theme>) {
142 self.theme_override = theme;
143 let theme = theme.or(self.window.theme()).unwrap_or(Theme::Light);
144 self.with_viewport(|v| v.color_scheme = theme_to_color_scheme(theme));
145 }
146
147 pub fn downcast_doc_mut<T: 'static>(&mut self) -> &mut T {
148 self.doc.as_any_mut().downcast_mut::<T>().unwrap()
149 }
150}
151
152impl<Rend: WindowRenderer> View<Rend> {
153 pub fn resume(&mut self) {
154 self.doc.resolve();
156
157 let (width, height) = self.doc.viewport().window_size;
159 let scale = self.doc.viewport().scale_f64();
160 self.renderer.resume(width, height);
161 if !self.renderer.is_active() {
162 panic!("Renderer failed to resume");
163 };
164
165 self.renderer
167 .render(|scene| paint_scene(scene, &self.doc, scale, width, height));
168
169 self.waker = Some(create_waker(&self.event_loop_proxy, self.window_id()));
171 }
172
173 pub fn suspend(&mut self) {
174 self.waker = None;
175 self.renderer.suspend();
176 }
177
178 pub fn poll(&mut self) -> bool {
179 if let Some(waker) = &self.waker {
180 let cx = std::task::Context::from_waker(waker);
181 if self.doc.poll(cx) {
182 #[cfg(feature = "accessibility")]
183 {
184 let changed = std::mem::take(&mut self.doc.changed);
186 if !changed.is_empty() {
187 self.accessibility.build_tree(&self.doc);
188 }
189 }
190
191 self.request_redraw();
192 return true;
193 }
194 }
195
196 false
197 }
198
199 pub fn request_redraw(&self) {
200 if self.renderer.is_active() {
201 self.window.request_redraw();
202 }
203 }
204
205 pub fn redraw(&mut self) {
206 self.doc.resolve();
207 let (width, height) = self.doc.viewport().window_size;
208 let scale = self.doc.viewport().scale_f64();
209 self.renderer
210 .render(|scene| paint_scene(scene, &self.doc, scale, width, height));
211 }
212
213 pub fn window_id(&self) -> WindowId {
214 self.window.id()
215 }
216
217 #[inline]
218 pub fn with_viewport(&mut self, cb: impl FnOnce(&mut Viewport)) {
219 let mut viewport = self.doc.viewport_mut();
220 cb(&mut viewport);
221 drop(viewport);
222 let (width, height) = self.doc.viewport().window_size;
223 if width > 0 && height > 0 {
224 self.renderer.set_size(width, height);
225 self.request_redraw();
226 }
227 }
228
229 #[cfg(feature = "accessibility")]
230 pub fn build_accessibility_tree(&mut self) {
231 self.accessibility.build_tree(&self.doc);
232 }
233
234 pub fn handle_winit_event(&mut self, event: WindowEvent) {
235 match event {
236 WindowEvent::Destroyed => {}
238 WindowEvent::ActivationTokenDone { .. } => {},
239 WindowEvent::CloseRequested => {
240 }
242 WindowEvent::RedrawRequested => {
243 self.redraw();
244 }
245
246 WindowEvent::Moved(_) => {}
248 WindowEvent::Occluded(_) => {},
249 WindowEvent::Resized(physical_size) => {
250 self.with_viewport(|v| v.window_size = (physical_size.width, physical_size.height));
251 }
252 WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
253 self.with_viewport(|v| v.set_hidpi_scale(scale_factor as f32));
254 }
255
256 WindowEvent::ThemeChanged(theme) => {
258 let color_scheme = theme_to_color_scheme(self.theme_override.unwrap_or(theme));
259 self.doc.viewport_mut().color_scheme = color_scheme;
260 }
261
262 WindowEvent::Ime(ime_event) => {
264 self.doc.handle_event(UiEvent::Ime(winit_ime_to_blitz(ime_event)));
265 self.request_redraw();
266 },
267 WindowEvent::ModifiersChanged(new_state) => {
268 self.keyboard_modifiers = new_state;
270 }
271 WindowEvent::KeyboardInput { event, .. } => {
272 let PhysicalKey::Code(key_code) = event.physical_key else {
273 return;
274 };
275
276 if event.state.is_pressed() {
277 let ctrl = self.keyboard_modifiers.state().control_key();
278 let meta = self.keyboard_modifiers.state().super_key();
279 let alt = self.keyboard_modifiers.state().alt_key();
280
281 if ctrl | meta {
283 match key_code {
284 KeyCode::Equal => self.doc.viewport_mut().zoom_by(0.1),
285 KeyCode::Minus => self.doc.viewport_mut().zoom_by(-0.1),
286 KeyCode::Digit0 => self.doc.viewport_mut().set_zoom(1.0),
287 _ => {}
288 };
289 }
290
291 if alt {
293 match key_code {
294 KeyCode::KeyD => {
295 self.doc.devtools_mut().toggle_show_layout();
296 self.request_redraw();
297 }
298 KeyCode::KeyH => {
299 self.doc.devtools_mut().toggle_highlight_hover();
300 self.request_redraw();
301 }
302 KeyCode::KeyT => self.doc.print_taffy_tree(),
303 _ => {}
304 };
305 }
306
307 }
308
309 let key_event_data = winit_key_event_to_blitz(&event, self.keyboard_modifiers.state());
311 let event = if event.state.is_pressed() {
312 UiEvent::KeyDown(key_event_data)
313 } else {
314 UiEvent::KeyUp(key_event_data)
315 };
316
317 self.doc.handle_event(event);
318 self.request_redraw();
319 }
320
321
322 WindowEvent::CursorEntered { .. } => {}
324 WindowEvent::CursorLeft { .. } => {}
325 WindowEvent::CursorMoved { position, .. } => {
326 let winit::dpi::LogicalPosition::<f32> { x, y } = position.to_logical(self.window.scale_factor());
327 self.mouse_pos = (x, y);
328 let event = UiEvent::MouseMove(BlitzMouseButtonEvent {
329 x,
330 y,
331 button: Default::default(),
332 buttons: self.buttons,
333 mods: winit_modifiers_to_kbt_modifiers(self.keyboard_modifiers.state()),
334 });
335 self.doc.handle_event(event);
336 self.request_redraw();
337 }
338 WindowEvent::MouseInput { button, state, .. } => {
339 let button = match button {
340 MouseButton::Left => MouseEventButton::Main,
341 MouseButton::Right => MouseEventButton::Secondary,
342 _ => return,
343 };
344
345 match state {
346 ElementState::Pressed => self.buttons |= button.into(),
347 ElementState::Released => self.buttons ^= button.into(),
348 }
349
350 let event = BlitzMouseButtonEvent {
351 x: self.mouse_pos.0,
352 y: self.mouse_pos.1,
353 button,
354 buttons: self.buttons,
355 mods: winit_modifiers_to_kbt_modifiers(self.keyboard_modifiers.state()),
356 };
357
358 let event = match state {
359 ElementState::Pressed => UiEvent::MouseDown(event),
360 ElementState::Released => UiEvent::MouseUp(event),
361 };
362 self.doc.handle_event(event);
363 self.request_redraw();
364 }
365 WindowEvent::MouseWheel { delta, .. } => {
366 let (scroll_x, scroll_y)= match delta {
367 winit::event::MouseScrollDelta::LineDelta(x, y) => (x as f64 * 20.0, y as f64 * 20.0),
368 winit::event::MouseScrollDelta::PixelDelta(offsets) => (offsets.x, offsets.y)
369 };
370
371 if let Some(hover_node_id) = self.doc.get_hover_node_id() {
372 self.doc.scroll_node_by(hover_node_id, scroll_x, scroll_y);
373 } else {
374 self.doc.scroll_viewport_by(scroll_x, scroll_y);
375 }
376 self.request_redraw();
377 }
378
379 WindowEvent::DroppedFile(_) => {}
381 WindowEvent::HoveredFile(_) => {}
382 WindowEvent::HoveredFileCancelled => {}
383 WindowEvent::Focused(_) => {}
384
385 WindowEvent::Touch(_) => {}
388 WindowEvent::TouchpadPressure { .. } => {}
389 WindowEvent::AxisMotion { .. } => {}
390 WindowEvent::PinchGesture { .. } => {},
391 WindowEvent::PanGesture { .. } => {},
392 WindowEvent::DoubleTapGesture { .. } => {},
393 WindowEvent::RotationGesture { .. } => {},
394 }
395 }
396}