1use std::sync::Arc;
8#[cfg(not(target_arch = "wasm32"))]
9use std::time::{Duration, Instant};
10#[cfg(target_arch = "wasm32")]
11use web_time::Instant;
12
13#[cfg(not(target_arch = "wasm32"))]
14use fenestra_core::Theme;
15use fenestra_core::{
16 App, Element, Fonts, FrameState, InputEvent, Key, KeyInput, build_frame, dispatch,
17 refresh_hover,
18};
19use kurbo::Point;
20use vello::peniko::Color;
21use vello::util::{RenderContext, RenderSurface};
22use vello::wgpu::{self, CurrentSurfaceTexture};
23use vello::{AaConfig, AaSupport, RenderParams, Renderer, RendererOptions, Scene};
24use winit::application::ApplicationHandler;
25use winit::dpi::LogicalSize;
26use winit::event::{MouseScrollDelta, StartCause, WindowEvent};
27#[cfg(not(target_arch = "wasm32"))]
28use winit::event_loop::EventLoopProxy;
29use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop};
30use winit::window::{Window, WindowId};
31
32use crate::ShellError;
33
34pub(crate) const LINE_SCROLL_PX: f64 = 40.0;
36
37pub(crate) fn wheel_deltas(delta: MouseScrollDelta, scale: f64) -> (f64, f64) {
40 match delta {
41 MouseScrollDelta::LineDelta(x, y) => {
42 (f64::from(x) * LINE_SCROLL_PX, f64::from(y) * LINE_SCROLL_PX)
43 }
44 MouseScrollDelta::PixelDelta(pos) => (pos.x / scale, pos.y / scale),
45 }
46}
47
48#[cfg(not(target_arch = "wasm32"))]
50type PaintFn = Box<dyn FnMut(&mut Scene, f64, f64, Color)>;
51#[cfg(not(target_arch = "wasm32"))]
53type ViewFn = Box<dyn Fn(&Theme) -> Element<()>>;
54
55#[derive(Debug, Clone)]
57pub struct WindowOptions {
58 pub title: String,
60 pub inner_size: (f64, f64),
62 pub min_size: Option<(f64, f64)>,
64 pub resizable: bool,
66 pub maximized: bool,
68 pub fullscreen: bool,
70 pub icon: Option<(u32, u32, Vec<u8>)>,
72 pub fonts: Vec<(fenestra_core::FamilyRole, Vec<u8>)>,
75}
76
77impl WindowOptions {
78 pub fn titled(title: impl Into<String>) -> Self {
80 Self {
81 title: title.into(),
82 inner_size: (1024.0, 768.0),
83 min_size: None,
84 resizable: true,
85 maximized: false,
86 fullscreen: false,
87 icon: None,
88 fonts: Vec::new(),
89 }
90 }
91
92 pub fn with_size(mut self, width: f64, height: f64) -> Self {
94 self.inner_size = (width, height);
95 self
96 }
97
98 pub fn with_min_size(mut self, width: f64, height: f64) -> Self {
100 self.min_size = Some((width, height));
101 self
102 }
103
104 pub fn with_resizable(mut self, resizable: bool) -> Self {
106 self.resizable = resizable;
107 self
108 }
109
110 pub fn maximized(mut self) -> Self {
112 self.maximized = true;
113 self
114 }
115
116 pub fn fullscreen(mut self) -> Self {
118 self.fullscreen = true;
119 self
120 }
121
122 pub fn with_icon(mut self, width: u32, height: u32, rgba: Vec<u8>) -> Self {
125 self.icon = Some((width, height, rgba));
126 self
127 }
128
129 pub fn with_font(mut self, role: fenestra_core::FamilyRole, data: Vec<u8>) -> Self {
132 self.fonts.push((role, data));
133 self
134 }
135}
136
137enum RenderState {
138 Active {
139 surface: Box<RenderSurface<'static>>,
140 valid_surface: bool,
141 window: Arc<Window>,
142 },
143 Suspended(Option<Arc<Window>>),
144 #[cfg(target_arch = "wasm32")]
146 Pending(Arc<Window>),
147}
148
149struct WindowShell {
151 context: RenderContext,
152 renderers: Vec<Option<Renderer>>,
153 state: RenderState,
154 scene: Scene,
155 options: WindowOptions,
156 background: Color,
157 #[cfg(target_arch = "wasm32")]
160 ready: WasmReady,
161}
162
163#[cfg(target_arch = "wasm32")]
165type WasmReady =
166 std::rc::Rc<std::cell::RefCell<Option<(RenderContext, Box<RenderSurface<'static>>)>>>;
167
168impl WindowShell {
169 fn new(options: WindowOptions, background: Color) -> Self {
170 Self {
171 context: RenderContext::new(),
172 renderers: Vec::new(),
173 state: RenderState::Suspended(None),
174 scene: Scene::new(),
175 options,
176 background,
177 #[cfg(target_arch = "wasm32")]
178 ready: WasmReady::default(),
179 }
180 }
181
182 fn resumed(&mut self, event_loop: &ActiveEventLoop) {
183 self.resumed_with(event_loop, |_, _| {});
184 }
185
186 fn resumed_with(
190 &mut self,
191 event_loop: &ActiveEventLoop,
192 before_visible: impl FnOnce(&ActiveEventLoop, &Arc<Window>),
193 ) {
194 let RenderState::Suspended(cached_window) = &mut self.state else {
195 return;
196 };
197 let window = cached_window.take().unwrap_or_else(|| {
198 let attrs = Window::default_attributes()
199 .with_title(self.options.title.clone())
200 .with_inner_size(LogicalSize::new(
201 self.options.inner_size.0,
202 self.options.inner_size.1,
203 ))
204 .with_resizable(self.options.resizable)
205 .with_maximized(self.options.maximized)
206 .with_visible(false);
207 let attrs = match self.options.min_size {
208 Some((w, h)) => attrs.with_min_inner_size(LogicalSize::new(w, h)),
209 None => attrs,
210 };
211 let attrs = if self.options.fullscreen {
212 attrs.with_fullscreen(Some(winit::window::Fullscreen::Borderless(None)))
213 } else {
214 attrs
215 };
216 #[cfg(not(target_arch = "wasm32"))]
217 let attrs = match self.options.icon.clone() {
218 Some((w, h, rgba)) => match winit::window::Icon::from_rgba(rgba, w, h) {
219 Ok(icon) => attrs.with_window_icon(Some(icon)),
220 Err(_) => attrs,
222 },
223 None => attrs,
224 };
225 #[cfg(target_arch = "wasm32")]
226 let attrs = {
227 use winit::platform::web::WindowAttributesExtWebSys;
228 attrs.with_append(true)
230 };
231 Arc::new(
232 event_loop
233 .create_window(attrs)
234 .expect("failed to create window"),
235 )
236 });
237 before_visible(event_loop, &window);
238 let was_hidden = window.is_visible() == Some(false);
239 self.activate(window.clone());
240 if was_hidden {
241 window.set_visible(true);
242 }
243 }
244
245 #[cfg(not(target_arch = "wasm32"))]
248 fn activate(&mut self, window: Arc<Window>) {
249 let size = window.inner_size();
250 let surface = pollster::block_on(self.context.create_surface(
251 window.clone(),
252 size.width.max(1),
253 size.height.max(1),
254 wgpu::PresentMode::AutoVsync,
255 ))
256 .expect("failed to create wgpu surface");
257
258 self.renderers
259 .resize_with(self.context.devices.len(), || None);
260 self.renderers[surface.dev_id].get_or_insert_with(|| {
261 Renderer::new(
262 &self.context.devices[surface.dev_id].device,
263 RendererOptions {
264 use_cpu: false,
265 antialiasing_support: AaSupport::area_only(),
266 ..Default::default()
267 },
268 )
269 .expect("failed to create vello renderer")
270 });
271
272 self.state = RenderState::Active {
273 surface: Box::new(surface),
274 valid_surface: size.width != 0 && size.height != 0,
275 window,
276 };
277 }
278
279 #[cfg(target_arch = "wasm32")]
282 fn activate(&mut self, window: Arc<Window>) {
283 let size = window.inner_size();
284 let ready = std::rc::Rc::clone(&self.ready);
285 let win = window.clone();
286 wasm_bindgen_futures::spawn_local(async move {
287 let mut context = RenderContext::new();
288 let surface = context
289 .create_surface(
290 win.clone(),
291 size.width.max(1),
292 size.height.max(1),
293 wgpu::PresentMode::AutoVsync,
294 )
295 .await
296 .expect("failed to create wgpu surface");
297 *ready.borrow_mut() = Some((context, Box::new(surface)));
298 win.request_redraw();
299 });
300 self.state = RenderState::Pending(window);
301 }
302
303 fn pump(&mut self) {
306 #[cfg(target_arch = "wasm32")]
307 if let RenderState::Pending(window) = &self.state
308 && let Some((context, surface)) = self.ready.borrow_mut().take()
309 {
310 let window = window.clone();
311 self.context = context;
312 self.renderers.clear();
313 self.renderers
314 .resize_with(self.context.devices.len(), || None);
315 self.renderers[surface.dev_id].get_or_insert_with(|| {
316 Renderer::new(
317 &self.context.devices[surface.dev_id].device,
318 RendererOptions {
319 use_cpu: false,
320 antialiasing_support: AaSupport::area_only(),
321 ..Default::default()
322 },
323 )
324 .expect("failed to create vello renderer")
325 });
326 let size = window.inner_size();
327 self.state = RenderState::Active {
328 surface,
329 valid_surface: size.width != 0 && size.height != 0,
330 window,
331 };
332 }
333 }
334
335 fn suspended(&mut self) {
336 if let RenderState::Active { window, .. } = &self.state {
337 self.state = RenderState::Suspended(Some(window.clone()));
338 }
339 }
340
341 fn window(&self) -> Option<&Arc<Window>> {
342 match &self.state {
343 RenderState::Active { window, .. } => Some(window),
344 _ => None,
345 }
346 }
347
348 fn resized(&mut self, width: u32, height: u32) {
349 let RenderState::Active {
350 surface,
351 valid_surface,
352 window,
353 } = &mut self.state
354 else {
355 return;
356 };
357 if width != 0 && height != 0 {
358 self.context.resize_surface(surface, width, height);
359 *valid_surface = true;
360 } else {
361 *valid_surface = false;
362 }
363 window.request_redraw();
364 }
365
366 fn logical_size(&self) -> Option<(f64, f64, f64)> {
368 match &self.state {
369 RenderState::Active {
370 surface, window, ..
371 } => {
372 let scale = window.scale_factor();
373 Some((
374 f64::from(surface.config.width) / scale,
375 f64::from(surface.config.height) / scale,
376 scale,
377 ))
378 }
379 _ => None,
380 }
381 }
382
383 fn present(&mut self, fragment: &Scene) {
385 let RenderState::Active {
386 surface,
387 valid_surface,
388 window,
389 } = &mut self.state
390 else {
391 return;
392 };
393 if !*valid_surface {
394 return;
395 }
396 let width = surface.config.width;
397 let height = surface.config.height;
398 let scale = window.scale_factor();
399
400 self.scene.reset();
401 self.scene
402 .append(fragment, Some(vello::kurbo::Affine::scale(scale)));
403
404 let handle = &self.context.devices[surface.dev_id];
405 self.renderers[surface.dev_id]
406 .as_mut()
407 .expect("renderer exists for surface device")
408 .render_to_texture(
409 &handle.device,
410 &handle.queue,
411 &self.scene,
412 &surface.target_view,
413 &RenderParams {
414 base_color: self.background,
415 width,
416 height,
417 antialiasing_method: AaConfig::Area,
418 },
419 )
420 .expect("vello render failed");
421
422 let surface_texture = match surface.surface.get_current_texture() {
423 CurrentSurfaceTexture::Success(texture) => texture,
424 CurrentSurfaceTexture::Outdated | CurrentSurfaceTexture::Suboptimal(_) => {
425 self.context.configure_surface(surface);
426 window.request_redraw();
427 return;
428 }
429 CurrentSurfaceTexture::Occluded => {
430 return;
433 }
434 CurrentSurfaceTexture::Timeout => {
435 window.request_redraw();
436 return;
437 }
438 CurrentSurfaceTexture::Lost => {
439 let window = window.clone();
442 window.request_redraw();
443 self.activate(window);
444 return;
445 }
446 CurrentSurfaceTexture::Validation => {
447 panic!("validation error acquiring wgpu surface texture")
448 }
449 };
450
451 let mut encoder = handle
452 .device
453 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
454 label: Some("fenestra surface blit"),
455 });
456 surface.blitter.copy(
457 &handle.device,
458 &mut encoder,
459 &surface.target_view,
460 &surface_texture
461 .texture
462 .create_view(&wgpu::TextureViewDescriptor::default()),
463 );
464 handle.queue.submit([encoder.finish()]);
465 surface_texture.present();
466 handle.device.poll(wgpu::PollType::Poll).unwrap();
467 }
468}
469
470#[cfg(not(target_arch = "wasm32"))]
476pub fn run_scene(
477 options: WindowOptions,
478 background: Color,
479 paint: impl FnMut(&mut Scene, f64, f64, Color) + 'static,
480) -> Result<(), ShellError> {
481 let event_loop = EventLoop::new().map_err(ShellError::EventLoop)?;
482 let mut app = SceneApp {
483 shell: WindowShell::new(options, background),
484 fragment: Scene::new(),
485 paint: Box::new(paint),
486 };
487 event_loop.run_app(&mut app).map_err(ShellError::EventLoop)
488}
489
490#[cfg(not(target_arch = "wasm32"))]
491struct SceneApp {
492 shell: WindowShell,
493 fragment: Scene,
494 paint: PaintFn,
495}
496
497#[cfg(not(target_arch = "wasm32"))]
498impl ApplicationHandler for SceneApp {
499 fn resumed(&mut self, event_loop: &ActiveEventLoop) {
500 self.shell.resumed(event_loop);
501 }
502
503 fn suspended(&mut self, _event_loop: &ActiveEventLoop) {
504 self.shell.suspended();
505 }
506
507 fn window_event(
508 &mut self,
509 event_loop: &ActiveEventLoop,
510 window_id: WindowId,
511 event: WindowEvent,
512 ) {
513 if self.shell.window().is_none_or(|w| w.id() != window_id) {
514 return;
515 }
516 match event {
517 WindowEvent::CloseRequested => event_loop.exit(),
518 WindowEvent::Resized(size) => self.shell.resized(size.width, size.height),
519 WindowEvent::ScaleFactorChanged { .. } => {
520 if let Some(w) = self.shell.window() {
521 w.request_redraw();
522 }
523 }
524 WindowEvent::Occluded(occluded) => {
525 if !occluded && let Some(w) = self.shell.window() {
526 w.request_redraw();
527 }
528 }
529 WindowEvent::RedrawRequested => {
530 let Some((lw, lh, _scale)) = self.shell.logical_size() else {
531 return;
532 };
533 self.fragment.reset();
534 let bg = self.shell.background;
535 (self.paint)(&mut self.fragment, lw, lh, bg);
536 let fragment = std::mem::replace(&mut self.fragment, Scene::new());
537 self.shell.present(&fragment);
538 self.fragment = fragment;
539 }
540 _ => {}
541 }
542 }
543}
544
545#[cfg(not(target_arch = "wasm32"))]
548fn live_state() -> FrameState {
549 let mut state = FrameState::new();
550 state.reduced_motion = crate::reduce_motion::os_reduce_motion();
551 state
552}
553
554#[cfg(target_arch = "wasm32")]
557fn live_state() -> FrameState {
558 FrameState::new()
559}
560
561#[cfg(not(target_arch = "wasm32"))]
567pub fn run_static(
568 options: WindowOptions,
569 theme: Theme,
570 view: impl Fn(&Theme) -> Element<()> + 'static,
571) -> Result<(), ShellError> {
572 let event_loop = EventLoop::new().map_err(ShellError::EventLoop)?;
573 let background = theme.bg;
574 let mut fonts = Fonts::with_system();
575 for (role, data) in &options.fonts {
576 fonts.register(*role, data.clone());
577 }
578 let mut app = StaticApp {
579 shell: WindowShell::new(options, background),
580 theme,
581 fonts,
582 state: live_state(),
583 view: Box::new(view),
584 cursor: Point::ORIGIN,
585 started: Instant::now(),
586 last_frame: None,
587 };
588 event_loop.run_app(&mut app).map_err(ShellError::EventLoop)
589}
590
591#[cfg(not(target_arch = "wasm32"))]
592struct StaticApp {
593 shell: WindowShell,
594 theme: Theme,
595 fonts: Fonts,
596 state: FrameState,
597 view: ViewFn,
598 cursor: Point,
600 started: Instant,
601 last_frame: Option<fenestra_core::Frame>,
603}
604
605#[cfg(not(target_arch = "wasm32"))]
606impl StaticApp {
607 fn redraw(&mut self, event_loop: &ActiveEventLoop) {
608 let Some((lw, lh, scale)) = self.shell.logical_size() else {
609 return;
610 };
611 self.state.tick(self.started.elapsed().as_secs_f64());
612 let el = (self.view)(&self.theme);
613 #[expect(clippy::cast_possible_truncation, reason = "window sizes fit in f32")]
614 let frame = build_frame(
615 &el,
616 &self.theme,
617 &mut self.fonts,
618 &mut self.state,
619 (lw as f32, lh as f32),
620 scale,
621 );
622 let scene = frame.paint(&mut self.fonts, &mut self.state);
628 self.shell.present(&scene);
629 if frame.animating {
630 event_loop.set_control_flow(ControlFlow::WaitUntil(
631 Instant::now() + Duration::from_millis(16),
632 ));
633 } else {
634 event_loop.set_control_flow(ControlFlow::Wait);
635 }
636 self.last_frame = Some(frame);
637 }
638}
639
640#[cfg(not(target_arch = "wasm32"))]
641impl ApplicationHandler for StaticApp {
642 fn resumed(&mut self, event_loop: &ActiveEventLoop) {
643 self.shell.resumed(event_loop);
644 }
645
646 fn suspended(&mut self, _event_loop: &ActiveEventLoop) {
647 self.shell.suspended();
648 }
649
650 fn new_events(&mut self, _event_loop: &ActiveEventLoop, cause: StartCause) {
651 if matches!(cause, StartCause::ResumeTimeReached { .. })
652 && let Some(w) = self.shell.window()
653 {
654 w.request_redraw();
655 }
656 }
657
658 fn window_event(
659 &mut self,
660 event_loop: &ActiveEventLoop,
661 window_id: WindowId,
662 event: WindowEvent,
663 ) {
664 if self.shell.window().is_none_or(|w| w.id() != window_id) {
665 return;
666 }
667 match event {
668 WindowEvent::CloseRequested => event_loop.exit(),
669 WindowEvent::Resized(size) => self.shell.resized(size.width, size.height),
670 WindowEvent::ScaleFactorChanged { .. } => {
671 if let Some(w) = self.shell.window() {
672 w.request_redraw();
673 }
674 }
675 WindowEvent::CursorMoved { position, .. } => {
676 let scale = self.shell.window().map_or(1.0, |w| w.scale_factor());
677 self.cursor = Point::new(position.x / scale, position.y / scale);
678 }
679 WindowEvent::MouseWheel { delta, .. } => {
680 let scale = self.shell.window().map_or(1.0, |w| w.scale_factor());
681 let (dx, dy) = wheel_deltas(delta, scale);
682 if let Some(frame) = &self.last_frame {
683 let y_target = (dy.abs() > 1e-3)
684 .then(|| frame.scrollable_y_at(self.cursor))
685 .flatten();
686 let x_target = (dx.abs() > 1e-3)
687 .then(|| frame.scrollable_x_at(self.cursor))
688 .flatten();
689 #[expect(clippy::cast_possible_truncation, reason = "scroll deltas fit in f32")]
690 {
691 if let Some(id) = y_target {
692 self.state.scroll_by(id, -dy as f32);
693 }
694 if let Some(id) = x_target {
695 self.state.scroll_by_x(id, -dx as f32);
696 }
697 }
698 if (y_target.is_some() || x_target.is_some())
699 && let Some(w) = self.shell.window()
700 {
701 w.request_redraw();
702 }
703 }
704 }
705 WindowEvent::RedrawRequested => self.redraw(event_loop),
706 _ => {}
707 }
708 }
709}
710
711enum RunnerEvent {
717 App(Box<dyn std::any::Any + Send>),
718 #[cfg(not(target_arch = "wasm32"))]
719 Access(accesskit_winit::Event),
720}
721
722#[cfg(not(target_arch = "wasm32"))]
723impl From<accesskit_winit::Event> for RunnerEvent {
724 fn from(event: accesskit_winit::Event) -> Self {
725 Self::Access(event)
726 }
727}
728
729pub fn run_app<A: App + 'static>(mut app: A, options: WindowOptions) -> Result<(), ShellError>
735where
736 A::Msg: Send,
737{
738 let event_loop = EventLoop::<RunnerEvent>::with_user_event()
739 .build()
740 .map_err(ShellError::EventLoop)?;
741 #[cfg(not(target_arch = "wasm32"))]
742 let access_proxy = event_loop.create_proxy();
743 let proxy = event_loop.create_proxy();
744 app.init(fenestra_core::Proxy::new(move |msg: A::Msg| {
745 let _ = proxy.send_event(RunnerEvent::App(Box::new(msg)));
747 }));
748 let background = app.theme().bg;
749 let mut fonts = Fonts::with_system();
750 for (role, data) in &options.fonts {
751 fonts.register(*role, data.clone());
752 }
753 #[cfg(target_arch = "wasm32")]
754 let state = live_state();
755 #[cfg(not(target_arch = "wasm32"))]
756 let mut state = live_state();
757 #[cfg(not(target_arch = "wasm32"))]
758 state.set_clipboard(Box::new(crate::OsClipboard::default()));
759 let runner = AppRunner {
760 shell: WindowShell::new(options, background),
761 app,
762 fonts,
763 state,
764 cursor: Point::ORIGIN,
765 started: Instant::now(),
766 last: None,
767 dirty: true,
768 cached_scene: None,
769 modifiers: winit::keyboard::ModifiersState::empty(),
770 #[cfg(not(target_arch = "wasm32"))]
771 adapter: None,
772 #[cfg(not(target_arch = "wasm32"))]
773 proxy: access_proxy,
774 #[cfg(not(target_arch = "wasm32"))]
775 secondary: std::collections::HashMap::new(),
776 };
777 #[cfg(not(target_arch = "wasm32"))]
778 {
779 let mut runner = runner;
780 event_loop
781 .run_app(&mut runner)
782 .map_err(ShellError::EventLoop)
783 }
784 #[cfg(target_arch = "wasm32")]
785 {
786 use winit::platform::web::EventLoopExtWebSys;
787 event_loop.spawn_app(runner);
789 Ok(())
790 }
791}
792
793struct AppRunner<A: App> {
794 shell: WindowShell,
795 app: A,
796 fonts: Fonts,
797 state: FrameState,
798 cursor: Point,
799 started: Instant,
800 last: Option<(Element<A::Msg>, fenestra_core::Frame)>,
802 dirty: bool,
806 cached_scene: Option<(Scene, (f64, f64, f64))>,
808 modifiers: winit::keyboard::ModifiersState,
809 #[cfg(not(target_arch = "wasm32"))]
811 adapter: Option<accesskit_winit::Adapter>,
812 #[cfg(not(target_arch = "wasm32"))]
814 proxy: EventLoopProxy<RunnerEvent>,
815 #[cfg(not(target_arch = "wasm32"))]
818 secondary: std::collections::HashMap<String, SecondaryWindow<A>>,
819}
820
821#[cfg(not(target_arch = "wasm32"))]
824struct SecondaryWindow<A: App> {
825 shell: WindowShell,
826 state: FrameState,
827 cursor: Point,
828 last: Option<(Element<A::Msg>, fenestra_core::Frame)>,
829 on_close: A::Msg,
830 title: String,
831 adapter: Option<accesskit_winit::Adapter>,
832}
833
834impl<A: App> AppRunner<A> {
835 fn redraw(&mut self, event_loop: &ActiveEventLoop) {
836 self.shell.pump();
837 let Some((lw, lh, scale)) = self.shell.logical_size() else {
838 return;
839 };
840 if !self.dirty
843 && let Some((scene, key)) = &self.cached_scene
844 && *key == (lw, lh, scale)
845 {
846 self.shell.present(scene);
847 return;
848 }
849 let theme = self.app.theme();
850 self.shell.background = theme.bg;
851 self.state.tick(self.started.elapsed().as_secs_f64());
852 #[expect(clippy::cast_possible_truncation, reason = "window sizes fit in f32")]
853 let logical = (lw as f32, lh as f32);
854 let view = self.app.view_at(fenestra_core::MAIN_WINDOW, logical);
855 let frame = build_frame(
856 &view,
857 &theme,
858 &mut self.fonts,
859 &mut self.state,
860 logical,
861 scale,
862 );
863 let scene = frame.paint(&mut self.fonts, &mut self.state);
866 self.shell.present(&scene);
867 self.cached_scene = Some((scene, (lw, lh, scale)));
870 self.dirty = frame.animating;
871 if refresh_hover(&view, &frame, &mut self.state)
874 && let Some(w) = self.shell.window()
875 {
876 self.dirty = true;
877 w.request_redraw();
878 }
879 if frame.animating {
880 #[cfg(not(target_arch = "wasm32"))]
881 event_loop.set_control_flow(ControlFlow::WaitUntil(
882 Instant::now() + Duration::from_millis(16),
883 ));
884 #[cfg(target_arch = "wasm32")]
886 if let Some(w) = self.shell.window() {
887 w.request_redraw();
888 }
889 } else {
890 #[cfg(not(target_arch = "wasm32"))]
891 let secondary_animating = self
892 .secondary
893 .values()
894 .any(|b| b.last.as_ref().is_some_and(|(_, f)| f.animating));
895 #[cfg(target_arch = "wasm32")]
896 let secondary_animating = false;
897 if !secondary_animating {
898 event_loop.set_control_flow(ControlFlow::Wait);
899 }
900 }
901 self.last = Some((view, frame));
902 if let Some(caret) = self.state.ime_caret()
904 && let Some(w) = self.shell.window()
905 {
906 w.set_ime_cursor_area(
907 winit::dpi::LogicalPosition::new(caret.x0, caret.y0),
908 winit::dpi::LogicalSize::new(1.0, caret.height()),
909 );
910 }
911 #[cfg(not(target_arch = "wasm32"))]
912 self.push_access_tree();
913 }
914
915 #[cfg(not(target_arch = "wasm32"))]
918 fn push_access_tree(&mut self) {
919 let scale = self.shell.window().map_or(1.0, |w| w.scale_factor());
920 let focus = self.state.focused();
921 if let Some(adapter) = &mut self.adapter
922 && let Some((_, frame)) = &self.last
923 {
924 adapter.update_if_active(|| crate::access::tree_update(frame, focus, scale));
925 }
926 }
927
928 fn input(&mut self, event: InputEvent) -> bool {
929 let Some((view, frame)) = &self.last else {
930 return false;
931 };
932 let result = dispatch(view, frame, &mut self.state, &mut self.fonts, event);
933 if let Some(cursor) = result.cursor
934 && let Some(w) = self.shell.window()
935 {
936 w.set_cursor(winit::window::Cursor::Icon(map_cursor(cursor)));
937 }
938 let had_msgs = !result.msgs.is_empty();
939 for msg in result.msgs {
940 self.app.update(msg);
941 }
942 if result.redraw || had_msgs {
943 self.dirty = true;
944 if let Some(w) = self.shell.window() {
945 w.request_redraw();
946 }
947 }
948 had_msgs
949 }
950
951 fn input_main(&mut self, event_loop: &ActiveEventLoop, event: InputEvent) {
954 if self.input(event) {
955 self.after_update(event_loop);
956 }
957 }
958
959 #[cfg(not(target_arch = "wasm32"))]
962 fn secondary_input_main(&mut self, key: &str, event_loop: &ActiveEventLoop, event: InputEvent) {
963 if self.secondary_input(key, event) {
964 self.after_update(event_loop);
965 }
966 }
967
968 fn after_update(&mut self, event_loop: &ActiveEventLoop) {
971 self.dirty = true;
972 #[cfg(not(target_arch = "wasm32"))]
973 self.reconcile_windows(event_loop);
974 #[cfg(target_arch = "wasm32")]
975 let _ = event_loop;
976 if let Some(w) = self.shell.window() {
977 w.request_redraw();
978 }
979 #[cfg(not(target_arch = "wasm32"))]
980 for bundle in self.secondary.values() {
981 if let Some(w) = bundle.shell.window() {
982 w.request_redraw();
983 }
984 }
985 }
986
987 #[cfg(not(target_arch = "wasm32"))]
990 fn reconcile_windows(&mut self, event_loop: &ActiveEventLoop) {
991 let desired = self.app.windows();
992 self.secondary
993 .retain(|key, _| desired.iter().any(|d| &d.key == key));
994 for desc in desired {
995 match self.secondary.get_mut(&desc.key) {
996 Some(bundle) => {
997 bundle.on_close = desc.on_close;
998 if bundle.title != desc.title {
999 bundle.title.clone_from(&desc.title);
1000 if let Some(w) = bundle.shell.window() {
1001 w.set_title(&desc.title);
1002 }
1003 }
1004 }
1005 None => {
1006 let mut shell = WindowShell::new(
1007 WindowOptions::titled(desc.title.clone())
1008 .with_size(desc.size.0, desc.size.1),
1009 self.shell.background,
1010 );
1011 let proxy = self.proxy.clone();
1012 let mut adapter = None;
1013 shell.resumed_with(event_loop, |el, window| {
1014 adapter = Some(accesskit_winit::Adapter::with_event_loop_proxy(
1015 el, window, proxy,
1016 ));
1017 });
1018 if let Some(w) = shell.window() {
1019 w.set_ime_allowed(true);
1020 w.request_redraw();
1021 }
1022 let mut state = live_state();
1023 state.set_clipboard(Box::new(crate::OsClipboard::default()));
1024 self.secondary.insert(
1025 desc.key.clone(),
1026 SecondaryWindow {
1027 shell,
1028 state,
1029 cursor: Point::ORIGIN,
1030 last: None,
1031 on_close: desc.on_close,
1032 title: desc.title,
1033 adapter,
1034 },
1035 );
1036 }
1037 }
1038 }
1039 }
1040
1041 #[cfg(not(target_arch = "wasm32"))]
1044 fn secondary_redraw(&mut self, key: &str, event_loop: &ActiveEventLoop) {
1045 let theme = self.app.theme_for(key);
1046 let now = self.started.elapsed().as_secs_f64();
1047 let Some(bundle) = self.secondary.get_mut(key) else {
1048 return;
1049 };
1050 bundle.shell.pump();
1051 let Some((lw, lh, scale)) = bundle.shell.logical_size() else {
1052 return;
1053 };
1054 bundle.shell.background = theme.bg;
1055 bundle.state.tick(now);
1056 #[expect(clippy::cast_possible_truncation, reason = "window sizes fit in f32")]
1057 let logical = (lw as f32, lh as f32);
1058 let view = self.app.view_at(key, logical);
1059 let frame = build_frame(
1060 &view,
1061 &theme,
1062 &mut self.fonts,
1063 &mut bundle.state,
1064 logical,
1065 scale,
1066 );
1067 let scene = frame.paint(&mut self.fonts, &mut bundle.state);
1070 bundle.shell.present(&scene);
1071 if refresh_hover(&view, &frame, &mut bundle.state)
1072 && let Some(w) = bundle.shell.window()
1073 {
1074 w.request_redraw();
1075 }
1076 if frame.animating {
1077 event_loop.set_control_flow(ControlFlow::WaitUntil(
1078 Instant::now() + Duration::from_millis(16),
1079 ));
1080 }
1081 if let Some(caret) = bundle.state.ime_caret()
1082 && let Some(w) = bundle.shell.window()
1083 {
1084 w.set_ime_cursor_area(
1085 winit::dpi::LogicalPosition::new(caret.x0, caret.y0),
1086 winit::dpi::LogicalSize::new(1.0, caret.height()),
1087 );
1088 }
1089 bundle.last = Some((view, frame));
1090 let focus = bundle.state.focused();
1091 if let Some(adapter) = &mut bundle.adapter
1092 && let Some((_, frame)) = &bundle.last
1093 {
1094 adapter.update_if_active(|| crate::access::tree_update(frame, focus, scale));
1095 }
1096 }
1097
1098 #[cfg(not(target_arch = "wasm32"))]
1101 fn secondary_window_event(
1102 &mut self,
1103 key: &str,
1104 event_loop: &ActiveEventLoop,
1105 event: WindowEvent,
1106 ) {
1107 if let Some(bundle) = self.secondary.get_mut(key)
1108 && let Some(window) = bundle.shell.window()
1109 && let Some(adapter) = &mut bundle.adapter
1110 {
1111 adapter.process_event(window, &event);
1112 }
1113 match event {
1114 WindowEvent::CloseRequested => {
1115 if let Some(msg) = self.secondary.get(key).map(|b| b.on_close.clone()) {
1116 self.app.update(msg);
1117 self.after_update(event_loop);
1118 }
1119 }
1120 WindowEvent::Resized(size) => {
1121 if let Some(bundle) = self.secondary.get_mut(key) {
1122 bundle.shell.resized(size.width, size.height);
1123 }
1124 }
1125 WindowEvent::ScaleFactorChanged { .. } | WindowEvent::Occluded(false) => {
1126 if let Some(w) = self.secondary.get(key).and_then(|b| b.shell.window()) {
1127 w.request_redraw();
1128 }
1129 }
1130 WindowEvent::ModifiersChanged(mods) => {
1131 self.modifiers = mods.state();
1132 let m = self.modifiers;
1133 self.secondary_input_main(
1134 key,
1135 event_loop,
1136 InputEvent::Modifiers {
1137 shift: m.shift_key(),
1138 ctrl: m.control_key(),
1139 alt: m.alt_key(),
1140 meta: m.super_key(),
1141 },
1142 );
1143 }
1144 WindowEvent::DroppedFile(path) => {
1145 self.secondary_input_main(key, event_loop, InputEvent::FileDrop(path));
1146 }
1147 WindowEvent::CursorLeft { .. } => {
1148 self.secondary_input_main(key, event_loop, InputEvent::PointerLeave);
1149 }
1150 WindowEvent::CursorMoved { position, .. } => {
1151 let Some(bundle) = self.secondary.get_mut(key) else {
1152 return;
1153 };
1154 let scale = bundle.shell.window().map_or(1.0, |w| w.scale_factor());
1155 bundle.cursor = Point::new(position.x / scale, position.y / scale);
1156 #[expect(clippy::cast_possible_truncation, reason = "positions fit in f32")]
1157 let (x, y) = (bundle.cursor.x as f32, bundle.cursor.y as f32);
1158 self.secondary_input_main(key, event_loop, InputEvent::PointerMove { x, y });
1159 }
1160 WindowEvent::MouseInput {
1161 state,
1162 button: winit::event::MouseButton::Left,
1163 ..
1164 } => {
1165 self.secondary_input_main(
1166 key,
1167 event_loop,
1168 match state {
1169 winit::event::ElementState::Pressed => InputEvent::PointerDown,
1170 winit::event::ElementState::Released => InputEvent::PointerUp,
1171 },
1172 );
1173 }
1174 WindowEvent::MouseInput {
1175 state,
1176 button: winit::event::MouseButton::Right,
1177 ..
1178 } => {
1179 self.secondary_input_main(
1180 key,
1181 event_loop,
1182 match state {
1183 winit::event::ElementState::Pressed => InputEvent::RightDown,
1184 winit::event::ElementState::Released => InputEvent::RightUp,
1185 },
1186 );
1187 }
1188 WindowEvent::MouseWheel { delta, .. } => {
1189 let scale = self
1190 .secondary
1191 .get(key)
1192 .and_then(|b| b.shell.window())
1193 .map_or(1.0, |w| w.scale_factor());
1194 let (dx, dy) = wheel_deltas(delta, scale);
1195 #[expect(clippy::cast_possible_truncation, reason = "deltas fit in f32")]
1196 self.secondary_input_main(
1197 key,
1198 event_loop,
1199 InputEvent::Wheel {
1200 dx: dx as f32,
1201 dy: dy as f32,
1202 },
1203 );
1204 }
1205 WindowEvent::KeyboardInput { event, .. }
1206 if event.state == winit::event::ElementState::Pressed =>
1207 {
1208 let mods = self.modifiers;
1209 let printable = !mods.control_key()
1210 && !mods.super_key()
1211 && event
1212 .text
1213 .as_ref()
1214 .is_some_and(|t| !t.is_empty() && t.chars().all(|c| !c.is_control()));
1215 if printable {
1216 if let Some(t) = &event.text {
1217 self.secondary_input_main(key, event_loop, InputEvent::Text(t.to_string()));
1218 }
1219 } else if let Some(input) = map_key(&event, mods) {
1220 self.secondary_input_main(key, event_loop, input);
1221 }
1222 }
1223 WindowEvent::Ime(ime) => match ime {
1224 winit::event::Ime::Preedit(text, cursor) => {
1225 self.secondary_input_main(
1226 key,
1227 event_loop,
1228 InputEvent::ImePreedit { text, cursor },
1229 );
1230 }
1231 winit::event::Ime::Commit(text) => {
1232 self.secondary_input_main(key, event_loop, InputEvent::Text(text));
1233 }
1234 winit::event::Ime::Enabled | winit::event::Ime::Disabled => {}
1235 },
1236 WindowEvent::RedrawRequested => self.secondary_redraw(key, event_loop),
1237 _ => {}
1238 }
1239 }
1240
1241 #[cfg(not(target_arch = "wasm32"))]
1244 fn secondary_input(&mut self, key: &str, event: InputEvent) -> bool {
1245 let Some(bundle) = self.secondary.get_mut(key) else {
1246 return false;
1247 };
1248 let Some((view, frame)) = &bundle.last else {
1249 return false;
1250 };
1251 let result = dispatch(view, frame, &mut bundle.state, &mut self.fonts, event);
1252 if let Some(cursor) = result.cursor
1253 && let Some(w) = bundle.shell.window()
1254 {
1255 w.set_cursor(winit::window::Cursor::Icon(map_cursor(cursor)));
1256 }
1257 let had_msgs = !result.msgs.is_empty();
1258 if (result.redraw || had_msgs)
1259 && let Some(w) = bundle.shell.window()
1260 {
1261 w.request_redraw();
1262 }
1263 let msgs = result.msgs;
1264 for msg in msgs {
1265 self.app.update(msg);
1266 }
1267 had_msgs
1268 }
1269}
1270
1271pub(crate) fn map_cursor(cursor: fenestra_core::Cursor) -> winit::window::CursorIcon {
1272 match cursor {
1273 fenestra_core::Cursor::Default => winit::window::CursorIcon::Default,
1274 fenestra_core::Cursor::Pointer => winit::window::CursorIcon::Pointer,
1275 fenestra_core::Cursor::Text => winit::window::CursorIcon::Text,
1276 fenestra_core::Cursor::NotAllowed => winit::window::CursorIcon::NotAllowed,
1277 }
1278}
1279
1280pub(crate) fn map_key(
1282 event: &winit::event::KeyEvent,
1283 mods: winit::keyboard::ModifiersState,
1284) -> Option<InputEvent> {
1285 use winit::keyboard::{Key as WKey, NamedKey};
1286 let key = match &event.logical_key {
1287 WKey::Named(NamedKey::Tab) => {
1288 return Some(if mods.shift_key() {
1289 InputEvent::ShiftTab
1290 } else {
1291 InputEvent::Tab
1292 });
1293 }
1294 WKey::Named(named) => match named {
1295 NamedKey::Enter => Key::Enter,
1296 NamedKey::Space => Key::Space,
1297 NamedKey::Escape => Key::Escape,
1298 NamedKey::ArrowLeft => Key::ArrowLeft,
1299 NamedKey::ArrowRight => Key::ArrowRight,
1300 NamedKey::ArrowUp => Key::ArrowUp,
1301 NamedKey::ArrowDown => Key::ArrowDown,
1302 NamedKey::Home => Key::Home,
1303 NamedKey::End => Key::End,
1304 NamedKey::Backspace => Key::Backspace,
1305 NamedKey::Delete => Key::Delete,
1306 NamedKey::PageUp => Key::PageUp,
1307 NamedKey::PageDown => Key::PageDown,
1308 _ => return None,
1309 },
1310 WKey::Character(s) => Key::Char(s.chars().next()?),
1311 _ => return None,
1312 };
1313 Some(InputEvent::Key(KeyInput {
1314 key,
1315 shift: mods.shift_key(),
1316 ctrl: mods.control_key(),
1317 alt: mods.alt_key(),
1318 meta: mods.super_key(),
1319 }))
1320}
1321
1322impl<A: App> ApplicationHandler<RunnerEvent> for AppRunner<A> {
1323 #[cfg(not(target_arch = "wasm32"))]
1324 fn resumed(&mut self, event_loop: &ActiveEventLoop) {
1325 self.dirty = true;
1328 let adapter = &mut self.adapter;
1329 let proxy = self.proxy.clone();
1330 self.shell.resumed_with(event_loop, |el, window| {
1331 if adapter.is_none() {
1333 *adapter = Some(accesskit_winit::Adapter::with_event_loop_proxy(
1334 el, window, proxy,
1335 ));
1336 }
1337 });
1338 if let Some(w) = self.shell.window() {
1339 w.set_ime_allowed(true);
1340 }
1341 for bundle in self.secondary.values_mut() {
1342 bundle.shell.resumed(event_loop);
1343 }
1344 self.reconcile_windows(event_loop);
1345 }
1346
1347 #[cfg(target_arch = "wasm32")]
1348 fn resumed(&mut self, event_loop: &ActiveEventLoop) {
1349 self.dirty = true;
1350 self.shell.resumed(event_loop);
1351 if let Some(w) = self.shell.window() {
1352 w.set_ime_allowed(true);
1353 }
1354 }
1355
1356 fn user_event(&mut self, event_loop: &ActiveEventLoop, event: RunnerEvent) {
1357 match event {
1358 RunnerEvent::App(msg) => {
1359 if let Ok(msg) = msg.downcast::<A::Msg>() {
1360 self.app.update(*msg);
1361 self.after_update(event_loop);
1362 }
1363 }
1364 #[cfg(not(target_arch = "wasm32"))]
1365 RunnerEvent::Access(ev) => {
1366 let is_main = self.shell.window().is_some_and(|w| w.id() == ev.window_id);
1369 let skey = (!is_main)
1370 .then(|| {
1371 self.secondary
1372 .iter()
1373 .find(|(_, b)| b.shell.window().is_some_and(|w| w.id() == ev.window_id))
1374 .map(|(k, _)| k.clone())
1375 })
1376 .flatten();
1377 if !is_main && skey.is_none() {
1378 return;
1379 }
1380 match ev.window_event {
1381 accesskit_winit::WindowEvent::InitialTreeRequested => match &skey {
1382 None => {
1383 if self.last.is_some() {
1384 self.push_access_tree();
1385 } else if let Some(w) = self.shell.window() {
1386 w.request_redraw();
1387 }
1388 }
1389 Some(key) => {
1390 if let Some(bundle) = self.secondary.get_mut(key) {
1391 let scale = bundle.shell.window().map_or(1.0, |w| w.scale_factor());
1392 let focus = bundle.state.focused();
1393 if let Some((_, frame)) = &bundle.last {
1394 if let Some(adapter) = &mut bundle.adapter {
1395 adapter.update_if_active(|| {
1396 crate::access::tree_update(frame, focus, scale)
1397 });
1398 }
1399 } else if let Some(w) = bundle.shell.window() {
1400 w.request_redraw();
1401 }
1402 }
1403 }
1404 },
1405 accesskit_winit::WindowEvent::ActionRequested(req) => {
1406 let id = fenestra_core::WidgetId(req.target_node.0);
1407 match req.action {
1408 accesskit::Action::Click => {
1409 let msg = match &skey {
1410 None => self.last.as_ref().and_then(|(view, frame)| {
1411 fenestra_core::click_msg_of(view, frame, &self.state, id)
1412 }),
1413 Some(key) => self.secondary.get(key).and_then(|bundle| {
1414 bundle.last.as_ref().and_then(|(view, frame)| {
1415 fenestra_core::click_msg_of(
1416 view,
1417 frame,
1418 &bundle.state,
1419 id,
1420 )
1421 })
1422 }),
1423 };
1424 if let Some(msg) = msg {
1425 self.app.update(msg);
1426 self.after_update(event_loop);
1427 }
1428 }
1429 accesskit::Action::Focus => match &skey {
1430 None => {
1431 self.state.set_focus(Some(id));
1432 self.dirty = true;
1433 if let Some(w) = self.shell.window() {
1434 w.request_redraw();
1435 }
1436 }
1437 Some(key) => {
1438 if let Some(bundle) = self.secondary.get_mut(key) {
1439 bundle.state.set_focus(Some(id));
1440 if let Some(w) = bundle.shell.window() {
1441 w.request_redraw();
1442 }
1443 }
1444 }
1445 },
1446 _ => {}
1447 }
1448 }
1449 accesskit_winit::WindowEvent::AccessibilityDeactivated => {}
1450 }
1451 }
1452 }
1453 }
1454
1455 fn suspended(&mut self, _event_loop: &ActiveEventLoop) {
1456 self.shell.suspended();
1457 #[cfg(not(target_arch = "wasm32"))]
1458 for bundle in self.secondary.values_mut() {
1459 bundle.shell.suspended();
1460 }
1461 }
1462
1463 fn new_events(&mut self, _event_loop: &ActiveEventLoop, cause: StartCause) {
1464 if !matches!(cause, StartCause::ResumeTimeReached { .. }) {
1465 return;
1466 }
1467 if let Some(w) = self.shell.window() {
1468 w.request_redraw();
1469 }
1470 #[cfg(not(target_arch = "wasm32"))]
1471 for bundle in self.secondary.values() {
1472 if let Some(w) = bundle.shell.window() {
1473 w.request_redraw();
1474 }
1475 }
1476 }
1477
1478 fn window_event(
1479 &mut self,
1480 event_loop: &ActiveEventLoop,
1481 window_id: WindowId,
1482 event: WindowEvent,
1483 ) {
1484 if self.shell.window().is_none_or(|w| w.id() != window_id) {
1485 #[cfg(not(target_arch = "wasm32"))]
1486 if let Some(key) = self
1487 .secondary
1488 .iter()
1489 .find(|(_, b)| b.shell.window().is_some_and(|w| w.id() == window_id))
1490 .map(|(k, _)| k.clone())
1491 {
1492 self.secondary_window_event(&key, event_loop, event);
1493 }
1494 return;
1495 }
1496 #[cfg(not(target_arch = "wasm32"))]
1497 if let Some(adapter) = &mut self.adapter
1498 && let Some(window) = self.shell.window()
1499 {
1500 adapter.process_event(window, &event);
1501 }
1502 match event {
1503 WindowEvent::CloseRequested => event_loop.exit(),
1504 WindowEvent::Resized(size) => {
1505 self.dirty = true;
1508 self.shell.resized(size.width, size.height);
1509 }
1510 WindowEvent::ScaleFactorChanged { .. } => {
1511 self.dirty = true;
1512 if let Some(w) = self.shell.window() {
1513 w.request_redraw();
1514 }
1515 }
1516 WindowEvent::Focused(true) => {
1517 #[cfg(not(target_arch = "wasm32"))]
1520 {
1521 let reduce = crate::reduce_motion::os_reduce_motion();
1522 if reduce != self.state.reduced_motion {
1523 self.state.reduced_motion = reduce;
1524 self.dirty = true;
1525 if let Some(w) = self.shell.window() {
1526 w.request_redraw();
1527 }
1528 }
1529 }
1530 }
1531 WindowEvent::ModifiersChanged(mods) => {
1532 self.modifiers = mods.state();
1533 let m = self.modifiers;
1534 self.input_main(
1535 event_loop,
1536 InputEvent::Modifiers {
1537 shift: m.shift_key(),
1538 ctrl: m.control_key(),
1539 alt: m.alt_key(),
1540 meta: m.super_key(),
1541 },
1542 );
1543 }
1544 WindowEvent::Occluded(occluded) => {
1545 if !occluded && let Some(w) = self.shell.window() {
1546 w.request_redraw();
1547 }
1548 }
1549 WindowEvent::DroppedFile(path) => {
1550 self.input_main(event_loop, InputEvent::FileDrop(path))
1551 }
1552 WindowEvent::CursorLeft { .. } => self.input_main(event_loop, InputEvent::PointerLeave),
1553 WindowEvent::CursorMoved { position, .. } => {
1554 let scale = self.shell.window().map_or(1.0, |w| w.scale_factor());
1555 self.cursor = Point::new(position.x / scale, position.y / scale);
1556 #[expect(clippy::cast_possible_truncation, reason = "positions fit in f32")]
1557 self.input_main(
1558 event_loop,
1559 InputEvent::PointerMove {
1560 x: self.cursor.x as f32,
1561 y: self.cursor.y as f32,
1562 },
1563 );
1564 }
1565 WindowEvent::MouseInput {
1566 state,
1567 button: winit::event::MouseButton::Left,
1568 ..
1569 } => {
1570 self.input_main(
1571 event_loop,
1572 match state {
1573 winit::event::ElementState::Pressed => InputEvent::PointerDown,
1574 winit::event::ElementState::Released => InputEvent::PointerUp,
1575 },
1576 );
1577 }
1578 WindowEvent::MouseInput {
1579 state,
1580 button: winit::event::MouseButton::Right,
1581 ..
1582 } => {
1583 self.input_main(
1584 event_loop,
1585 match state {
1586 winit::event::ElementState::Pressed => InputEvent::RightDown,
1587 winit::event::ElementState::Released => InputEvent::RightUp,
1588 },
1589 );
1590 }
1591 WindowEvent::MouseWheel { delta, .. } => {
1592 let scale = self.shell.window().map_or(1.0, |w| w.scale_factor());
1593 let (dx, dy) = wheel_deltas(delta, scale);
1594 #[expect(clippy::cast_possible_truncation, reason = "deltas fit in f32")]
1595 self.input_main(
1596 event_loop,
1597 InputEvent::Wheel {
1598 dx: dx as f32,
1599 dy: dy as f32,
1600 },
1601 );
1602 }
1603 WindowEvent::KeyboardInput { event, .. }
1604 if event.state == winit::event::ElementState::Pressed =>
1605 {
1606 {
1607 let mods = self.modifiers;
1608 let printable = !mods.control_key()
1611 && !mods.super_key()
1612 && event
1613 .text
1614 .as_ref()
1615 .is_some_and(|t| !t.is_empty() && t.chars().all(|c| !c.is_control()));
1616 if printable {
1617 if let Some(t) = &event.text {
1618 self.input_main(event_loop, InputEvent::Text(t.to_string()));
1619 }
1620 } else if let Some(input) = map_key(&event, mods) {
1621 self.input_main(event_loop, input);
1622 }
1623 }
1624 }
1625 WindowEvent::Ime(ime) => match ime {
1626 winit::event::Ime::Preedit(text, cursor) => {
1627 self.input_main(event_loop, InputEvent::ImePreedit { text, cursor });
1628 }
1629 winit::event::Ime::Commit(text) => {
1630 self.input_main(event_loop, InputEvent::Text(text));
1631 }
1632 winit::event::Ime::Enabled | winit::event::Ime::Disabled => {}
1633 },
1634 WindowEvent::RedrawRequested => self.redraw(event_loop),
1635 _ => {}
1636 }
1637 }
1638}