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