1use std::sync::Arc;
8use std::time::{Duration, Instant};
9
10use fenestra_core::{
11 App, Element, Fonts, FrameState, InputEvent, Key, KeyInput, Theme, build_frame, dispatch,
12 refresh_hover,
13};
14use kurbo::Point;
15use vello::peniko::Color;
16use vello::util::{RenderContext, RenderSurface};
17use vello::wgpu::{self, CurrentSurfaceTexture};
18use vello::{AaConfig, AaSupport, RenderParams, Renderer, RendererOptions, Scene};
19use winit::application::ApplicationHandler;
20use winit::dpi::LogicalSize;
21use winit::event::{MouseScrollDelta, StartCause, WindowEvent};
22use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop, EventLoopProxy};
23use winit::window::{Window, WindowId};
24
25use crate::ShellError;
26
27const LINE_SCROLL_PX: f64 = 40.0;
29
30type PaintFn = Box<dyn FnMut(&mut Scene, f64, f64, Color)>;
32type ViewFn = Box<dyn Fn(&Theme) -> Element<()>>;
34
35#[derive(Debug, Clone)]
37pub struct WindowOptions {
38 pub title: String,
40 pub inner_size: (f64, f64),
42}
43
44impl WindowOptions {
45 pub fn titled(title: impl Into<String>) -> Self {
47 Self {
48 title: title.into(),
49 inner_size: (1024.0, 768.0),
50 }
51 }
52
53 pub fn with_size(mut self, width: f64, height: f64) -> Self {
55 self.inner_size = (width, height);
56 self
57 }
58}
59
60enum RenderState {
61 Active {
62 surface: Box<RenderSurface<'static>>,
63 valid_surface: bool,
64 window: Arc<Window>,
65 },
66 Suspended(Option<Arc<Window>>),
67}
68
69struct WindowShell {
71 context: RenderContext,
72 renderers: Vec<Option<Renderer>>,
73 state: RenderState,
74 scene: Scene,
75 options: WindowOptions,
76 background: Color,
77}
78
79impl WindowShell {
80 fn new(options: WindowOptions, background: Color) -> Self {
81 Self {
82 context: RenderContext::new(),
83 renderers: Vec::new(),
84 state: RenderState::Suspended(None),
85 scene: Scene::new(),
86 options,
87 background,
88 }
89 }
90
91 fn resumed(&mut self, event_loop: &ActiveEventLoop) {
92 self.resumed_with(event_loop, |_, _| {});
93 }
94
95 fn resumed_with(
99 &mut self,
100 event_loop: &ActiveEventLoop,
101 before_visible: impl FnOnce(&ActiveEventLoop, &Arc<Window>),
102 ) {
103 let RenderState::Suspended(cached_window) = &mut self.state else {
104 return;
105 };
106 let window = cached_window.take().unwrap_or_else(|| {
107 let attrs = Window::default_attributes()
108 .with_title(self.options.title.clone())
109 .with_inner_size(LogicalSize::new(
110 self.options.inner_size.0,
111 self.options.inner_size.1,
112 ))
113 .with_visible(false);
114 Arc::new(
115 event_loop
116 .create_window(attrs)
117 .expect("failed to create window"),
118 )
119 });
120 before_visible(event_loop, &window);
121 let was_hidden = window.is_visible() == Some(false);
122 self.activate(window.clone());
123 if was_hidden {
124 window.set_visible(true);
125 }
126 }
127
128 fn activate(&mut self, window: Arc<Window>) {
131 let size = window.inner_size();
132 let surface = pollster::block_on(self.context.create_surface(
133 window.clone(),
134 size.width.max(1),
135 size.height.max(1),
136 wgpu::PresentMode::AutoVsync,
137 ))
138 .expect("failed to create wgpu surface");
139
140 self.renderers
141 .resize_with(self.context.devices.len(), || None);
142 self.renderers[surface.dev_id].get_or_insert_with(|| {
143 Renderer::new(
144 &self.context.devices[surface.dev_id].device,
145 RendererOptions {
146 use_cpu: false,
147 antialiasing_support: AaSupport::area_only(),
148 ..Default::default()
149 },
150 )
151 .expect("failed to create vello renderer")
152 });
153
154 self.state = RenderState::Active {
155 surface: Box::new(surface),
156 valid_surface: size.width != 0 && size.height != 0,
157 window,
158 };
159 }
160
161 fn suspended(&mut self) {
162 if let RenderState::Active { window, .. } = &self.state {
163 self.state = RenderState::Suspended(Some(window.clone()));
164 }
165 }
166
167 fn window(&self) -> Option<&Arc<Window>> {
168 match &self.state {
169 RenderState::Active { window, .. } => Some(window),
170 RenderState::Suspended(_) => None,
171 }
172 }
173
174 fn resized(&mut self, width: u32, height: u32) {
175 let RenderState::Active {
176 surface,
177 valid_surface,
178 window,
179 } = &mut self.state
180 else {
181 return;
182 };
183 if width != 0 && height != 0 {
184 self.context.resize_surface(surface, width, height);
185 *valid_surface = true;
186 } else {
187 *valid_surface = false;
188 }
189 window.request_redraw();
190 }
191
192 fn logical_size(&self) -> Option<(f64, f64, f64)> {
194 match &self.state {
195 RenderState::Active {
196 surface, window, ..
197 } => {
198 let scale = window.scale_factor();
199 Some((
200 f64::from(surface.config.width) / scale,
201 f64::from(surface.config.height) / scale,
202 scale,
203 ))
204 }
205 RenderState::Suspended(_) => None,
206 }
207 }
208
209 fn present(&mut self, fragment: &Scene) {
211 let RenderState::Active {
212 surface,
213 valid_surface,
214 window,
215 } = &mut self.state
216 else {
217 return;
218 };
219 if !*valid_surface {
220 return;
221 }
222 let width = surface.config.width;
223 let height = surface.config.height;
224 let scale = window.scale_factor();
225
226 self.scene.reset();
227 self.scene
228 .append(fragment, Some(vello::kurbo::Affine::scale(scale)));
229
230 let handle = &self.context.devices[surface.dev_id];
231 self.renderers[surface.dev_id]
232 .as_mut()
233 .expect("renderer exists for surface device")
234 .render_to_texture(
235 &handle.device,
236 &handle.queue,
237 &self.scene,
238 &surface.target_view,
239 &RenderParams {
240 base_color: self.background,
241 width,
242 height,
243 antialiasing_method: AaConfig::Area,
244 },
245 )
246 .expect("vello render failed");
247
248 let surface_texture = match surface.surface.get_current_texture() {
249 CurrentSurfaceTexture::Success(texture) => texture,
250 CurrentSurfaceTexture::Outdated | CurrentSurfaceTexture::Suboptimal(_) => {
251 self.context.configure_surface(surface);
252 window.request_redraw();
253 return;
254 }
255 CurrentSurfaceTexture::Occluded => {
256 return;
259 }
260 CurrentSurfaceTexture::Timeout => {
261 window.request_redraw();
262 return;
263 }
264 CurrentSurfaceTexture::Lost => {
265 let window = window.clone();
268 window.request_redraw();
269 self.activate(window);
270 return;
271 }
272 CurrentSurfaceTexture::Validation => {
273 panic!("validation error acquiring wgpu surface texture")
274 }
275 };
276
277 let mut encoder = handle
278 .device
279 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
280 label: Some("fenestra surface blit"),
281 });
282 surface.blitter.copy(
283 &handle.device,
284 &mut encoder,
285 &surface.target_view,
286 &surface_texture
287 .texture
288 .create_view(&wgpu::TextureViewDescriptor::default()),
289 );
290 handle.queue.submit([encoder.finish()]);
291 surface_texture.present();
292 handle.device.poll(wgpu::PollType::Poll).unwrap();
293 }
294}
295
296pub fn run_scene(
302 options: WindowOptions,
303 background: Color,
304 paint: impl FnMut(&mut Scene, f64, f64, Color) + 'static,
305) -> Result<(), ShellError> {
306 let event_loop = EventLoop::new().map_err(ShellError::EventLoop)?;
307 let mut app = SceneApp {
308 shell: WindowShell::new(options, background),
309 fragment: Scene::new(),
310 paint: Box::new(paint),
311 };
312 event_loop.run_app(&mut app).map_err(ShellError::EventLoop)
313}
314
315struct SceneApp {
316 shell: WindowShell,
317 fragment: Scene,
318 paint: PaintFn,
319}
320
321impl ApplicationHandler for SceneApp {
322 fn resumed(&mut self, event_loop: &ActiveEventLoop) {
323 self.shell.resumed(event_loop);
324 }
325
326 fn suspended(&mut self, _event_loop: &ActiveEventLoop) {
327 self.shell.suspended();
328 }
329
330 fn window_event(
331 &mut self,
332 event_loop: &ActiveEventLoop,
333 window_id: WindowId,
334 event: WindowEvent,
335 ) {
336 if self.shell.window().is_none_or(|w| w.id() != window_id) {
337 return;
338 }
339 match event {
340 WindowEvent::CloseRequested => event_loop.exit(),
341 WindowEvent::Resized(size) => self.shell.resized(size.width, size.height),
342 WindowEvent::ScaleFactorChanged { .. } => {
343 if let Some(w) = self.shell.window() {
344 w.request_redraw();
345 }
346 }
347 WindowEvent::Occluded(occluded) => {
348 if !occluded && let Some(w) = self.shell.window() {
349 w.request_redraw();
350 }
351 }
352 WindowEvent::RedrawRequested => {
353 let Some((lw, lh, _scale)) = self.shell.logical_size() else {
354 return;
355 };
356 self.fragment.reset();
357 let bg = self.shell.background;
358 (self.paint)(&mut self.fragment, lw, lh, bg);
359 let fragment = std::mem::replace(&mut self.fragment, Scene::new());
360 self.shell.present(&fragment);
361 self.fragment = fragment;
362 }
363 _ => {}
364 }
365 }
366}
367
368pub fn run_static(
374 options: WindowOptions,
375 theme: Theme,
376 view: impl Fn(&Theme) -> Element<()> + 'static,
377) -> Result<(), ShellError> {
378 let event_loop = EventLoop::new().map_err(ShellError::EventLoop)?;
379 let background = theme.bg;
380 let mut app = StaticApp {
381 shell: WindowShell::new(options, background),
382 theme,
383 fonts: Fonts::with_system(),
384 state: FrameState::new(),
385 view: Box::new(view),
386 cursor: Point::ORIGIN,
387 started: Instant::now(),
388 last_frame: None,
389 };
390 event_loop.run_app(&mut app).map_err(ShellError::EventLoop)
391}
392
393struct StaticApp {
394 shell: WindowShell,
395 theme: Theme,
396 fonts: Fonts,
397 state: FrameState,
398 view: ViewFn,
399 cursor: Point,
401 started: Instant,
402 last_frame: Option<fenestra_core::Frame>,
404}
405
406impl StaticApp {
407 fn redraw(&mut self, event_loop: &ActiveEventLoop) {
408 let Some((lw, lh, scale)) = self.shell.logical_size() else {
409 return;
410 };
411 self.state.tick(self.started.elapsed().as_secs_f64());
412 let el = (self.view)(&self.theme);
413 #[expect(clippy::cast_possible_truncation, reason = "window sizes fit in f32")]
414 let frame = build_frame(
415 &el,
416 &self.theme,
417 &mut self.fonts,
418 &mut self.state,
419 (lw as f32, lh as f32),
420 scale,
421 );
422 let scene = frame.paint(&mut self.fonts, &mut self.state);
423 self.shell.present(&scene);
424 if frame.animating {
425 event_loop.set_control_flow(ControlFlow::WaitUntil(
426 Instant::now() + Duration::from_millis(16),
427 ));
428 } else {
429 event_loop.set_control_flow(ControlFlow::Wait);
430 }
431 self.last_frame = Some(frame);
432 }
433}
434
435impl ApplicationHandler for StaticApp {
436 fn resumed(&mut self, event_loop: &ActiveEventLoop) {
437 self.shell.resumed(event_loop);
438 }
439
440 fn suspended(&mut self, _event_loop: &ActiveEventLoop) {
441 self.shell.suspended();
442 }
443
444 fn new_events(&mut self, _event_loop: &ActiveEventLoop, cause: StartCause) {
445 if matches!(cause, StartCause::ResumeTimeReached { .. })
446 && let Some(w) = self.shell.window()
447 {
448 w.request_redraw();
449 }
450 }
451
452 fn window_event(
453 &mut self,
454 event_loop: &ActiveEventLoop,
455 window_id: WindowId,
456 event: WindowEvent,
457 ) {
458 if self.shell.window().is_none_or(|w| w.id() != window_id) {
459 return;
460 }
461 match event {
462 WindowEvent::CloseRequested => event_loop.exit(),
463 WindowEvent::Resized(size) => self.shell.resized(size.width, size.height),
464 WindowEvent::ScaleFactorChanged { .. } => {
465 if let Some(w) = self.shell.window() {
466 w.request_redraw();
467 }
468 }
469 WindowEvent::CursorMoved { position, .. } => {
470 let scale = self.shell.window().map_or(1.0, |w| w.scale_factor());
471 self.cursor = Point::new(position.x / scale, position.y / scale);
472 }
473 WindowEvent::MouseWheel { delta, .. } => {
474 let dy = match delta {
475 MouseScrollDelta::LineDelta(_, y) => f64::from(y) * LINE_SCROLL_PX,
476 MouseScrollDelta::PixelDelta(pos) => {
477 let scale = self.shell.window().map_or(1.0, |w| w.scale_factor());
478 pos.y / scale
479 }
480 };
481 if let Some(frame) = &self.last_frame
482 && let Some(id) = frame.scrollable_at(self.cursor)
483 {
484 #[expect(
485 clippy::cast_possible_truncation,
486 reason = "scroll deltas fit in f32"
487 )]
488 self.state.scroll_by(id, -dy as f32);
489 if let Some(w) = self.shell.window() {
490 w.request_redraw();
491 }
492 }
493 }
494 WindowEvent::RedrawRequested => self.redraw(event_loop),
495 _ => {}
496 }
497 }
498}
499
500enum RunnerEvent {
506 App(Box<dyn std::any::Any + Send>),
507 Access(accesskit_winit::Event),
508}
509
510impl From<accesskit_winit::Event> for RunnerEvent {
511 fn from(event: accesskit_winit::Event) -> Self {
512 Self::Access(event)
513 }
514}
515
516pub fn run_app<A: App + 'static>(mut app: A, options: WindowOptions) -> Result<(), ShellError>
522where
523 A::Msg: Send,
524{
525 let event_loop = EventLoop::<RunnerEvent>::with_user_event()
526 .build()
527 .map_err(ShellError::EventLoop)?;
528 let access_proxy = event_loop.create_proxy();
529 let proxy = event_loop.create_proxy();
530 app.init(fenestra_core::Proxy::new(move |msg: A::Msg| {
531 let _ = proxy.send_event(RunnerEvent::App(Box::new(msg)));
533 }));
534 let background = app.theme().bg;
535 let mut state = FrameState::new();
536 state.set_clipboard(Box::new(crate::OsClipboard::default()));
537 let mut runner = AppRunner {
538 shell: WindowShell::new(options, background),
539 app,
540 fonts: Fonts::with_system(),
541 state,
542 cursor: Point::ORIGIN,
543 started: Instant::now(),
544 last: None,
545 modifiers: winit::keyboard::ModifiersState::empty(),
546 adapter: None,
547 proxy: access_proxy,
548 };
549 event_loop
550 .run_app(&mut runner)
551 .map_err(ShellError::EventLoop)
552}
553
554struct AppRunner<A: App> {
555 shell: WindowShell,
556 app: A,
557 fonts: Fonts,
558 state: FrameState,
559 cursor: Point,
560 started: Instant,
561 last: Option<(Element<A::Msg>, fenestra_core::Frame)>,
563 modifiers: winit::keyboard::ModifiersState,
564 adapter: Option<accesskit_winit::Adapter>,
566 proxy: EventLoopProxy<RunnerEvent>,
568}
569
570impl<A: App> AppRunner<A> {
571 fn redraw(&mut self, event_loop: &ActiveEventLoop) {
572 let Some((lw, lh, scale)) = self.shell.logical_size() else {
573 return;
574 };
575 let theme = self.app.theme();
576 self.shell.background = theme.bg;
577 self.state.tick(self.started.elapsed().as_secs_f64());
578 let view = self.app.view();
579 #[expect(clippy::cast_possible_truncation, reason = "window sizes fit in f32")]
580 let frame = build_frame(
581 &view,
582 &theme,
583 &mut self.fonts,
584 &mut self.state,
585 (lw as f32, lh as f32),
586 scale,
587 );
588 let scene = frame.paint(&mut self.fonts, &mut self.state);
589 self.shell.present(&scene);
590 if refresh_hover(&view, &frame, &mut self.state)
593 && let Some(w) = self.shell.window()
594 {
595 w.request_redraw();
596 }
597 if frame.animating {
598 event_loop.set_control_flow(ControlFlow::WaitUntil(
599 Instant::now() + Duration::from_millis(16),
600 ));
601 } else {
602 event_loop.set_control_flow(ControlFlow::Wait);
603 }
604 self.last = Some((view, frame));
605 self.push_access_tree();
606 }
607
608 fn push_access_tree(&mut self) {
611 let scale = self.shell.window().map_or(1.0, |w| w.scale_factor());
612 let focus = self.state.focused();
613 if let Some(adapter) = &mut self.adapter
614 && let Some((_, frame)) = &self.last
615 {
616 adapter.update_if_active(|| crate::access::tree_update(frame, focus, scale));
617 }
618 }
619
620 fn input(&mut self, event: InputEvent) {
621 let Some((view, frame)) = &self.last else {
622 return;
623 };
624 let result = dispatch(view, frame, &mut self.state, &mut self.fonts, event);
625 if let Some(cursor) = result.cursor
626 && let Some(w) = self.shell.window()
627 {
628 w.set_cursor(winit::window::Cursor::Icon(map_cursor(cursor)));
629 }
630 let had_msgs = !result.msgs.is_empty();
631 for msg in result.msgs {
632 self.app.update(msg);
633 }
634 if (result.redraw || had_msgs)
635 && let Some(w) = self.shell.window()
636 {
637 w.request_redraw();
638 }
639 }
640}
641
642fn map_cursor(cursor: fenestra_core::Cursor) -> winit::window::CursorIcon {
643 match cursor {
644 fenestra_core::Cursor::Default => winit::window::CursorIcon::Default,
645 fenestra_core::Cursor::Pointer => winit::window::CursorIcon::Pointer,
646 fenestra_core::Cursor::Text => winit::window::CursorIcon::Text,
647 fenestra_core::Cursor::NotAllowed => winit::window::CursorIcon::NotAllowed,
648 }
649}
650
651fn map_key(
653 event: &winit::event::KeyEvent,
654 mods: winit::keyboard::ModifiersState,
655) -> Option<InputEvent> {
656 use winit::keyboard::{Key as WKey, NamedKey};
657 let key = match &event.logical_key {
658 WKey::Named(NamedKey::Tab) => {
659 return Some(if mods.shift_key() {
660 InputEvent::ShiftTab
661 } else {
662 InputEvent::Tab
663 });
664 }
665 WKey::Named(named) => match named {
666 NamedKey::Enter => Key::Enter,
667 NamedKey::Space => Key::Space,
668 NamedKey::Escape => Key::Escape,
669 NamedKey::ArrowLeft => Key::ArrowLeft,
670 NamedKey::ArrowRight => Key::ArrowRight,
671 NamedKey::ArrowUp => Key::ArrowUp,
672 NamedKey::ArrowDown => Key::ArrowDown,
673 NamedKey::Home => Key::Home,
674 NamedKey::End => Key::End,
675 NamedKey::Backspace => Key::Backspace,
676 NamedKey::Delete => Key::Delete,
677 _ => return None,
678 },
679 WKey::Character(s) => Key::Char(s.chars().next()?),
680 _ => return None,
681 };
682 Some(InputEvent::Key(KeyInput {
683 key,
684 shift: mods.shift_key(),
685 ctrl: mods.control_key(),
686 alt: mods.alt_key(),
687 meta: mods.super_key(),
688 }))
689}
690
691impl<A: App> ApplicationHandler<RunnerEvent> for AppRunner<A> {
692 fn resumed(&mut self, event_loop: &ActiveEventLoop) {
693 let adapter = &mut self.adapter;
694 let proxy = self.proxy.clone();
695 self.shell.resumed_with(event_loop, |el, window| {
696 if adapter.is_none() {
698 *adapter = Some(accesskit_winit::Adapter::with_event_loop_proxy(
699 el, window, proxy,
700 ));
701 }
702 });
703 if let Some(w) = self.shell.window() {
704 w.set_ime_allowed(true);
705 }
706 }
707
708 fn user_event(&mut self, _event_loop: &ActiveEventLoop, event: RunnerEvent) {
709 match event {
710 RunnerEvent::App(msg) => {
711 if let Ok(msg) = msg.downcast::<A::Msg>() {
712 self.app.update(*msg);
713 if let Some(w) = self.shell.window() {
714 w.request_redraw();
715 }
716 }
717 }
718 RunnerEvent::Access(ev) => match ev.window_event {
719 accesskit_winit::WindowEvent::InitialTreeRequested => {
720 if self.last.is_some() {
721 self.push_access_tree();
722 } else if let Some(w) = self.shell.window() {
723 w.request_redraw();
724 }
725 }
726 accesskit_winit::WindowEvent::ActionRequested(req) => {
727 let id = fenestra_core::WidgetId(req.target_node.0);
728 match req.action {
729 accesskit::Action::Click => {
730 if let Some((view, _)) = &self.last
731 && let Some(msg) = fenestra_core::click_msg_of(view, id)
732 {
733 self.app.update(msg);
734 if let Some(w) = self.shell.window() {
735 w.request_redraw();
736 }
737 }
738 }
739 accesskit::Action::Focus => {
740 self.state.set_focus(Some(id));
741 if let Some(w) = self.shell.window() {
742 w.request_redraw();
743 }
744 }
745 _ => {}
746 }
747 }
748 accesskit_winit::WindowEvent::AccessibilityDeactivated => {}
749 },
750 }
751 }
752
753 fn suspended(&mut self, _event_loop: &ActiveEventLoop) {
754 self.shell.suspended();
755 }
756
757 fn new_events(&mut self, _event_loop: &ActiveEventLoop, cause: StartCause) {
758 if matches!(cause, StartCause::ResumeTimeReached { .. })
759 && let Some(w) = self.shell.window()
760 {
761 w.request_redraw();
762 }
763 }
764
765 fn window_event(
766 &mut self,
767 event_loop: &ActiveEventLoop,
768 window_id: WindowId,
769 event: WindowEvent,
770 ) {
771 if self.shell.window().is_none_or(|w| w.id() != window_id) {
772 return;
773 }
774 if let Some(adapter) = &mut self.adapter
775 && let Some(window) = self.shell.window()
776 {
777 adapter.process_event(window, &event);
778 }
779 match event {
780 WindowEvent::CloseRequested => event_loop.exit(),
781 WindowEvent::Resized(size) => self.shell.resized(size.width, size.height),
782 WindowEvent::ScaleFactorChanged { .. } => {
783 if let Some(w) = self.shell.window() {
784 w.request_redraw();
785 }
786 }
787 WindowEvent::ModifiersChanged(mods) => self.modifiers = mods.state(),
788 WindowEvent::Occluded(occluded) => {
789 if !occluded && let Some(w) = self.shell.window() {
790 w.request_redraw();
791 }
792 }
793 WindowEvent::CursorLeft { .. } => self.input(InputEvent::PointerLeave),
794 WindowEvent::CursorMoved { position, .. } => {
795 let scale = self.shell.window().map_or(1.0, |w| w.scale_factor());
796 self.cursor = Point::new(position.x / scale, position.y / scale);
797 #[expect(clippy::cast_possible_truncation, reason = "positions fit in f32")]
798 self.input(InputEvent::PointerMove {
799 x: self.cursor.x as f32,
800 y: self.cursor.y as f32,
801 });
802 }
803 WindowEvent::MouseInput {
804 state,
805 button: winit::event::MouseButton::Left,
806 ..
807 } => {
808 self.input(match state {
809 winit::event::ElementState::Pressed => InputEvent::PointerDown,
810 winit::event::ElementState::Released => InputEvent::PointerUp,
811 });
812 }
813 WindowEvent::MouseWheel { delta, .. } => {
814 let dy = match delta {
815 MouseScrollDelta::LineDelta(_, y) => f64::from(y) * LINE_SCROLL_PX,
816 MouseScrollDelta::PixelDelta(pos) => {
817 let scale = self.shell.window().map_or(1.0, |w| w.scale_factor());
818 pos.y / scale
819 }
820 };
821 #[expect(clippy::cast_possible_truncation, reason = "deltas fit in f32")]
822 self.input(InputEvent::Wheel { dy: dy as f32 });
823 }
824 WindowEvent::KeyboardInput { event, .. }
825 if event.state == winit::event::ElementState::Pressed =>
826 {
827 {
828 let mods = self.modifiers;
829 let printable = !mods.control_key()
832 && !mods.super_key()
833 && event
834 .text
835 .as_ref()
836 .is_some_and(|t| !t.is_empty() && t.chars().all(|c| !c.is_control()));
837 if printable {
838 if let Some(t) = &event.text {
839 self.input(InputEvent::Text(t.to_string()));
840 }
841 } else if let Some(input) = map_key(&event, mods) {
842 self.input(input);
843 }
844 }
845 }
846 WindowEvent::Ime(ime) => match ime {
847 winit::event::Ime::Preedit(text, cursor) => {
848 self.input(InputEvent::ImePreedit { text, cursor });
849 }
850 winit::event::Ime::Commit(text) => {
851 self.input(InputEvent::Text(text));
852 }
853 winit::event::Ime::Enabled | winit::event::Ime::Disabled => {}
854 },
855 WindowEvent::RedrawRequested => self.redraw(event_loop),
856 _ => {}
857 }
858 }
859}