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"))]
551pub fn run_static(
552 options: WindowOptions,
553 theme: Theme,
554 view: impl Fn(&Theme) -> Element<()> + 'static,
555) -> Result<(), ShellError> {
556 let event_loop = EventLoop::new().map_err(ShellError::EventLoop)?;
557 let background = theme.bg;
558 let mut fonts = Fonts::with_system();
559 for (role, data) in &options.fonts {
560 fonts.register(*role, data.clone());
561 }
562 let mut app = StaticApp {
563 shell: WindowShell::new(options, background),
564 theme,
565 fonts,
566 state: FrameState::new(),
567 view: Box::new(view),
568 cursor: Point::ORIGIN,
569 started: Instant::now(),
570 last_frame: None,
571 };
572 event_loop.run_app(&mut app).map_err(ShellError::EventLoop)
573}
574
575#[cfg(not(target_arch = "wasm32"))]
576struct StaticApp {
577 shell: WindowShell,
578 theme: Theme,
579 fonts: Fonts,
580 state: FrameState,
581 view: ViewFn,
582 cursor: Point,
584 started: Instant,
585 last_frame: Option<fenestra_core::Frame>,
587}
588
589#[cfg(not(target_arch = "wasm32"))]
590impl StaticApp {
591 fn redraw(&mut self, event_loop: &ActiveEventLoop) {
592 let Some((lw, lh, scale)) = self.shell.logical_size() else {
593 return;
594 };
595 self.state.tick(self.started.elapsed().as_secs_f64());
596 let el = (self.view)(&self.theme);
597 #[expect(clippy::cast_possible_truncation, reason = "window sizes fit in f32")]
598 let frame = build_frame(
599 &el,
600 &self.theme,
601 &mut self.fonts,
602 &mut self.state,
603 (lw as f32, lh as f32),
604 scale,
605 );
606 let scene = frame.paint(&mut self.fonts, &mut self.state);
612 self.shell.present(&scene);
613 if frame.animating {
614 event_loop.set_control_flow(ControlFlow::WaitUntil(
615 Instant::now() + Duration::from_millis(16),
616 ));
617 } else {
618 event_loop.set_control_flow(ControlFlow::Wait);
619 }
620 self.last_frame = Some(frame);
621 }
622}
623
624#[cfg(not(target_arch = "wasm32"))]
625impl ApplicationHandler for StaticApp {
626 fn resumed(&mut self, event_loop: &ActiveEventLoop) {
627 self.shell.resumed(event_loop);
628 }
629
630 fn suspended(&mut self, _event_loop: &ActiveEventLoop) {
631 self.shell.suspended();
632 }
633
634 fn new_events(&mut self, _event_loop: &ActiveEventLoop, cause: StartCause) {
635 if matches!(cause, StartCause::ResumeTimeReached { .. })
636 && let Some(w) = self.shell.window()
637 {
638 w.request_redraw();
639 }
640 }
641
642 fn window_event(
643 &mut self,
644 event_loop: &ActiveEventLoop,
645 window_id: WindowId,
646 event: WindowEvent,
647 ) {
648 if self.shell.window().is_none_or(|w| w.id() != window_id) {
649 return;
650 }
651 match event {
652 WindowEvent::CloseRequested => event_loop.exit(),
653 WindowEvent::Resized(size) => self.shell.resized(size.width, size.height),
654 WindowEvent::ScaleFactorChanged { .. } => {
655 if let Some(w) = self.shell.window() {
656 w.request_redraw();
657 }
658 }
659 WindowEvent::CursorMoved { position, .. } => {
660 let scale = self.shell.window().map_or(1.0, |w| w.scale_factor());
661 self.cursor = Point::new(position.x / scale, position.y / scale);
662 }
663 WindowEvent::MouseWheel { delta, .. } => {
664 let scale = self.shell.window().map_or(1.0, |w| w.scale_factor());
665 let (dx, dy) = wheel_deltas(delta, scale);
666 if let Some(frame) = &self.last_frame {
667 let y_target = (dy.abs() > 1e-3)
668 .then(|| frame.scrollable_y_at(self.cursor))
669 .flatten();
670 let x_target = (dx.abs() > 1e-3)
671 .then(|| frame.scrollable_x_at(self.cursor))
672 .flatten();
673 #[expect(clippy::cast_possible_truncation, reason = "scroll deltas fit in f32")]
674 {
675 if let Some(id) = y_target {
676 self.state.scroll_by(id, -dy as f32);
677 }
678 if let Some(id) = x_target {
679 self.state.scroll_by_x(id, -dx as f32);
680 }
681 }
682 if (y_target.is_some() || x_target.is_some())
683 && let Some(w) = self.shell.window()
684 {
685 w.request_redraw();
686 }
687 }
688 }
689 WindowEvent::RedrawRequested => self.redraw(event_loop),
690 _ => {}
691 }
692 }
693}
694
695enum RunnerEvent {
701 App(Box<dyn std::any::Any + Send>),
702 #[cfg(not(target_arch = "wasm32"))]
703 Access(accesskit_winit::Event),
704}
705
706#[cfg(not(target_arch = "wasm32"))]
707impl From<accesskit_winit::Event> for RunnerEvent {
708 fn from(event: accesskit_winit::Event) -> Self {
709 Self::Access(event)
710 }
711}
712
713pub fn run_app<A: App + 'static>(mut app: A, options: WindowOptions) -> Result<(), ShellError>
719where
720 A::Msg: Send,
721{
722 let event_loop = EventLoop::<RunnerEvent>::with_user_event()
723 .build()
724 .map_err(ShellError::EventLoop)?;
725 #[cfg(not(target_arch = "wasm32"))]
726 let access_proxy = event_loop.create_proxy();
727 let proxy = event_loop.create_proxy();
728 app.init(fenestra_core::Proxy::new(move |msg: A::Msg| {
729 let _ = proxy.send_event(RunnerEvent::App(Box::new(msg)));
731 }));
732 let background = app.theme().bg;
733 let mut fonts = Fonts::with_system();
734 for (role, data) in &options.fonts {
735 fonts.register(*role, data.clone());
736 }
737 #[cfg(target_arch = "wasm32")]
738 let state = FrameState::new();
739 #[cfg(not(target_arch = "wasm32"))]
740 let mut state = FrameState::new();
741 #[cfg(not(target_arch = "wasm32"))]
742 state.set_clipboard(Box::new(crate::OsClipboard::default()));
743 let runner = AppRunner {
744 shell: WindowShell::new(options, background),
745 app,
746 fonts,
747 state,
748 cursor: Point::ORIGIN,
749 started: Instant::now(),
750 last: None,
751 dirty: true,
752 cached_scene: None,
753 modifiers: winit::keyboard::ModifiersState::empty(),
754 #[cfg(not(target_arch = "wasm32"))]
755 adapter: None,
756 #[cfg(not(target_arch = "wasm32"))]
757 proxy: access_proxy,
758 #[cfg(not(target_arch = "wasm32"))]
759 secondary: std::collections::HashMap::new(),
760 };
761 #[cfg(not(target_arch = "wasm32"))]
762 {
763 let mut runner = runner;
764 event_loop
765 .run_app(&mut runner)
766 .map_err(ShellError::EventLoop)
767 }
768 #[cfg(target_arch = "wasm32")]
769 {
770 use winit::platform::web::EventLoopExtWebSys;
771 event_loop.spawn_app(runner);
773 Ok(())
774 }
775}
776
777struct AppRunner<A: App> {
778 shell: WindowShell,
779 app: A,
780 fonts: Fonts,
781 state: FrameState,
782 cursor: Point,
783 started: Instant,
784 last: Option<(Element<A::Msg>, fenestra_core::Frame)>,
786 dirty: bool,
790 cached_scene: Option<(Scene, (f64, f64, f64))>,
792 modifiers: winit::keyboard::ModifiersState,
793 #[cfg(not(target_arch = "wasm32"))]
795 adapter: Option<accesskit_winit::Adapter>,
796 #[cfg(not(target_arch = "wasm32"))]
798 proxy: EventLoopProxy<RunnerEvent>,
799 #[cfg(not(target_arch = "wasm32"))]
802 secondary: std::collections::HashMap<String, SecondaryWindow<A>>,
803}
804
805#[cfg(not(target_arch = "wasm32"))]
808struct SecondaryWindow<A: App> {
809 shell: WindowShell,
810 state: FrameState,
811 cursor: Point,
812 last: Option<(Element<A::Msg>, fenestra_core::Frame)>,
813 on_close: A::Msg,
814 title: String,
815 adapter: Option<accesskit_winit::Adapter>,
816}
817
818impl<A: App> AppRunner<A> {
819 fn redraw(&mut self, event_loop: &ActiveEventLoop) {
820 self.shell.pump();
821 let Some((lw, lh, scale)) = self.shell.logical_size() else {
822 return;
823 };
824 if !self.dirty
827 && let Some((scene, key)) = &self.cached_scene
828 && *key == (lw, lh, scale)
829 {
830 self.shell.present(scene);
831 return;
832 }
833 let theme = self.app.theme();
834 self.shell.background = theme.bg;
835 self.state.tick(self.started.elapsed().as_secs_f64());
836 #[expect(clippy::cast_possible_truncation, reason = "window sizes fit in f32")]
837 let logical = (lw as f32, lh as f32);
838 let view = self.app.view_at(fenestra_core::MAIN_WINDOW, logical);
839 let frame = build_frame(
840 &view,
841 &theme,
842 &mut self.fonts,
843 &mut self.state,
844 logical,
845 scale,
846 );
847 let scene = frame.paint(&mut self.fonts, &mut self.state);
850 self.shell.present(&scene);
851 self.cached_scene = Some((scene, (lw, lh, scale)));
854 self.dirty = frame.animating;
855 if refresh_hover(&view, &frame, &mut self.state)
858 && let Some(w) = self.shell.window()
859 {
860 self.dirty = true;
861 w.request_redraw();
862 }
863 if frame.animating {
864 #[cfg(not(target_arch = "wasm32"))]
865 event_loop.set_control_flow(ControlFlow::WaitUntil(
866 Instant::now() + Duration::from_millis(16),
867 ));
868 #[cfg(target_arch = "wasm32")]
870 if let Some(w) = self.shell.window() {
871 w.request_redraw();
872 }
873 } else {
874 #[cfg(not(target_arch = "wasm32"))]
875 let secondary_animating = self
876 .secondary
877 .values()
878 .any(|b| b.last.as_ref().is_some_and(|(_, f)| f.animating));
879 #[cfg(target_arch = "wasm32")]
880 let secondary_animating = false;
881 if !secondary_animating {
882 event_loop.set_control_flow(ControlFlow::Wait);
883 }
884 }
885 self.last = Some((view, frame));
886 if let Some(caret) = self.state.ime_caret()
888 && let Some(w) = self.shell.window()
889 {
890 w.set_ime_cursor_area(
891 winit::dpi::LogicalPosition::new(caret.x0, caret.y0),
892 winit::dpi::LogicalSize::new(1.0, caret.height()),
893 );
894 }
895 #[cfg(not(target_arch = "wasm32"))]
896 self.push_access_tree();
897 }
898
899 #[cfg(not(target_arch = "wasm32"))]
902 fn push_access_tree(&mut self) {
903 let scale = self.shell.window().map_or(1.0, |w| w.scale_factor());
904 let focus = self.state.focused();
905 if let Some(adapter) = &mut self.adapter
906 && let Some((_, frame)) = &self.last
907 {
908 adapter.update_if_active(|| crate::access::tree_update(frame, focus, scale));
909 }
910 }
911
912 fn input(&mut self, event: InputEvent) -> bool {
913 let Some((view, frame)) = &self.last else {
914 return false;
915 };
916 let result = dispatch(view, frame, &mut self.state, &mut self.fonts, event);
917 if let Some(cursor) = result.cursor
918 && let Some(w) = self.shell.window()
919 {
920 w.set_cursor(winit::window::Cursor::Icon(map_cursor(cursor)));
921 }
922 let had_msgs = !result.msgs.is_empty();
923 for msg in result.msgs {
924 self.app.update(msg);
925 }
926 if result.redraw || had_msgs {
927 self.dirty = true;
928 if let Some(w) = self.shell.window() {
929 w.request_redraw();
930 }
931 }
932 had_msgs
933 }
934
935 fn input_main(&mut self, event_loop: &ActiveEventLoop, event: InputEvent) {
938 if self.input(event) {
939 self.after_update(event_loop);
940 }
941 }
942
943 #[cfg(not(target_arch = "wasm32"))]
946 fn secondary_input_main(&mut self, key: &str, event_loop: &ActiveEventLoop, event: InputEvent) {
947 if self.secondary_input(key, event) {
948 self.after_update(event_loop);
949 }
950 }
951
952 fn after_update(&mut self, event_loop: &ActiveEventLoop) {
955 self.dirty = true;
956 #[cfg(not(target_arch = "wasm32"))]
957 self.reconcile_windows(event_loop);
958 #[cfg(target_arch = "wasm32")]
959 let _ = event_loop;
960 if let Some(w) = self.shell.window() {
961 w.request_redraw();
962 }
963 #[cfg(not(target_arch = "wasm32"))]
964 for bundle in self.secondary.values() {
965 if let Some(w) = bundle.shell.window() {
966 w.request_redraw();
967 }
968 }
969 }
970
971 #[cfg(not(target_arch = "wasm32"))]
974 fn reconcile_windows(&mut self, event_loop: &ActiveEventLoop) {
975 let desired = self.app.windows();
976 self.secondary
977 .retain(|key, _| desired.iter().any(|d| &d.key == key));
978 for desc in desired {
979 match self.secondary.get_mut(&desc.key) {
980 Some(bundle) => {
981 bundle.on_close = desc.on_close;
982 if bundle.title != desc.title {
983 bundle.title.clone_from(&desc.title);
984 if let Some(w) = bundle.shell.window() {
985 w.set_title(&desc.title);
986 }
987 }
988 }
989 None => {
990 let mut shell = WindowShell::new(
991 WindowOptions::titled(desc.title.clone())
992 .with_size(desc.size.0, desc.size.1),
993 self.shell.background,
994 );
995 let proxy = self.proxy.clone();
996 let mut adapter = None;
997 shell.resumed_with(event_loop, |el, window| {
998 adapter = Some(accesskit_winit::Adapter::with_event_loop_proxy(
999 el, window, proxy,
1000 ));
1001 });
1002 if let Some(w) = shell.window() {
1003 w.set_ime_allowed(true);
1004 w.request_redraw();
1005 }
1006 let mut state = FrameState::new();
1007 state.set_clipboard(Box::new(crate::OsClipboard::default()));
1008 self.secondary.insert(
1009 desc.key.clone(),
1010 SecondaryWindow {
1011 shell,
1012 state,
1013 cursor: Point::ORIGIN,
1014 last: None,
1015 on_close: desc.on_close,
1016 title: desc.title,
1017 adapter,
1018 },
1019 );
1020 }
1021 }
1022 }
1023 }
1024
1025 #[cfg(not(target_arch = "wasm32"))]
1028 fn secondary_redraw(&mut self, key: &str, event_loop: &ActiveEventLoop) {
1029 let theme = self.app.theme_for(key);
1030 let now = self.started.elapsed().as_secs_f64();
1031 let Some(bundle) = self.secondary.get_mut(key) else {
1032 return;
1033 };
1034 bundle.shell.pump();
1035 let Some((lw, lh, scale)) = bundle.shell.logical_size() else {
1036 return;
1037 };
1038 bundle.shell.background = theme.bg;
1039 bundle.state.tick(now);
1040 #[expect(clippy::cast_possible_truncation, reason = "window sizes fit in f32")]
1041 let logical = (lw as f32, lh as f32);
1042 let view = self.app.view_at(key, logical);
1043 let frame = build_frame(
1044 &view,
1045 &theme,
1046 &mut self.fonts,
1047 &mut bundle.state,
1048 logical,
1049 scale,
1050 );
1051 let scene = frame.paint(&mut self.fonts, &mut bundle.state);
1054 bundle.shell.present(&scene);
1055 if refresh_hover(&view, &frame, &mut bundle.state)
1056 && let Some(w) = bundle.shell.window()
1057 {
1058 w.request_redraw();
1059 }
1060 if frame.animating {
1061 event_loop.set_control_flow(ControlFlow::WaitUntil(
1062 Instant::now() + Duration::from_millis(16),
1063 ));
1064 }
1065 if let Some(caret) = bundle.state.ime_caret()
1066 && let Some(w) = bundle.shell.window()
1067 {
1068 w.set_ime_cursor_area(
1069 winit::dpi::LogicalPosition::new(caret.x0, caret.y0),
1070 winit::dpi::LogicalSize::new(1.0, caret.height()),
1071 );
1072 }
1073 bundle.last = Some((view, frame));
1074 let focus = bundle.state.focused();
1075 if let Some(adapter) = &mut bundle.adapter
1076 && let Some((_, frame)) = &bundle.last
1077 {
1078 adapter.update_if_active(|| crate::access::tree_update(frame, focus, scale));
1079 }
1080 }
1081
1082 #[cfg(not(target_arch = "wasm32"))]
1085 fn secondary_window_event(
1086 &mut self,
1087 key: &str,
1088 event_loop: &ActiveEventLoop,
1089 event: WindowEvent,
1090 ) {
1091 if let Some(bundle) = self.secondary.get_mut(key)
1092 && let Some(window) = bundle.shell.window()
1093 && let Some(adapter) = &mut bundle.adapter
1094 {
1095 adapter.process_event(window, &event);
1096 }
1097 match event {
1098 WindowEvent::CloseRequested => {
1099 if let Some(msg) = self.secondary.get(key).map(|b| b.on_close.clone()) {
1100 self.app.update(msg);
1101 self.after_update(event_loop);
1102 }
1103 }
1104 WindowEvent::Resized(size) => {
1105 if let Some(bundle) = self.secondary.get_mut(key) {
1106 bundle.shell.resized(size.width, size.height);
1107 }
1108 }
1109 WindowEvent::ScaleFactorChanged { .. } | WindowEvent::Occluded(false) => {
1110 if let Some(w) = self.secondary.get(key).and_then(|b| b.shell.window()) {
1111 w.request_redraw();
1112 }
1113 }
1114 WindowEvent::ModifiersChanged(mods) => {
1115 self.modifiers = mods.state();
1116 let m = self.modifiers;
1117 self.secondary_input_main(
1118 key,
1119 event_loop,
1120 InputEvent::Modifiers {
1121 shift: m.shift_key(),
1122 ctrl: m.control_key(),
1123 alt: m.alt_key(),
1124 meta: m.super_key(),
1125 },
1126 );
1127 }
1128 WindowEvent::DroppedFile(path) => {
1129 self.secondary_input_main(key, event_loop, InputEvent::FileDrop(path));
1130 }
1131 WindowEvent::CursorLeft { .. } => {
1132 self.secondary_input_main(key, event_loop, InputEvent::PointerLeave);
1133 }
1134 WindowEvent::CursorMoved { position, .. } => {
1135 let Some(bundle) = self.secondary.get_mut(key) else {
1136 return;
1137 };
1138 let scale = bundle.shell.window().map_or(1.0, |w| w.scale_factor());
1139 bundle.cursor = Point::new(position.x / scale, position.y / scale);
1140 #[expect(clippy::cast_possible_truncation, reason = "positions fit in f32")]
1141 let (x, y) = (bundle.cursor.x as f32, bundle.cursor.y as f32);
1142 self.secondary_input_main(key, event_loop, InputEvent::PointerMove { x, y });
1143 }
1144 WindowEvent::MouseInput {
1145 state,
1146 button: winit::event::MouseButton::Left,
1147 ..
1148 } => {
1149 self.secondary_input_main(
1150 key,
1151 event_loop,
1152 match state {
1153 winit::event::ElementState::Pressed => InputEvent::PointerDown,
1154 winit::event::ElementState::Released => InputEvent::PointerUp,
1155 },
1156 );
1157 }
1158 WindowEvent::MouseInput {
1159 state,
1160 button: winit::event::MouseButton::Right,
1161 ..
1162 } => {
1163 self.secondary_input_main(
1164 key,
1165 event_loop,
1166 match state {
1167 winit::event::ElementState::Pressed => InputEvent::RightDown,
1168 winit::event::ElementState::Released => InputEvent::RightUp,
1169 },
1170 );
1171 }
1172 WindowEvent::MouseWheel { delta, .. } => {
1173 let scale = self
1174 .secondary
1175 .get(key)
1176 .and_then(|b| b.shell.window())
1177 .map_or(1.0, |w| w.scale_factor());
1178 let (dx, dy) = wheel_deltas(delta, scale);
1179 #[expect(clippy::cast_possible_truncation, reason = "deltas fit in f32")]
1180 self.secondary_input_main(
1181 key,
1182 event_loop,
1183 InputEvent::Wheel {
1184 dx: dx as f32,
1185 dy: dy as f32,
1186 },
1187 );
1188 }
1189 WindowEvent::KeyboardInput { event, .. }
1190 if event.state == winit::event::ElementState::Pressed =>
1191 {
1192 let mods = self.modifiers;
1193 let printable = !mods.control_key()
1194 && !mods.super_key()
1195 && event
1196 .text
1197 .as_ref()
1198 .is_some_and(|t| !t.is_empty() && t.chars().all(|c| !c.is_control()));
1199 if printable {
1200 if let Some(t) = &event.text {
1201 self.secondary_input_main(key, event_loop, InputEvent::Text(t.to_string()));
1202 }
1203 } else if let Some(input) = map_key(&event, mods) {
1204 self.secondary_input_main(key, event_loop, input);
1205 }
1206 }
1207 WindowEvent::Ime(ime) => match ime {
1208 winit::event::Ime::Preedit(text, cursor) => {
1209 self.secondary_input_main(
1210 key,
1211 event_loop,
1212 InputEvent::ImePreedit { text, cursor },
1213 );
1214 }
1215 winit::event::Ime::Commit(text) => {
1216 self.secondary_input_main(key, event_loop, InputEvent::Text(text));
1217 }
1218 winit::event::Ime::Enabled | winit::event::Ime::Disabled => {}
1219 },
1220 WindowEvent::RedrawRequested => self.secondary_redraw(key, event_loop),
1221 _ => {}
1222 }
1223 }
1224
1225 #[cfg(not(target_arch = "wasm32"))]
1228 fn secondary_input(&mut self, key: &str, event: InputEvent) -> bool {
1229 let Some(bundle) = self.secondary.get_mut(key) else {
1230 return false;
1231 };
1232 let Some((view, frame)) = &bundle.last else {
1233 return false;
1234 };
1235 let result = dispatch(view, frame, &mut bundle.state, &mut self.fonts, event);
1236 if let Some(cursor) = result.cursor
1237 && let Some(w) = bundle.shell.window()
1238 {
1239 w.set_cursor(winit::window::Cursor::Icon(map_cursor(cursor)));
1240 }
1241 let had_msgs = !result.msgs.is_empty();
1242 if (result.redraw || had_msgs)
1243 && let Some(w) = bundle.shell.window()
1244 {
1245 w.request_redraw();
1246 }
1247 let msgs = result.msgs;
1248 for msg in msgs {
1249 self.app.update(msg);
1250 }
1251 had_msgs
1252 }
1253}
1254
1255pub(crate) fn map_cursor(cursor: fenestra_core::Cursor) -> winit::window::CursorIcon {
1256 match cursor {
1257 fenestra_core::Cursor::Default => winit::window::CursorIcon::Default,
1258 fenestra_core::Cursor::Pointer => winit::window::CursorIcon::Pointer,
1259 fenestra_core::Cursor::Text => winit::window::CursorIcon::Text,
1260 fenestra_core::Cursor::NotAllowed => winit::window::CursorIcon::NotAllowed,
1261 }
1262}
1263
1264pub(crate) fn map_key(
1266 event: &winit::event::KeyEvent,
1267 mods: winit::keyboard::ModifiersState,
1268) -> Option<InputEvent> {
1269 use winit::keyboard::{Key as WKey, NamedKey};
1270 let key = match &event.logical_key {
1271 WKey::Named(NamedKey::Tab) => {
1272 return Some(if mods.shift_key() {
1273 InputEvent::ShiftTab
1274 } else {
1275 InputEvent::Tab
1276 });
1277 }
1278 WKey::Named(named) => match named {
1279 NamedKey::Enter => Key::Enter,
1280 NamedKey::Space => Key::Space,
1281 NamedKey::Escape => Key::Escape,
1282 NamedKey::ArrowLeft => Key::ArrowLeft,
1283 NamedKey::ArrowRight => Key::ArrowRight,
1284 NamedKey::ArrowUp => Key::ArrowUp,
1285 NamedKey::ArrowDown => Key::ArrowDown,
1286 NamedKey::Home => Key::Home,
1287 NamedKey::End => Key::End,
1288 NamedKey::Backspace => Key::Backspace,
1289 NamedKey::Delete => Key::Delete,
1290 NamedKey::PageUp => Key::PageUp,
1291 NamedKey::PageDown => Key::PageDown,
1292 _ => return None,
1293 },
1294 WKey::Character(s) => Key::Char(s.chars().next()?),
1295 _ => return None,
1296 };
1297 Some(InputEvent::Key(KeyInput {
1298 key,
1299 shift: mods.shift_key(),
1300 ctrl: mods.control_key(),
1301 alt: mods.alt_key(),
1302 meta: mods.super_key(),
1303 }))
1304}
1305
1306impl<A: App> ApplicationHandler<RunnerEvent> for AppRunner<A> {
1307 #[cfg(not(target_arch = "wasm32"))]
1308 fn resumed(&mut self, event_loop: &ActiveEventLoop) {
1309 self.dirty = true;
1312 let adapter = &mut self.adapter;
1313 let proxy = self.proxy.clone();
1314 self.shell.resumed_with(event_loop, |el, window| {
1315 if adapter.is_none() {
1317 *adapter = Some(accesskit_winit::Adapter::with_event_loop_proxy(
1318 el, window, proxy,
1319 ));
1320 }
1321 });
1322 if let Some(w) = self.shell.window() {
1323 w.set_ime_allowed(true);
1324 }
1325 for bundle in self.secondary.values_mut() {
1326 bundle.shell.resumed(event_loop);
1327 }
1328 self.reconcile_windows(event_loop);
1329 }
1330
1331 #[cfg(target_arch = "wasm32")]
1332 fn resumed(&mut self, event_loop: &ActiveEventLoop) {
1333 self.dirty = true;
1334 self.shell.resumed(event_loop);
1335 if let Some(w) = self.shell.window() {
1336 w.set_ime_allowed(true);
1337 }
1338 }
1339
1340 fn user_event(&mut self, event_loop: &ActiveEventLoop, event: RunnerEvent) {
1341 match event {
1342 RunnerEvent::App(msg) => {
1343 if let Ok(msg) = msg.downcast::<A::Msg>() {
1344 self.app.update(*msg);
1345 self.after_update(event_loop);
1346 }
1347 }
1348 #[cfg(not(target_arch = "wasm32"))]
1349 RunnerEvent::Access(ev) => {
1350 let is_main = self.shell.window().is_some_and(|w| w.id() == ev.window_id);
1353 let skey = (!is_main)
1354 .then(|| {
1355 self.secondary
1356 .iter()
1357 .find(|(_, b)| b.shell.window().is_some_and(|w| w.id() == ev.window_id))
1358 .map(|(k, _)| k.clone())
1359 })
1360 .flatten();
1361 if !is_main && skey.is_none() {
1362 return;
1363 }
1364 match ev.window_event {
1365 accesskit_winit::WindowEvent::InitialTreeRequested => match &skey {
1366 None => {
1367 if self.last.is_some() {
1368 self.push_access_tree();
1369 } else if let Some(w) = self.shell.window() {
1370 w.request_redraw();
1371 }
1372 }
1373 Some(key) => {
1374 if let Some(bundle) = self.secondary.get_mut(key) {
1375 let scale = bundle.shell.window().map_or(1.0, |w| w.scale_factor());
1376 let focus = bundle.state.focused();
1377 if let Some((_, frame)) = &bundle.last {
1378 if let Some(adapter) = &mut bundle.adapter {
1379 adapter.update_if_active(|| {
1380 crate::access::tree_update(frame, focus, scale)
1381 });
1382 }
1383 } else if let Some(w) = bundle.shell.window() {
1384 w.request_redraw();
1385 }
1386 }
1387 }
1388 },
1389 accesskit_winit::WindowEvent::ActionRequested(req) => {
1390 let id = fenestra_core::WidgetId(req.target_node.0);
1391 match req.action {
1392 accesskit::Action::Click => {
1393 let msg = match &skey {
1394 None => self.last.as_ref().and_then(|(view, frame)| {
1395 fenestra_core::click_msg_of(view, frame, &self.state, id)
1396 }),
1397 Some(key) => self.secondary.get(key).and_then(|bundle| {
1398 bundle.last.as_ref().and_then(|(view, frame)| {
1399 fenestra_core::click_msg_of(
1400 view,
1401 frame,
1402 &bundle.state,
1403 id,
1404 )
1405 })
1406 }),
1407 };
1408 if let Some(msg) = msg {
1409 self.app.update(msg);
1410 self.after_update(event_loop);
1411 }
1412 }
1413 accesskit::Action::Focus => match &skey {
1414 None => {
1415 self.state.set_focus(Some(id));
1416 self.dirty = true;
1417 if let Some(w) = self.shell.window() {
1418 w.request_redraw();
1419 }
1420 }
1421 Some(key) => {
1422 if let Some(bundle) = self.secondary.get_mut(key) {
1423 bundle.state.set_focus(Some(id));
1424 if let Some(w) = bundle.shell.window() {
1425 w.request_redraw();
1426 }
1427 }
1428 }
1429 },
1430 _ => {}
1431 }
1432 }
1433 accesskit_winit::WindowEvent::AccessibilityDeactivated => {}
1434 }
1435 }
1436 }
1437 }
1438
1439 fn suspended(&mut self, _event_loop: &ActiveEventLoop) {
1440 self.shell.suspended();
1441 #[cfg(not(target_arch = "wasm32"))]
1442 for bundle in self.secondary.values_mut() {
1443 bundle.shell.suspended();
1444 }
1445 }
1446
1447 fn new_events(&mut self, _event_loop: &ActiveEventLoop, cause: StartCause) {
1448 if !matches!(cause, StartCause::ResumeTimeReached { .. }) {
1449 return;
1450 }
1451 if let Some(w) = self.shell.window() {
1452 w.request_redraw();
1453 }
1454 #[cfg(not(target_arch = "wasm32"))]
1455 for bundle in self.secondary.values() {
1456 if let Some(w) = bundle.shell.window() {
1457 w.request_redraw();
1458 }
1459 }
1460 }
1461
1462 fn window_event(
1463 &mut self,
1464 event_loop: &ActiveEventLoop,
1465 window_id: WindowId,
1466 event: WindowEvent,
1467 ) {
1468 if self.shell.window().is_none_or(|w| w.id() != window_id) {
1469 #[cfg(not(target_arch = "wasm32"))]
1470 if let Some(key) = self
1471 .secondary
1472 .iter()
1473 .find(|(_, b)| b.shell.window().is_some_and(|w| w.id() == window_id))
1474 .map(|(k, _)| k.clone())
1475 {
1476 self.secondary_window_event(&key, event_loop, event);
1477 }
1478 return;
1479 }
1480 #[cfg(not(target_arch = "wasm32"))]
1481 if let Some(adapter) = &mut self.adapter
1482 && let Some(window) = self.shell.window()
1483 {
1484 adapter.process_event(window, &event);
1485 }
1486 match event {
1487 WindowEvent::CloseRequested => event_loop.exit(),
1488 WindowEvent::Resized(size) => {
1489 self.dirty = true;
1492 self.shell.resized(size.width, size.height);
1493 }
1494 WindowEvent::ScaleFactorChanged { .. } => {
1495 self.dirty = true;
1496 if let Some(w) = self.shell.window() {
1497 w.request_redraw();
1498 }
1499 }
1500 WindowEvent::ModifiersChanged(mods) => {
1501 self.modifiers = mods.state();
1502 let m = self.modifiers;
1503 self.input_main(
1504 event_loop,
1505 InputEvent::Modifiers {
1506 shift: m.shift_key(),
1507 ctrl: m.control_key(),
1508 alt: m.alt_key(),
1509 meta: m.super_key(),
1510 },
1511 );
1512 }
1513 WindowEvent::Occluded(occluded) => {
1514 if !occluded && let Some(w) = self.shell.window() {
1515 w.request_redraw();
1516 }
1517 }
1518 WindowEvent::DroppedFile(path) => {
1519 self.input_main(event_loop, InputEvent::FileDrop(path))
1520 }
1521 WindowEvent::CursorLeft { .. } => self.input_main(event_loop, InputEvent::PointerLeave),
1522 WindowEvent::CursorMoved { position, .. } => {
1523 let scale = self.shell.window().map_or(1.0, |w| w.scale_factor());
1524 self.cursor = Point::new(position.x / scale, position.y / scale);
1525 #[expect(clippy::cast_possible_truncation, reason = "positions fit in f32")]
1526 self.input_main(
1527 event_loop,
1528 InputEvent::PointerMove {
1529 x: self.cursor.x as f32,
1530 y: self.cursor.y as f32,
1531 },
1532 );
1533 }
1534 WindowEvent::MouseInput {
1535 state,
1536 button: winit::event::MouseButton::Left,
1537 ..
1538 } => {
1539 self.input_main(
1540 event_loop,
1541 match state {
1542 winit::event::ElementState::Pressed => InputEvent::PointerDown,
1543 winit::event::ElementState::Released => InputEvent::PointerUp,
1544 },
1545 );
1546 }
1547 WindowEvent::MouseInput {
1548 state,
1549 button: winit::event::MouseButton::Right,
1550 ..
1551 } => {
1552 self.input_main(
1553 event_loop,
1554 match state {
1555 winit::event::ElementState::Pressed => InputEvent::RightDown,
1556 winit::event::ElementState::Released => InputEvent::RightUp,
1557 },
1558 );
1559 }
1560 WindowEvent::MouseWheel { delta, .. } => {
1561 let scale = self.shell.window().map_or(1.0, |w| w.scale_factor());
1562 let (dx, dy) = wheel_deltas(delta, scale);
1563 #[expect(clippy::cast_possible_truncation, reason = "deltas fit in f32")]
1564 self.input_main(
1565 event_loop,
1566 InputEvent::Wheel {
1567 dx: dx as f32,
1568 dy: dy as f32,
1569 },
1570 );
1571 }
1572 WindowEvent::KeyboardInput { event, .. }
1573 if event.state == winit::event::ElementState::Pressed =>
1574 {
1575 {
1576 let mods = self.modifiers;
1577 let printable = !mods.control_key()
1580 && !mods.super_key()
1581 && event
1582 .text
1583 .as_ref()
1584 .is_some_and(|t| !t.is_empty() && t.chars().all(|c| !c.is_control()));
1585 if printable {
1586 if let Some(t) = &event.text {
1587 self.input_main(event_loop, InputEvent::Text(t.to_string()));
1588 }
1589 } else if let Some(input) = map_key(&event, mods) {
1590 self.input_main(event_loop, input);
1591 }
1592 }
1593 }
1594 WindowEvent::Ime(ime) => match ime {
1595 winit::event::Ime::Preedit(text, cursor) => {
1596 self.input_main(event_loop, InputEvent::ImePreedit { text, cursor });
1597 }
1598 winit::event::Ime::Commit(text) => {
1599 self.input_main(event_loop, InputEvent::Text(text));
1600 }
1601 winit::event::Ime::Enabled | winit::event::Ime::Disabled => {}
1602 },
1603 WindowEvent::RedrawRequested => self.redraw(event_loop),
1604 _ => {}
1605 }
1606 }
1607}