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
34const 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 modifiers: winit::keyboard::ModifiersState::empty(),
731 #[cfg(not(target_arch = "wasm32"))]
732 adapter: None,
733 #[cfg(not(target_arch = "wasm32"))]
734 proxy: access_proxy,
735 #[cfg(not(target_arch = "wasm32"))]
736 secondary: std::collections::HashMap::new(),
737 };
738 #[cfg(not(target_arch = "wasm32"))]
739 {
740 let mut runner = runner;
741 event_loop
742 .run_app(&mut runner)
743 .map_err(ShellError::EventLoop)
744 }
745 #[cfg(target_arch = "wasm32")]
746 {
747 use winit::platform::web::EventLoopExtWebSys;
748 event_loop.spawn_app(runner);
750 Ok(())
751 }
752}
753
754struct AppRunner<A: App> {
755 shell: WindowShell,
756 app: A,
757 fonts: Fonts,
758 state: FrameState,
759 cursor: Point,
760 started: Instant,
761 last: Option<(Element<A::Msg>, fenestra_core::Frame)>,
763 modifiers: winit::keyboard::ModifiersState,
764 #[cfg(not(target_arch = "wasm32"))]
766 adapter: Option<accesskit_winit::Adapter>,
767 #[cfg(not(target_arch = "wasm32"))]
769 proxy: EventLoopProxy<RunnerEvent>,
770 #[cfg(not(target_arch = "wasm32"))]
773 secondary: std::collections::HashMap<String, SecondaryWindow<A>>,
774}
775
776#[cfg(not(target_arch = "wasm32"))]
779struct SecondaryWindow<A: App> {
780 shell: WindowShell,
781 state: FrameState,
782 cursor: Point,
783 last: Option<(Element<A::Msg>, fenestra_core::Frame)>,
784 on_close: A::Msg,
785 title: String,
786 adapter: Option<accesskit_winit::Adapter>,
787}
788
789impl<A: App> AppRunner<A> {
790 fn redraw(&mut self, event_loop: &ActiveEventLoop) {
791 self.shell.pump();
792 let Some((lw, lh, scale)) = self.shell.logical_size() else {
793 return;
794 };
795 let theme = self.app.theme();
796 self.shell.background = theme.bg;
797 self.state.tick(self.started.elapsed().as_secs_f64());
798 let view = self.app.view_for(fenestra_core::MAIN_WINDOW);
799 #[expect(clippy::cast_possible_truncation, reason = "window sizes fit in f32")]
800 let frame = build_frame(
801 &view,
802 &theme,
803 &mut self.fonts,
804 &mut self.state,
805 (lw as f32, lh as f32),
806 scale,
807 );
808 let scene = frame.paint(&mut self.fonts, &mut self.state);
809 self.shell.present(&scene);
810 if refresh_hover(&view, &frame, &mut self.state)
813 && let Some(w) = self.shell.window()
814 {
815 w.request_redraw();
816 }
817 if frame.animating {
818 #[cfg(not(target_arch = "wasm32"))]
819 event_loop.set_control_flow(ControlFlow::WaitUntil(
820 Instant::now() + Duration::from_millis(16),
821 ));
822 #[cfg(target_arch = "wasm32")]
824 if let Some(w) = self.shell.window() {
825 w.request_redraw();
826 }
827 } else {
828 #[cfg(not(target_arch = "wasm32"))]
829 let secondary_animating = self
830 .secondary
831 .values()
832 .any(|b| b.last.as_ref().is_some_and(|(_, f)| f.animating));
833 #[cfg(target_arch = "wasm32")]
834 let secondary_animating = false;
835 if !secondary_animating {
836 event_loop.set_control_flow(ControlFlow::Wait);
837 }
838 }
839 self.last = Some((view, frame));
840 if let Some(caret) = self.state.ime_caret()
842 && let Some(w) = self.shell.window()
843 {
844 w.set_ime_cursor_area(
845 winit::dpi::LogicalPosition::new(caret.x0, caret.y0),
846 winit::dpi::LogicalSize::new(1.0, caret.height()),
847 );
848 }
849 #[cfg(not(target_arch = "wasm32"))]
850 self.push_access_tree();
851 }
852
853 #[cfg(not(target_arch = "wasm32"))]
856 fn push_access_tree(&mut self) {
857 let scale = self.shell.window().map_or(1.0, |w| w.scale_factor());
858 let focus = self.state.focused();
859 if let Some(adapter) = &mut self.adapter
860 && let Some((_, frame)) = &self.last
861 {
862 adapter.update_if_active(|| crate::access::tree_update(frame, focus, scale));
863 }
864 }
865
866 fn input(&mut self, event: InputEvent) -> bool {
867 let Some((view, frame)) = &self.last else {
868 return false;
869 };
870 let result = dispatch(view, frame, &mut self.state, &mut self.fonts, event);
871 if let Some(cursor) = result.cursor
872 && let Some(w) = self.shell.window()
873 {
874 w.set_cursor(winit::window::Cursor::Icon(map_cursor(cursor)));
875 }
876 let had_msgs = !result.msgs.is_empty();
877 for msg in result.msgs {
878 self.app.update(msg);
879 }
880 if (result.redraw || had_msgs)
881 && let Some(w) = self.shell.window()
882 {
883 w.request_redraw();
884 }
885 had_msgs
886 }
887
888 fn input_main(&mut self, event_loop: &ActiveEventLoop, event: InputEvent) {
891 if self.input(event) {
892 self.after_update(event_loop);
893 }
894 }
895
896 #[cfg(not(target_arch = "wasm32"))]
899 fn secondary_input_main(&mut self, key: &str, event_loop: &ActiveEventLoop, event: InputEvent) {
900 if self.secondary_input(key, event) {
901 self.after_update(event_loop);
902 }
903 }
904
905 fn after_update(&mut self, event_loop: &ActiveEventLoop) {
908 #[cfg(not(target_arch = "wasm32"))]
909 self.reconcile_windows(event_loop);
910 #[cfg(target_arch = "wasm32")]
911 let _ = event_loop;
912 if let Some(w) = self.shell.window() {
913 w.request_redraw();
914 }
915 #[cfg(not(target_arch = "wasm32"))]
916 for bundle in self.secondary.values() {
917 if let Some(w) = bundle.shell.window() {
918 w.request_redraw();
919 }
920 }
921 }
922
923 #[cfg(not(target_arch = "wasm32"))]
926 fn reconcile_windows(&mut self, event_loop: &ActiveEventLoop) {
927 let desired = self.app.windows();
928 self.secondary
929 .retain(|key, _| desired.iter().any(|d| &d.key == key));
930 for desc in desired {
931 match self.secondary.get_mut(&desc.key) {
932 Some(bundle) => {
933 bundle.on_close = desc.on_close;
934 if bundle.title != desc.title {
935 bundle.title.clone_from(&desc.title);
936 if let Some(w) = bundle.shell.window() {
937 w.set_title(&desc.title);
938 }
939 }
940 }
941 None => {
942 let mut shell = WindowShell::new(
943 WindowOptions::titled(desc.title.clone())
944 .with_size(desc.size.0, desc.size.1),
945 self.shell.background,
946 );
947 let proxy = self.proxy.clone();
948 let mut adapter = None;
949 shell.resumed_with(event_loop, |el, window| {
950 adapter = Some(accesskit_winit::Adapter::with_event_loop_proxy(
951 el, window, proxy,
952 ));
953 });
954 if let Some(w) = shell.window() {
955 w.set_ime_allowed(true);
956 w.request_redraw();
957 }
958 let mut state = FrameState::new();
959 state.set_clipboard(Box::new(crate::OsClipboard::default()));
960 self.secondary.insert(
961 desc.key.clone(),
962 SecondaryWindow {
963 shell,
964 state,
965 cursor: Point::ORIGIN,
966 last: None,
967 on_close: desc.on_close,
968 title: desc.title,
969 adapter,
970 },
971 );
972 }
973 }
974 }
975 }
976
977 #[cfg(not(target_arch = "wasm32"))]
980 fn secondary_redraw(&mut self, key: &str, event_loop: &ActiveEventLoop) {
981 let theme = self.app.theme();
982 let now = self.started.elapsed().as_secs_f64();
983 let Some(bundle) = self.secondary.get_mut(key) else {
984 return;
985 };
986 bundle.shell.pump();
987 let Some((lw, lh, scale)) = bundle.shell.logical_size() else {
988 return;
989 };
990 bundle.shell.background = theme.bg;
991 bundle.state.tick(now);
992 let view = self.app.view_for(key);
993 #[expect(clippy::cast_possible_truncation, reason = "window sizes fit in f32")]
994 let frame = build_frame(
995 &view,
996 &theme,
997 &mut self.fonts,
998 &mut bundle.state,
999 (lw as f32, lh as f32),
1000 scale,
1001 );
1002 let scene = frame.paint(&mut self.fonts, &mut bundle.state);
1003 bundle.shell.present(&scene);
1004 if refresh_hover(&view, &frame, &mut bundle.state)
1005 && let Some(w) = bundle.shell.window()
1006 {
1007 w.request_redraw();
1008 }
1009 if frame.animating {
1010 event_loop.set_control_flow(ControlFlow::WaitUntil(
1011 Instant::now() + Duration::from_millis(16),
1012 ));
1013 }
1014 if let Some(caret) = bundle.state.ime_caret()
1015 && let Some(w) = bundle.shell.window()
1016 {
1017 w.set_ime_cursor_area(
1018 winit::dpi::LogicalPosition::new(caret.x0, caret.y0),
1019 winit::dpi::LogicalSize::new(1.0, caret.height()),
1020 );
1021 }
1022 bundle.last = Some((view, frame));
1023 let focus = bundle.state.focused();
1024 if let Some(adapter) = &mut bundle.adapter
1025 && let Some((_, frame)) = &bundle.last
1026 {
1027 adapter.update_if_active(|| crate::access::tree_update(frame, focus, scale));
1028 }
1029 }
1030
1031 #[cfg(not(target_arch = "wasm32"))]
1034 fn secondary_window_event(
1035 &mut self,
1036 key: &str,
1037 event_loop: &ActiveEventLoop,
1038 event: WindowEvent,
1039 ) {
1040 if let Some(bundle) = self.secondary.get_mut(key)
1041 && let Some(window) = bundle.shell.window()
1042 && let Some(adapter) = &mut bundle.adapter
1043 {
1044 adapter.process_event(window, &event);
1045 }
1046 match event {
1047 WindowEvent::CloseRequested => {
1048 if let Some(msg) = self.secondary.get(key).map(|b| b.on_close.clone()) {
1049 self.app.update(msg);
1050 self.after_update(event_loop);
1051 }
1052 }
1053 WindowEvent::Resized(size) => {
1054 if let Some(bundle) = self.secondary.get_mut(key) {
1055 bundle.shell.resized(size.width, size.height);
1056 }
1057 }
1058 WindowEvent::ScaleFactorChanged { .. } | WindowEvent::Occluded(false) => {
1059 if let Some(w) = self.secondary.get(key).and_then(|b| b.shell.window()) {
1060 w.request_redraw();
1061 }
1062 }
1063 WindowEvent::ModifiersChanged(mods) => {
1064 self.modifiers = mods.state();
1065 let m = self.modifiers;
1066 self.secondary_input_main(
1067 key,
1068 event_loop,
1069 InputEvent::Modifiers {
1070 shift: m.shift_key(),
1071 ctrl: m.control_key(),
1072 alt: m.alt_key(),
1073 meta: m.super_key(),
1074 },
1075 );
1076 }
1077 WindowEvent::DroppedFile(path) => {
1078 self.secondary_input_main(key, event_loop, InputEvent::FileDrop(path));
1079 }
1080 WindowEvent::CursorLeft { .. } => {
1081 self.secondary_input_main(key, event_loop, InputEvent::PointerLeave);
1082 }
1083 WindowEvent::CursorMoved { position, .. } => {
1084 let Some(bundle) = self.secondary.get_mut(key) else {
1085 return;
1086 };
1087 let scale = bundle.shell.window().map_or(1.0, |w| w.scale_factor());
1088 bundle.cursor = Point::new(position.x / scale, position.y / scale);
1089 #[expect(clippy::cast_possible_truncation, reason = "positions fit in f32")]
1090 let (x, y) = (bundle.cursor.x as f32, bundle.cursor.y as f32);
1091 self.secondary_input_main(key, event_loop, InputEvent::PointerMove { x, y });
1092 }
1093 WindowEvent::MouseInput {
1094 state,
1095 button: winit::event::MouseButton::Left,
1096 ..
1097 } => {
1098 self.secondary_input_main(
1099 key,
1100 event_loop,
1101 match state {
1102 winit::event::ElementState::Pressed => InputEvent::PointerDown,
1103 winit::event::ElementState::Released => InputEvent::PointerUp,
1104 },
1105 );
1106 }
1107 WindowEvent::MouseInput {
1108 state,
1109 button: winit::event::MouseButton::Right,
1110 ..
1111 } => {
1112 self.secondary_input_main(
1113 key,
1114 event_loop,
1115 match state {
1116 winit::event::ElementState::Pressed => InputEvent::RightDown,
1117 winit::event::ElementState::Released => InputEvent::RightUp,
1118 },
1119 );
1120 }
1121 WindowEvent::MouseWheel { delta, .. } => {
1122 let dy = match delta {
1123 MouseScrollDelta::LineDelta(_, y) => f64::from(y) * LINE_SCROLL_PX,
1124 MouseScrollDelta::PixelDelta(pos) => {
1125 let scale = self
1126 .secondary
1127 .get(key)
1128 .and_then(|b| b.shell.window())
1129 .map_or(1.0, |w| w.scale_factor());
1130 pos.y / scale
1131 }
1132 };
1133 #[expect(clippy::cast_possible_truncation, reason = "deltas fit in f32")]
1134 self.secondary_input_main(key, event_loop, InputEvent::Wheel { dy: dy as f32 });
1135 }
1136 WindowEvent::KeyboardInput { event, .. }
1137 if event.state == winit::event::ElementState::Pressed =>
1138 {
1139 let mods = self.modifiers;
1140 let printable = !mods.control_key()
1141 && !mods.super_key()
1142 && event
1143 .text
1144 .as_ref()
1145 .is_some_and(|t| !t.is_empty() && t.chars().all(|c| !c.is_control()));
1146 if printable {
1147 if let Some(t) = &event.text {
1148 self.secondary_input_main(key, event_loop, InputEvent::Text(t.to_string()));
1149 }
1150 } else if let Some(input) = map_key(&event, mods) {
1151 self.secondary_input_main(key, event_loop, input);
1152 }
1153 }
1154 WindowEvent::Ime(ime) => match ime {
1155 winit::event::Ime::Preedit(text, cursor) => {
1156 self.secondary_input_main(
1157 key,
1158 event_loop,
1159 InputEvent::ImePreedit { text, cursor },
1160 );
1161 }
1162 winit::event::Ime::Commit(text) => {
1163 self.secondary_input_main(key, event_loop, InputEvent::Text(text));
1164 }
1165 winit::event::Ime::Enabled | winit::event::Ime::Disabled => {}
1166 },
1167 WindowEvent::RedrawRequested => self.secondary_redraw(key, event_loop),
1168 _ => {}
1169 }
1170 }
1171
1172 #[cfg(not(target_arch = "wasm32"))]
1175 fn secondary_input(&mut self, key: &str, event: InputEvent) -> bool {
1176 let Some(bundle) = self.secondary.get_mut(key) else {
1177 return false;
1178 };
1179 let Some((view, frame)) = &bundle.last else {
1180 return false;
1181 };
1182 let result = dispatch(view, frame, &mut bundle.state, &mut self.fonts, event);
1183 if let Some(cursor) = result.cursor
1184 && let Some(w) = bundle.shell.window()
1185 {
1186 w.set_cursor(winit::window::Cursor::Icon(map_cursor(cursor)));
1187 }
1188 let had_msgs = !result.msgs.is_empty();
1189 if (result.redraw || had_msgs)
1190 && let Some(w) = bundle.shell.window()
1191 {
1192 w.request_redraw();
1193 }
1194 let msgs = result.msgs;
1195 for msg in msgs {
1196 self.app.update(msg);
1197 }
1198 had_msgs
1199 }
1200}
1201
1202fn map_cursor(cursor: fenestra_core::Cursor) -> winit::window::CursorIcon {
1203 match cursor {
1204 fenestra_core::Cursor::Default => winit::window::CursorIcon::Default,
1205 fenestra_core::Cursor::Pointer => winit::window::CursorIcon::Pointer,
1206 fenestra_core::Cursor::Text => winit::window::CursorIcon::Text,
1207 fenestra_core::Cursor::NotAllowed => winit::window::CursorIcon::NotAllowed,
1208 }
1209}
1210
1211fn map_key(
1213 event: &winit::event::KeyEvent,
1214 mods: winit::keyboard::ModifiersState,
1215) -> Option<InputEvent> {
1216 use winit::keyboard::{Key as WKey, NamedKey};
1217 let key = match &event.logical_key {
1218 WKey::Named(NamedKey::Tab) => {
1219 return Some(if mods.shift_key() {
1220 InputEvent::ShiftTab
1221 } else {
1222 InputEvent::Tab
1223 });
1224 }
1225 WKey::Named(named) => match named {
1226 NamedKey::Enter => Key::Enter,
1227 NamedKey::Space => Key::Space,
1228 NamedKey::Escape => Key::Escape,
1229 NamedKey::ArrowLeft => Key::ArrowLeft,
1230 NamedKey::ArrowRight => Key::ArrowRight,
1231 NamedKey::ArrowUp => Key::ArrowUp,
1232 NamedKey::ArrowDown => Key::ArrowDown,
1233 NamedKey::Home => Key::Home,
1234 NamedKey::End => Key::End,
1235 NamedKey::Backspace => Key::Backspace,
1236 NamedKey::Delete => Key::Delete,
1237 NamedKey::PageUp => Key::PageUp,
1238 NamedKey::PageDown => Key::PageDown,
1239 _ => return None,
1240 },
1241 WKey::Character(s) => Key::Char(s.chars().next()?),
1242 _ => return None,
1243 };
1244 Some(InputEvent::Key(KeyInput {
1245 key,
1246 shift: mods.shift_key(),
1247 ctrl: mods.control_key(),
1248 alt: mods.alt_key(),
1249 meta: mods.super_key(),
1250 }))
1251}
1252
1253impl<A: App> ApplicationHandler<RunnerEvent> for AppRunner<A> {
1254 #[cfg(not(target_arch = "wasm32"))]
1255 fn resumed(&mut self, event_loop: &ActiveEventLoop) {
1256 let adapter = &mut self.adapter;
1257 let proxy = self.proxy.clone();
1258 self.shell.resumed_with(event_loop, |el, window| {
1259 if adapter.is_none() {
1261 *adapter = Some(accesskit_winit::Adapter::with_event_loop_proxy(
1262 el, window, proxy,
1263 ));
1264 }
1265 });
1266 if let Some(w) = self.shell.window() {
1267 w.set_ime_allowed(true);
1268 }
1269 for bundle in self.secondary.values_mut() {
1270 bundle.shell.resumed(event_loop);
1271 }
1272 self.reconcile_windows(event_loop);
1273 }
1274
1275 #[cfg(target_arch = "wasm32")]
1276 fn resumed(&mut self, event_loop: &ActiveEventLoop) {
1277 self.shell.resumed(event_loop);
1278 if let Some(w) = self.shell.window() {
1279 w.set_ime_allowed(true);
1280 }
1281 }
1282
1283 fn user_event(&mut self, event_loop: &ActiveEventLoop, event: RunnerEvent) {
1284 match event {
1285 RunnerEvent::App(msg) => {
1286 if let Ok(msg) = msg.downcast::<A::Msg>() {
1287 self.app.update(*msg);
1288 self.after_update(event_loop);
1289 }
1290 }
1291 #[cfg(not(target_arch = "wasm32"))]
1292 RunnerEvent::Access(ev) => {
1293 let is_main = self.shell.window().is_some_and(|w| w.id() == ev.window_id);
1296 let skey = (!is_main)
1297 .then(|| {
1298 self.secondary
1299 .iter()
1300 .find(|(_, b)| b.shell.window().is_some_and(|w| w.id() == ev.window_id))
1301 .map(|(k, _)| k.clone())
1302 })
1303 .flatten();
1304 if !is_main && skey.is_none() {
1305 return;
1306 }
1307 match ev.window_event {
1308 accesskit_winit::WindowEvent::InitialTreeRequested => match &skey {
1309 None => {
1310 if self.last.is_some() {
1311 self.push_access_tree();
1312 } else if let Some(w) = self.shell.window() {
1313 w.request_redraw();
1314 }
1315 }
1316 Some(key) => {
1317 if let Some(bundle) = self.secondary.get_mut(key) {
1318 let scale = bundle.shell.window().map_or(1.0, |w| w.scale_factor());
1319 let focus = bundle.state.focused();
1320 if let Some((_, frame)) = &bundle.last {
1321 if let Some(adapter) = &mut bundle.adapter {
1322 adapter.update_if_active(|| {
1323 crate::access::tree_update(frame, focus, scale)
1324 });
1325 }
1326 } else if let Some(w) = bundle.shell.window() {
1327 w.request_redraw();
1328 }
1329 }
1330 }
1331 },
1332 accesskit_winit::WindowEvent::ActionRequested(req) => {
1333 let id = fenestra_core::WidgetId(req.target_node.0);
1334 match req.action {
1335 accesskit::Action::Click => {
1336 let msg = match &skey {
1337 None => self.last.as_ref().and_then(|(view, frame)| {
1338 fenestra_core::click_msg_of(view, frame, &self.state, id)
1339 }),
1340 Some(key) => self.secondary.get(key).and_then(|bundle| {
1341 bundle.last.as_ref().and_then(|(view, frame)| {
1342 fenestra_core::click_msg_of(
1343 view,
1344 frame,
1345 &bundle.state,
1346 id,
1347 )
1348 })
1349 }),
1350 };
1351 if let Some(msg) = msg {
1352 self.app.update(msg);
1353 self.after_update(event_loop);
1354 }
1355 }
1356 accesskit::Action::Focus => match &skey {
1357 None => {
1358 self.state.set_focus(Some(id));
1359 if let Some(w) = self.shell.window() {
1360 w.request_redraw();
1361 }
1362 }
1363 Some(key) => {
1364 if let Some(bundle) = self.secondary.get_mut(key) {
1365 bundle.state.set_focus(Some(id));
1366 if let Some(w) = bundle.shell.window() {
1367 w.request_redraw();
1368 }
1369 }
1370 }
1371 },
1372 _ => {}
1373 }
1374 }
1375 accesskit_winit::WindowEvent::AccessibilityDeactivated => {}
1376 }
1377 }
1378 }
1379 }
1380
1381 fn suspended(&mut self, _event_loop: &ActiveEventLoop) {
1382 self.shell.suspended();
1383 #[cfg(not(target_arch = "wasm32"))]
1384 for bundle in self.secondary.values_mut() {
1385 bundle.shell.suspended();
1386 }
1387 }
1388
1389 fn new_events(&mut self, _event_loop: &ActiveEventLoop, cause: StartCause) {
1390 if !matches!(cause, StartCause::ResumeTimeReached { .. }) {
1391 return;
1392 }
1393 if let Some(w) = self.shell.window() {
1394 w.request_redraw();
1395 }
1396 #[cfg(not(target_arch = "wasm32"))]
1397 for bundle in self.secondary.values() {
1398 if let Some(w) = bundle.shell.window() {
1399 w.request_redraw();
1400 }
1401 }
1402 }
1403
1404 fn window_event(
1405 &mut self,
1406 event_loop: &ActiveEventLoop,
1407 window_id: WindowId,
1408 event: WindowEvent,
1409 ) {
1410 if self.shell.window().is_none_or(|w| w.id() != window_id) {
1411 #[cfg(not(target_arch = "wasm32"))]
1412 if let Some(key) = self
1413 .secondary
1414 .iter()
1415 .find(|(_, b)| b.shell.window().is_some_and(|w| w.id() == window_id))
1416 .map(|(k, _)| k.clone())
1417 {
1418 self.secondary_window_event(&key, event_loop, event);
1419 }
1420 return;
1421 }
1422 #[cfg(not(target_arch = "wasm32"))]
1423 if let Some(adapter) = &mut self.adapter
1424 && let Some(window) = self.shell.window()
1425 {
1426 adapter.process_event(window, &event);
1427 }
1428 match event {
1429 WindowEvent::CloseRequested => event_loop.exit(),
1430 WindowEvent::Resized(size) => self.shell.resized(size.width, size.height),
1431 WindowEvent::ScaleFactorChanged { .. } => {
1432 if let Some(w) = self.shell.window() {
1433 w.request_redraw();
1434 }
1435 }
1436 WindowEvent::ModifiersChanged(mods) => {
1437 self.modifiers = mods.state();
1438 let m = self.modifiers;
1439 self.input_main(
1440 event_loop,
1441 InputEvent::Modifiers {
1442 shift: m.shift_key(),
1443 ctrl: m.control_key(),
1444 alt: m.alt_key(),
1445 meta: m.super_key(),
1446 },
1447 );
1448 }
1449 WindowEvent::Occluded(occluded) => {
1450 if !occluded && let Some(w) = self.shell.window() {
1451 w.request_redraw();
1452 }
1453 }
1454 WindowEvent::DroppedFile(path) => {
1455 self.input_main(event_loop, InputEvent::FileDrop(path))
1456 }
1457 WindowEvent::CursorLeft { .. } => self.input_main(event_loop, InputEvent::PointerLeave),
1458 WindowEvent::CursorMoved { position, .. } => {
1459 let scale = self.shell.window().map_or(1.0, |w| w.scale_factor());
1460 self.cursor = Point::new(position.x / scale, position.y / scale);
1461 #[expect(clippy::cast_possible_truncation, reason = "positions fit in f32")]
1462 self.input_main(
1463 event_loop,
1464 InputEvent::PointerMove {
1465 x: self.cursor.x as f32,
1466 y: self.cursor.y as f32,
1467 },
1468 );
1469 }
1470 WindowEvent::MouseInput {
1471 state,
1472 button: winit::event::MouseButton::Left,
1473 ..
1474 } => {
1475 self.input_main(
1476 event_loop,
1477 match state {
1478 winit::event::ElementState::Pressed => InputEvent::PointerDown,
1479 winit::event::ElementState::Released => InputEvent::PointerUp,
1480 },
1481 );
1482 }
1483 WindowEvent::MouseInput {
1484 state,
1485 button: winit::event::MouseButton::Right,
1486 ..
1487 } => {
1488 self.input_main(
1489 event_loop,
1490 match state {
1491 winit::event::ElementState::Pressed => InputEvent::RightDown,
1492 winit::event::ElementState::Released => InputEvent::RightUp,
1493 },
1494 );
1495 }
1496 WindowEvent::MouseWheel { delta, .. } => {
1497 let dy = match delta {
1498 MouseScrollDelta::LineDelta(_, y) => f64::from(y) * LINE_SCROLL_PX,
1499 MouseScrollDelta::PixelDelta(pos) => {
1500 let scale = self.shell.window().map_or(1.0, |w| w.scale_factor());
1501 pos.y / scale
1502 }
1503 };
1504 #[expect(clippy::cast_possible_truncation, reason = "deltas fit in f32")]
1505 self.input_main(event_loop, InputEvent::Wheel { dy: dy as f32 });
1506 }
1507 WindowEvent::KeyboardInput { event, .. }
1508 if event.state == winit::event::ElementState::Pressed =>
1509 {
1510 {
1511 let mods = self.modifiers;
1512 let printable = !mods.control_key()
1515 && !mods.super_key()
1516 && event
1517 .text
1518 .as_ref()
1519 .is_some_and(|t| !t.is_empty() && t.chars().all(|c| !c.is_control()));
1520 if printable {
1521 if let Some(t) = &event.text {
1522 self.input_main(event_loop, InputEvent::Text(t.to_string()));
1523 }
1524 } else if let Some(input) = map_key(&event, mods) {
1525 self.input_main(event_loop, input);
1526 }
1527 }
1528 }
1529 WindowEvent::Ime(ime) => match ime {
1530 winit::event::Ime::Preedit(text, cursor) => {
1531 self.input_main(event_loop, InputEvent::ImePreedit { text, cursor });
1532 }
1533 winit::event::Ime::Commit(text) => {
1534 self.input_main(event_loop, InputEvent::Text(text));
1535 }
1536 winit::event::Ime::Enabled | winit::event::Ime::Disabled => {}
1537 },
1538 WindowEvent::RedrawRequested => self.redraw(event_loop),
1539 _ => {}
1540 }
1541 }
1542}