1#![allow(unexpected_cfgs)]
2#![cfg_attr(
3 target_arch = "wasm32",
4 allow(dead_code, unused_imports, unused_variables)
5)]
6
7use anyhow::Result;
8use base64::Engine;
9use fission_core::internal::BuildCtx;
10use std::collections::{HashMap, VecDeque};
11use std::sync::mpsc;
12use std::sync::{Arc, Mutex};
13use std::time::Duration;
14#[cfg(target_arch = "wasm32")]
15use std::{cell::RefCell, rc::Rc};
16#[cfg(feature = "tray")]
17use winit::event::StartCause;
18#[cfg(target_os = "android")]
19use winit::platform::android::{activity::AndroidApp, EventLoopBuilderExtAndroid};
20#[cfg(target_os = "ios")]
21use winit::platform::ios::WindowAttributesExtIOS;
22#[cfg(target_os = "macos")]
23use winit::platform::macos::{ActivationPolicy, EventLoopBuilderExtMacOS};
24#[cfg(target_arch = "wasm32")]
25use winit::platform::web::{EventLoopExtWebSys, WindowAttributesExtWebSys, WindowExtWebSys};
26use winit::{
27 dpi::{PhysicalPosition, PhysicalSize},
28 event::{Event, Ime, MouseButton, MouseScrollDelta, TouchPhase, WindowEvent},
29 event_loop::{
30 ActiveEventLoop as EventLoopWindowTarget, ControlFlow, EventLoop, EventLoopProxy,
31 },
32 window::{CursorIcon, Window, WindowAttributes, WindowId},
33};
34
35use fission_core::env::{VideoStatus, WindowInsets};
36use fission_core::internal::downcast_render_object;
37use fission_core::internal::InternalLoweringCx;
38use fission_core::{
39 Action, ActionId, ActionRegistry, DeepLink, DeepLinkConfig, DeepLinkReceived, Env, GlobalState,
40 InputEvent, KeyCode, KeyEvent as FissionKeyEvent, NotificationResponse,
41 NotificationResponseReceived, OpenUrlRequest, PointerButton, PointerEvent, Runtime,
42 ServiceBindings, View, Widget, WidgetIdExt, OPEN_URL,
43};
44use fission_core::{ActionInput, CapabilityInvocationPayload, Effect};
45use fission_diagnostics::prelude as diag;
46use fission_ir::semantics::MouseCursor;
47use fission_ir::{CoreIR, Op, WidgetId};
48use fission_layout::{LayoutEngine, LayoutSize};
49use fission_render::{LayoutPoint, LayoutRect, Renderer as _};
50use fission_render_vello::parley::FontContext;
51use fission_render_vello::{
52 workload_profile_for_scene, RetainedSceneCache, VelloRenderer, VelloTextMeasurer,
53};
54use fission_shell::async_host::{
55 AsyncMessage, AsyncRegistry, RunningServiceHandle, ServiceControlMessage,
56};
57use fission_shell::{VideoEvent, VideoPlayer};
58use fission_theme::fonts;
59use fontique::{Blob, Collection, CollectionOptions, FontInfoOverride, SourceCache};
60
61use fission_test_driver::TestEvent;
62
63#[cfg(not(target_arch = "wasm32"))]
65use pollster::block_on;
66#[cfg(not(target_arch = "wasm32"))]
67use std::time::Instant;
68use vello::util::{RenderContext, RenderSurface};
69use vello::wgpu;
70use vello::{AaSupport, Renderer as VelloSceneRenderer, RendererOptions, Scene};
71#[cfg(target_arch = "wasm32")]
72use wasm_bindgen::{Clamped, JsCast};
73#[cfg(target_arch = "wasm32")]
74use web_sys::{CanvasRenderingContext2d, HtmlCanvasElement, ImageData};
75#[cfg(target_arch = "wasm32")]
76use web_time::Instant;
77
78mod compositor;
79use compositor::TextureLayerCompositor;
80mod accessibility;
81use accessibility::AccessibilityBridge;
82mod pipeline;
83pub use pipeline::{InvalidationSet, Pipeline};
84mod renderer_diagnostics;
85#[cfg(target_arch = "wasm32")]
86use renderer_diagnostics::renderer_request_from_value;
87use renderer_diagnostics::{emit_renderer_report, RendererReport, RendererRequest};
88mod software_renderer;
89use software_renderer::SoftwareRenderer;
90mod video_backend;
91use video_backend::create_video_backend;
92mod web_backend;
93use web_backend::PlatformWebBackend;
94
95mod clipboard;
96use clipboard::DesktopClipboard;
97pub use clipboard::{ClipboardHost, MemoryClipboardHost};
98#[cfg(not(any(target_os = "android", target_os = "ios", target_arch = "wasm32")))]
99mod file_picker;
100mod geolocation;
101pub use geolocation::{GeolocationHost, MemoryGeolocationHost, UnsupportedGeolocationHost};
102mod haptics;
103pub use haptics::{HapticHost, MemoryHapticHost, UnsupportedHapticHost};
104mod barcode;
105#[cfg(any(target_os = "android", target_os = "ios", target_os = "macos"))]
106mod barcode_decode;
107pub use barcode::{BarcodeScannerHost, MemoryBarcodeScannerHost, UnsupportedBarcodeScannerHost};
108mod biometric;
109pub use biometric::{BiometricHost, MemoryBiometricHost, UnsupportedBiometricHost};
110mod bluetooth;
111pub use bluetooth::{BluetoothHost, MemoryBluetoothHost, UnsupportedBluetoothHost};
112mod camera;
113pub use camera::{CameraHost, MemoryCameraHost, UnsupportedCameraHost};
114mod ime;
115use ime::{DesktopImeHandler, TextInputConfig};
116mod microphone;
117pub use microphone::{MemoryMicrophoneHost, MicrophoneHost, UnsupportedMicrophoneHost};
118mod notifications;
119pub use notifications::{MemoryNotificationHost, NotificationHost, UnsupportedNotificationHost};
120mod nfc;
121pub use nfc::{MemoryNfcHost, NfcHost, UnsupportedNfcHost};
122mod passkey;
123pub use passkey::{MemoryPasskeyHost, PasskeyHost, UnsupportedPasskeyHost};
124#[cfg(feature = "tray")]
125pub mod tray;
126#[cfg(feature = "tray")]
127pub use tray::{
128 TrayActivateBehavior, TrayConfig, TrayHostAction, TrayIconSource, TrayMenu, TrayMenuAction,
129 TrayMenuBuilder, TrayMenuEntry, TrayMenuItem, WindowCloseBehavior,
130};
131pub mod test_control;
132mod wifi;
133pub use wifi::{MemoryWifiHost, UnsupportedWifiHost, WifiHost};
134mod volume;
135pub use volume::{MemoryVolumeHost, UnsupportedVolumeHost, VolumeHost};
136#[cfg(target_os = "android")]
137mod android_capabilities;
138#[cfg(target_os = "ios")]
139mod ios_capabilities;
140#[cfg(target_os = "macos")]
141mod macos_capabilities;
142#[cfg(target_arch = "wasm32")]
143mod web_capabilities;
144
145use fission_core::action::ActionEnvelope;
146
147type EffectResult = AsyncMessage;
148
149type ServiceKey = (String, String);
150type ServiceBindingKey = (String, String, u64);
151
152struct ActiveServiceHandle {
153 runtime: RunningServiceHandle,
154}
155
156#[cfg(not(target_arch = "wasm32"))]
157fn open_host_url(url: &str, _in_app: bool) -> Result<(), String> {
158 if cfg!(target_os = "macos") {
159 std::process::Command::new("open")
160 .arg(url)
161 .spawn()
162 .map(|_| ())
163 .map_err(|error| error.to_string())
164 } else if cfg!(target_os = "windows") {
165 std::process::Command::new("cmd")
166 .args(["/C", "start", url])
167 .spawn()
168 .map(|_| ())
169 .map_err(|error| error.to_string())
170 } else {
171 std::process::Command::new("xdg-open")
172 .arg(url)
173 .spawn()
174 .map(|_| ())
175 .map_err(|error| error.to_string())
176 }
177}
178
179#[cfg(target_arch = "wasm32")]
180fn open_host_url(url: &str, in_app: bool) -> Result<(), String> {
181 let window = web_sys::window().ok_or_else(|| "browser window is not available".to_string())?;
182 if in_app {
183 window.location().set_href(url).map_err(js_error_to_string)
184 } else {
185 window
186 .open_with_url_and_target(url, "_blank")
187 .map_err(js_error_to_string)?
188 .ok_or_else(|| format!("browser blocked opening url `{url}`"))?;
189 Ok(())
190 }
191}
192
193fn register_builtin_operation_capabilities(async_registry: &mut AsyncRegistry) {
194 async_registry.register_operation_capability(
195 OPEN_URL,
196 |request: OpenUrlRequest, _| async move {
197 open_host_url(&request.url, request.in_app)?;
198 Ok(())
199 },
200 );
201 #[cfg(target_arch = "wasm32")]
202 {
203 web_capabilities::register_web_operation_capabilities(async_registry);
204 register_unsupported_file_picker_capability(async_registry);
205 }
206
207 #[cfg(not(target_arch = "wasm32"))]
208 {
209 #[cfg(not(any(target_os = "android", target_os = "ios")))]
210 file_picker::register_file_picker_capability(async_registry);
211 #[cfg(any(target_os = "android", target_os = "ios"))]
212 register_unsupported_file_picker_capability(async_registry);
213
214 notifications::register_notification_capabilities(
215 async_registry,
216 Arc::new(notifications::native_notification_host()),
217 );
218 nfc::register_nfc_capabilities(async_registry, Arc::new(UnsupportedNfcHost));
219 biometric::register_biometric_capabilities(
220 async_registry,
221 Arc::new(UnsupportedBiometricHost),
222 );
223 passkey::register_passkey_capabilities(async_registry, Arc::new(UnsupportedPasskeyHost));
224 bluetooth::register_bluetooth_capabilities(
225 async_registry,
226 Arc::new(UnsupportedBluetoothHost),
227 );
228 barcode::register_barcode_scanner_capabilities(
229 async_registry,
230 Arc::new(UnsupportedBarcodeScannerHost),
231 );
232 camera::register_camera_capabilities(async_registry, Arc::new(UnsupportedCameraHost));
233 clipboard::register_clipboard_capabilities(
234 async_registry,
235 Arc::new(DesktopClipboard::new()),
236 );
237 geolocation::register_geolocation_capabilities(
238 async_registry,
239 Arc::new(UnsupportedGeolocationHost),
240 );
241 haptics::register_haptic_capabilities(
242 async_registry,
243 Arc::new(haptics::native_haptic_host()),
244 );
245 microphone::register_microphone_capabilities(
246 async_registry,
247 Arc::new(UnsupportedMicrophoneHost),
248 );
249 wifi::register_wifi_capabilities(async_registry, Arc::new(UnsupportedWifiHost));
250 volume::register_volume_capabilities(
251 async_registry,
252 Arc::new(volume::native_volume_host()),
253 );
254 #[cfg(target_os = "macos")]
255 macos_capabilities::register_macos_operation_capabilities(async_registry);
256 #[cfg(target_os = "ios")]
257 ios_capabilities::register_ios_operation_capabilities(async_registry);
258 }
259}
260
261#[cfg(any(target_os = "android", target_os = "ios", target_arch = "wasm32"))]
262fn register_unsupported_file_picker_capability(async_registry: &mut AsyncRegistry) {
263 async_registry.register_operation_capability(
264 fission_core::PICK_OPEN_FILES,
265 |_request: fission_core::PickOpenFilesRequest, _| async move {
266 Err::<fission_core::PickOpenFilesResult, _>(
267 fission_core::PickOpenFilesError::unsupported("pick_open_files"),
268 )
269 },
270 );
271}
272
273fn collect_startup_deep_links(config: &DeepLinkConfig) -> Vec<DeepLink> {
274 let args = std::env::args().skip(1).collect::<Vec<_>>();
275 let mut env_values = Vec::new();
276 if let Ok(value) = std::env::var("FISSION_DEEP_LINK_URL") {
277 env_values.push(value);
278 }
279 if let Ok(value) = std::env::var("FISSION_DEEP_LINKS") {
280 env_values.extend(
281 value
282 .split('\n')
283 .map(str::trim)
284 .filter(|value| !value.is_empty())
285 .map(ToOwned::to_owned),
286 );
287 }
288
289 #[cfg(target_arch = "wasm32")]
290 if let Some(window) = web_sys::window() {
291 if let Ok(href) = window.location().href() {
292 env_values.push(href);
293 }
294 }
295
296 collect_startup_deep_links_from(config, args, env_values)
297}
298
299fn collect_startup_deep_links_from(
300 config: &DeepLinkConfig,
301 args: impl IntoIterator<Item = String>,
302 env_values: impl IntoIterator<Item = String>,
303) -> Vec<DeepLink> {
304 let mut links = Vec::new();
305 for url in env_values.into_iter().chain(args) {
306 if config.matches(&url) {
307 links.push(
308 DeepLink::new(url.clone())
309 .cold_start(true)
310 .source(config.source_for(&url)),
311 );
312 }
313 }
314 links
315}
316
317#[cfg(target_arch = "wasm32")]
318fn js_error_to_string(error: wasm_bindgen::JsValue) -> String {
319 error
320 .as_string()
321 .unwrap_or_else(|| format!("JavaScript error: {error:?}"))
322}
323
324struct ActivePlayer {
325 player: Box<dyn VideoPlayer>,
326 last_status: Option<VideoStatus>,
327 last_rate: Option<f32>,
328 last_volume: Option<f32>,
329 last_muted: Option<bool>,
330}
331
332struct RenderState<'w> {
333 surface: RenderSurface<'w>,
334 target_texture_size: (u32, u32),
335 #[cfg(feature = "three-d")]
336 scene3d_renderer: fission_3d::render::Scene3DRenderer,
337 main_renderer: MainRenderer,
338 renderer_report: RendererReport,
339}
340
341enum MainRenderer {
342 Vello {
343 renderer: VelloSceneRenderer,
344 texture_compositor: TextureLayerCompositor,
345 },
346 Software,
347}
348
349#[cfg(target_arch = "wasm32")]
350struct WebCanvasPresenter {
351 canvas: HtmlCanvasElement,
352 context: CanvasRenderingContext2d,
353 report: RendererReport,
354}
355
356#[cfg(target_arch = "wasm32")]
357impl WebCanvasPresenter {
358 fn new(window: &Window) -> anyhow::Result<Self> {
359 let canvas = window
360 .canvas()
361 .ok_or_else(|| anyhow::anyhow!("winit web window did not expose a canvas"))?;
362 let context = canvas
363 .get_context("2d")
364 .map_err(|error| anyhow::anyhow!(js_error_to_string(error)))?
365 .ok_or_else(|| anyhow::anyhow!("2D canvas context is unavailable"))?
366 .dyn_into::<CanvasRenderingContext2d>()
367 .map_err(|error| anyhow::anyhow!(js_error_to_string(error.into())))?;
368 Ok(Self {
369 canvas,
370 context,
371 report: RendererReport::new(
372 "canvas2d-software",
373 web_renderer_request(),
374 None,
375 None,
376 None,
377 0,
378 0,
379 1.0,
380 ),
381 })
382 }
383
384 fn present(
385 &mut self,
386 rgba: &[u8],
387 width: u32,
388 height: u32,
389 scale_factor: f64,
390 ) -> anyhow::Result<()> {
391 self.canvas.set_width(width.max(1));
392 self.canvas.set_height(height.max(1));
393 self.report.width = width.max(1);
394 self.report.height = height.max(1);
395 self.report.scale_factor = scale_factor;
396 let image =
397 ImageData::new_with_u8_clamped_array_and_sh(Clamped(rgba), width.max(1), height.max(1))
398 .map_err(|error| anyhow::anyhow!(js_error_to_string(error)))?;
399 self.context
400 .put_image_data(&image, 0.0, 0.0)
401 .map_err(|error| anyhow::anyhow!(js_error_to_string(error)))?;
402 Ok(())
403 }
404}
405
406#[cfg(target_arch = "wasm32")]
407struct WebGpuPresenter {
408 render_cx: RenderContext,
409 render_state: RenderState<'static>,
410 scene: Scene,
411 retained_scene_cache: RetainedSceneCache,
412}
413
414#[cfg(target_arch = "wasm32")]
415enum WebRenderer {
416 WebGpu(WebGpuPresenter),
417 Canvas2d(WebCanvasPresenter),
418}
419
420#[cfg(target_arch = "wasm32")]
421impl WebRenderer {
422 fn report(&self) -> &RendererReport {
423 match self {
424 Self::WebGpu(presenter) => &presenter.render_state.renderer_report,
425 Self::Canvas2d(presenter) => &presenter.report,
426 }
427 }
428
429 fn active_name(&self) -> &str {
430 self.report().active.as_str()
431 }
432}
433
434#[cfg(target_arch = "wasm32")]
435type PendingWebGpuInit = Rc<RefCell<Option<Result<WebGpuPresenter, String>>>>;
436
437#[derive(Debug, Clone, Copy, PartialEq)]
438struct WindowViewportState {
439 physical_size: PhysicalSize<u32>,
440 scale_factor: f64,
441}
442
443impl WindowViewportState {
444 fn from_window(window: &Window) -> Self {
445 #[cfg(target_arch = "wasm32")]
446 if let Some(viewport) = web_browser_viewport_state() {
447 return viewport;
448 }
449
450 let reported_scale_factor = normalize_scale_factor(window.scale_factor());
451 #[cfg(target_os = "ios")]
452 {
453 let mut physical_size = window.outer_size();
457 let effective_scale_factor = ios_effective_scale_factor(reported_scale_factor);
458 if effective_scale_factor > reported_scale_factor && reported_scale_factor <= 1.0 {
459 physical_size = logical_viewport_to_physical_size(
460 LayoutSize::new(physical_size.width as f32, physical_size.height as f32),
461 effective_scale_factor,
462 );
463 }
464 return Self {
465 physical_size,
466 scale_factor: effective_scale_factor,
467 };
468 }
469
470 #[cfg(not(target_os = "ios"))]
471 {
472 Self {
473 physical_size: window.inner_size(),
474 scale_factor: reported_scale_factor,
475 }
476 }
477 }
478
479 fn logical_size(self) -> LayoutSize {
480 physical_size_to_layout_size(self.physical_size, self.scale_factor)
481 }
482
483 fn with_physical_size(self, physical_size: PhysicalSize<u32>) -> Self {
484 Self {
485 physical_size,
486 ..self
487 }
488 }
489
490 fn with_logical_size(self, logical_size: LayoutSize) -> Self {
491 self.with_physical_size(logical_viewport_to_physical_size(
492 logical_size,
493 self.scale_factor,
494 ))
495 }
496
497 #[cfg(any(test, not(target_os = "ios")))]
498 fn with_scale_factor(self, scale_factor: f64) -> Self {
499 let scale_factor = normalize_scale_factor(scale_factor);
500 let logical_size = self.logical_size();
501 Self {
502 physical_size: logical_viewport_to_physical_size(logical_size, scale_factor),
503 scale_factor,
504 }
505 }
506}
507
508#[cfg(any(test, target_os = "ios"))]
509fn window_insets_from_safe_area_frames(
510 inner_position: PhysicalPosition<i32>,
511 outer_position: PhysicalPosition<i32>,
512 inner_size: PhysicalSize<u32>,
513 outer_size: PhysicalSize<u32>,
514 scale_factor: f64,
515) -> WindowInsets {
516 let scale_factor = normalize_scale_factor(scale_factor) as f32;
517 let left_px = (inner_position.x - outer_position.x).max(0) as i64;
518 let top_px = (inner_position.y - outer_position.y).max(0) as i64;
519 let right_px = (outer_size.width as i64 - inner_size.width as i64 - left_px).max(0);
520 let bottom_px = (outer_size.height as i64 - inner_size.height as i64 - top_px).max(0);
521
522 WindowInsets {
523 top: top_px as f32 / scale_factor,
524 bottom: bottom_px as f32 / scale_factor,
525 left: left_px as f32 / scale_factor,
526 right: right_px as f32 / scale_factor,
527 }
528}
529
530fn window_safe_area_insets(window: &Window, scale_factor: f64) -> WindowInsets {
531 #[cfg(target_os = "ios")]
532 {
533 if let (Ok(inner_position), Ok(outer_position)) =
534 (window.inner_position(), window.outer_position())
535 {
536 return window_insets_from_safe_area_frames(
537 inner_position,
538 outer_position,
539 window.inner_size(),
540 window.outer_size(),
541 scale_factor,
542 );
543 }
544 }
545
546 let _ = (window, scale_factor);
547 WindowInsets::default()
548}
549
550#[cfg(not(target_arch = "wasm32"))]
551fn create_render_state<'w>(
552 render_cx: &mut RenderContext,
553 window: Arc<Window>,
554 viewport: WindowViewportState,
555) -> anyhow::Result<RenderState<'w>> {
556 let mut surface = block_on(render_cx.create_surface(
557 window.clone(),
558 viewport.physical_size.width,
559 viewport.physical_size.height,
560 wgpu::PresentMode::AutoVsync,
561 ))
562 .map_err(|error| anyhow::anyhow!("failed to create render surface: {error}"))?;
563
564 let device_handle = &render_cx.devices[surface.dev_id];
565 #[cfg(target_os = "ios")]
566 device_handle.device.on_uncaptured_error(Box::new(|error| {
567 eprintln!("wgpu uncaptured error: {error}");
568 }));
569 let surface_caps = surface.surface.get_capabilities(device_handle.adapter());
570 surface.config.alpha_mode = surface_caps
571 .alpha_modes
572 .iter()
573 .copied()
574 .find(|mode| *mode == wgpu::CompositeAlphaMode::PostMultiplied)
575 .unwrap_or_else(|| {
576 surface_caps
577 .alpha_modes
578 .first()
579 .copied()
580 .unwrap_or(wgpu::CompositeAlphaMode::Opaque)
581 });
582 surface
583 .surface
584 .configure(&device_handle.device, &surface.config);
585
586 let target_texture_size = (surface.config.width, surface.config.height);
587 recreate_target_texture(
588 &mut surface,
589 render_cx,
590 target_texture_size.0,
591 target_texture_size.1,
592 );
593
594 #[cfg(feature = "three-d")]
595 let scene3d_renderer = fission_3d::render::Scene3DRenderer::new(
596 &device_handle.device,
597 viewport.physical_size.width,
598 viewport.physical_size.height,
599 wgpu::TextureFormat::Rgba8Unorm,
600 );
601
602 let request = native_renderer_request();
603 let supports_indirect_execution = device_handle
604 .adapter()
605 .get_downlevel_capabilities()
606 .flags
607 .contains(wgpu::DownlevelFlags::INDIRECT_EXECUTION);
608 let (main_renderer, renderer_report) = create_native_main_renderer(
609 device_handle,
610 request,
611 supports_indirect_execution,
612 viewport.physical_size.width,
613 viewport.physical_size.height,
614 viewport.scale_factor,
615 )?;
616 emit_renderer_report(&renderer_report);
617
618 Ok(RenderState {
619 surface,
620 target_texture_size,
621 #[cfg(feature = "three-d")]
622 scene3d_renderer,
623 main_renderer,
624 renderer_report,
625 })
626}
627
628#[cfg(not(target_arch = "wasm32"))]
629fn present_startup_clear_frame(
630 render_state: &mut RenderState<'_>,
631 render_cx: &RenderContext,
632 clear_color: wgpu::Color,
633) -> anyhow::Result<()> {
634 let surface_texture = render_state
635 .surface
636 .surface
637 .get_current_texture()
638 .map_err(|error| anyhow::anyhow!("failed to get startup surface texture: {error}"))?;
639 let target_view = surface_texture
640 .texture
641 .create_view(&wgpu::TextureViewDescriptor::default());
642 let device_handle = &render_cx.devices[render_state.surface.dev_id];
643 let mut encoder =
644 device_handle
645 .device
646 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
647 label: Some("Fission startup clear encoder"),
648 });
649 {
650 let _pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
651 label: Some("Fission startup clear pass"),
652 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
653 view: &target_view,
654 resolve_target: None,
655 depth_slice: None,
656 ops: wgpu::Operations {
657 load: wgpu::LoadOp::Clear(clear_color),
658 store: wgpu::StoreOp::Store,
659 },
660 })],
661 depth_stencil_attachment: None,
662 timestamp_writes: None,
663 occlusion_query_set: None,
664 });
665 }
666 device_handle.queue.submit(Some(encoder.finish()));
667 surface_texture.present();
668 Ok(())
669}
670
671#[cfg(not(target_arch = "wasm32"))]
672fn theme_background_wgpu_color(env: &Env) -> wgpu::Color {
673 wgpu::Color {
674 r: f64::from(env.theme.tokens.colors.background.r) / 255.0,
675 g: f64::from(env.theme.tokens.colors.background.g) / 255.0,
676 b: f64::from(env.theme.tokens.colors.background.b) / 255.0,
677 a: f64::from(env.theme.tokens.colors.background.a) / 255.0,
678 }
679}
680
681#[cfg(not(target_arch = "wasm32"))]
682fn native_renderer_request() -> RendererRequest {
683 let request = RendererRequest::from_env();
684 let force_cpu_vello = std::env::var("FISSION_VELLO_USE_CPU")
685 .map(|value| matches!(value.to_ascii_lowercase().as_str(), "1" | "true" | "yes"))
686 .unwrap_or(false);
687 if force_cpu_vello {
688 RendererRequest::NativeVelloCpu
689 } else {
690 request
691 }
692}
693
694#[cfg(target_arch = "wasm32")]
695fn web_renderer_request() -> RendererRequest {
696 if let Some(window) = web_sys::window() {
697 if let Ok(search) = window.location().search() {
698 if let Some(value) = query_param(&search, "fission_renderer") {
699 return renderer_request_from_value(Some(&value));
700 }
701 }
702 let global = js_sys::global();
703 if let Ok(value) = js_sys::Reflect::get(
704 &global,
705 &wasm_bindgen::JsValue::from_str("FISSION_RENDERER"),
706 ) {
707 if let Some(value) = value.as_string() {
708 return renderer_request_from_value(Some(&value));
709 }
710 }
711 }
712 RendererRequest::Auto
713}
714
715#[cfg(target_arch = "wasm32")]
716fn query_param(search: &str, name: &str) -> Option<String> {
717 let search = search.strip_prefix('?').unwrap_or(search);
718 search.split('&').find_map(|part| {
719 let mut pieces = part.splitn(2, '=');
720 let key = pieces.next()?;
721 if key == name {
722 pieces.next().map(|value| value.replace('+', " "))
723 } else {
724 None
725 }
726 })
727}
728
729#[cfg(not(target_arch = "wasm32"))]
730fn create_native_main_renderer(
731 device_handle: &vello::util::DeviceHandle,
732 request: RendererRequest,
733 supports_indirect_execution: bool,
734 width: u32,
735 height: u32,
736 scale_factor: f64,
737) -> anyhow::Result<(MainRenderer, RendererReport)> {
738 let (backend, adapter) = adapter_labels(device_handle.adapter());
739 if matches!(request, RendererRequest::NativeSoftware) {
740 return Ok((
741 MainRenderer::Software,
742 RendererReport::new(
743 "native-software-upload",
744 request,
745 backend,
746 adapter,
747 Some("forced_by_renderer_request".to_string()),
748 width,
749 height,
750 scale_factor,
751 ),
752 ));
753 }
754
755 let cpu_requested = matches!(request, RendererRequest::NativeVelloCpu);
756 match create_vello_main_renderer(device_handle, cpu_requested, supports_indirect_execution) {
757 Ok(renderer) => {
758 let active = if cpu_requested {
759 "native-vello-cpu"
760 } else if cfg!(target_os = "ios") || cfg!(target_os = "macos") {
761 "metal-vello"
762 } else {
763 "native-vello"
764 };
765 Ok((
766 renderer,
767 RendererReport::new(
768 active,
769 request,
770 backend,
771 adapter,
772 if matches!(request, RendererRequest::NativeVelloCpu) {
773 Some("forced_cpu_vello".to_string())
774 } else if !supports_indirect_execution {
775 Some("direct_dispatch_fallback".to_string())
776 } else if cpu_requested {
777 Some("missing_indirect_execution".to_string())
778 } else {
779 None
780 },
781 width,
782 height,
783 scale_factor,
784 ),
785 ))
786 }
787 Err(gpu_error) if request.is_explicit_gpu() => Err(anyhow::anyhow!(
788 "requested native Vello GPU renderer but initialization failed: {gpu_error}"
789 )),
790 Err(gpu_error) => match create_vello_main_renderer(device_handle, true, true) {
791 Ok(renderer) => Ok((
792 renderer,
793 RendererReport::new(
794 "native-vello-cpu",
795 request,
796 backend,
797 adapter,
798 Some(format!("gpu_vello_init_failed:{gpu_error}")),
799 width,
800 height,
801 scale_factor,
802 ),
803 )),
804 Err(cpu_error) => Ok((
805 MainRenderer::Software,
806 RendererReport::new(
807 "native-software-upload",
808 request,
809 backend,
810 adapter,
811 Some(format!(
812 "gpu_vello_init_failed:{gpu_error};cpu_vello_init_failed:{cpu_error}"
813 )),
814 width,
815 height,
816 scale_factor,
817 ),
818 )),
819 },
820 }
821}
822
823#[cfg(not(target_arch = "wasm32"))]
824fn create_vello_main_renderer(
825 device_handle: &vello::util::DeviceHandle,
826 use_cpu: bool,
827 use_indirect_dispatch: bool,
828) -> anyhow::Result<MainRenderer> {
829 let renderer = VelloSceneRenderer::new(
830 &device_handle.device,
831 RendererOptions {
832 use_cpu,
833 use_indirect_dispatch,
834 antialiasing_support: AaSupport::all(),
835 num_init_threads: None,
836 pipeline_cache: None,
837 },
838 )
839 .map_err(|error| anyhow::anyhow!("failed to create vello renderer: {error}"))?;
840
841 let texture_compositor =
842 TextureLayerCompositor::new(&device_handle.device, wgpu::TextureFormat::Rgba8Unorm);
843 Ok(MainRenderer::Vello {
844 renderer,
845 texture_compositor,
846 })
847}
848
849fn adapter_labels(adapter: &wgpu::Adapter) -> (Option<String>, Option<String>) {
850 let info = adapter.get_info();
851 let backend = Some(format!("{:?}", info.backend));
852 let adapter = (!info.name.trim().is_empty()).then_some(info.name);
853 (backend, adapter)
854}
855
856#[cfg(target_arch = "wasm32")]
857async fn create_webgpu_presenter(
858 canvas: HtmlCanvasElement,
859 viewport: WindowViewportState,
860 request: RendererRequest,
861) -> anyhow::Result<WebGpuPresenter> {
862 canvas.set_width(viewport.physical_size.width.max(1));
863 canvas.set_height(viewport.physical_size.height.max(1));
864 let mut render_cx = RenderContext::new();
865 let surface = render_cx
866 .instance
867 .create_surface(wgpu::SurfaceTarget::Canvas(canvas))
868 .map_err(|error| anyhow::anyhow!("failed to create webgpu canvas surface: {error}"))?;
869 let mut surface = render_cx
870 .create_render_surface(
871 surface,
872 viewport.physical_size.width,
873 viewport.physical_size.height,
874 wgpu::PresentMode::AutoVsync,
875 )
876 .await
877 .map_err(|error| anyhow::anyhow!("failed to create webgpu render surface: {error}"))?;
878
879 let device_handle = &render_cx.devices[surface.dev_id];
880 let surface_caps = surface.surface.get_capabilities(device_handle.adapter());
881 surface.config.alpha_mode = surface_caps
882 .alpha_modes
883 .iter()
884 .copied()
885 .find(|mode| *mode == wgpu::CompositeAlphaMode::PostMultiplied)
886 .unwrap_or_else(|| {
887 surface_caps
888 .alpha_modes
889 .first()
890 .copied()
891 .unwrap_or(wgpu::CompositeAlphaMode::Opaque)
892 });
893 surface
894 .surface
895 .configure(&device_handle.device, &surface.config);
896
897 let target_texture_size = (surface.config.width, surface.config.height);
898 recreate_target_texture(
899 &mut surface,
900 &render_cx,
901 target_texture_size.0,
902 target_texture_size.1,
903 );
904 let main_renderer = create_webgpu_main_renderer(device_handle, request)?;
905 let active_renderer = match &main_renderer {
906 MainRenderer::Vello { .. } => "webgpu-vello",
907 MainRenderer::Software => "webgpu-software",
908 };
909 let (backend, adapter) = adapter_labels(device_handle.adapter());
910 let renderer_report = RendererReport::new(
911 active_renderer,
912 request,
913 backend,
914 adapter,
915 None,
916 viewport.physical_size.width,
917 viewport.physical_size.height,
918 viewport.scale_factor,
919 );
920 let render_state = RenderState {
921 surface,
922 target_texture_size,
923 #[cfg(feature = "three-d")]
924 scene3d_renderer: fission_3d::render::Scene3DRenderer::new(
925 &device_handle.device,
926 viewport.physical_size.width,
927 viewport.physical_size.height,
928 wgpu::TextureFormat::Rgba8Unorm,
929 ),
930 main_renderer,
931 renderer_report,
932 };
933 Ok(WebGpuPresenter {
934 render_cx,
935 render_state,
936 scene: Scene::new(),
937 retained_scene_cache: RetainedSceneCache::default(),
938 })
939}
940
941#[cfg(target_arch = "wasm32")]
942fn create_webgpu_main_renderer(
943 device_handle: &vello::util::DeviceHandle,
944 request: RendererRequest,
945) -> anyhow::Result<MainRenderer> {
946 if matches!(request, RendererRequest::Canvas2dSoftware) {
947 return Err(anyhow::anyhow!(
948 "webgpu renderer disabled by renderer request"
949 ));
950 }
951 let renderer = VelloSceneRenderer::new(
952 &device_handle.device,
953 RendererOptions {
954 use_cpu: false,
955 use_indirect_dispatch: true,
956 antialiasing_support: AaSupport::all(),
957 num_init_threads: None,
958 pipeline_cache: None,
959 },
960 )
961 .map_err(|error| anyhow::anyhow!("failed to create webgpu Vello renderer: {error}"))?;
962 let texture_compositor =
963 TextureLayerCompositor::new(&device_handle.device, wgpu::TextureFormat::Rgba8Unorm);
964 Ok(MainRenderer::Vello {
965 renderer,
966 texture_compositor,
967 })
968}
969
970#[cfg(target_arch = "wasm32")]
971fn publish_web_renderer_report(report: &RendererReport) {
972 let line = report.concise_line();
973 web_sys::console::info_1(&wasm_bindgen::JsValue::from_str(&format!(
974 "fission-shell-winit: {line}"
975 )));
976 set_web_global_json("__FISSION_RENDERER_INFO", report);
977 post_web_runtime_event("/__fission/renderer", report);
978}
979
980#[cfg(target_arch = "wasm32")]
981#[derive(serde::Serialize)]
982struct WebFramePerf<'a> {
983 renderer: &'a str,
984 total_ms: f64,
985}
986
987#[cfg(target_arch = "wasm32")]
988#[derive(serde::Serialize)]
989struct WebInputLatency<'a> {
990 renderer: &'a str,
991 latency_ms: f64,
992}
993
994#[cfg(target_arch = "wasm32")]
995fn publish_web_frame_perf(renderer: &str, total_ms: f64) {
996 let perf = WebFramePerf { renderer, total_ms };
997 append_web_perf_sample("frames", total_ms);
998 diag::emit(
999 diag::DiagCategory::Frame,
1000 diag::DiagLevel::Debug,
1001 diag::DiagEventKind::FramePerformance {
1002 renderer: renderer.to_string(),
1003 total_ms,
1004 },
1005 );
1006 set_web_global_json("__FISSION_LAST_FRAME_PERF", &perf);
1007}
1008
1009#[cfg(target_arch = "wasm32")]
1010fn publish_web_input_latency(renderer: &str, latency_ms: f64) {
1011 let latency = WebInputLatency {
1012 renderer,
1013 latency_ms,
1014 };
1015 append_web_perf_sample("inputLatencies", latency_ms);
1016 diag::emit(
1017 diag::DiagCategory::Input,
1018 diag::DiagLevel::Debug,
1019 diag::DiagEventKind::InputLatency {
1020 renderer: renderer.to_string(),
1021 latency_ms,
1022 },
1023 );
1024 set_web_global_json("__FISSION_LAST_INPUT_LATENCY", &latency);
1025}
1026
1027#[cfg(target_arch = "wasm32")]
1028fn set_web_global_json<T: serde::Serialize>(name: &str, value: &T) {
1029 let Ok(json) = serde_json::to_string(value) else {
1030 return;
1031 };
1032 let Ok(js_value) = js_sys::JSON::parse(&json) else {
1033 return;
1034 };
1035 let _ = js_sys::Reflect::set(
1036 &js_sys::global(),
1037 &wasm_bindgen::JsValue::from_str(name),
1038 &js_value,
1039 );
1040}
1041
1042#[cfg(target_arch = "wasm32")]
1043fn append_web_perf_sample(name: &str, value: f64) {
1044 let global = js_sys::global();
1045 let key = wasm_bindgen::JsValue::from_str("__FISSION_PERF");
1046 let perf = js_sys::Reflect::get(&global, &key)
1047 .ok()
1048 .filter(|value| value.is_object())
1049 .unwrap_or_else(|| {
1050 let object = js_sys::Object::new();
1051 let _ = js_sys::Reflect::set(&global, &key, &object);
1052 object.into()
1053 });
1054 let sample_key = wasm_bindgen::JsValue::from_str(name);
1055 let samples = js_sys::Reflect::get(&perf, &sample_key)
1056 .ok()
1057 .and_then(|value| value.dyn_into::<js_sys::Array>().ok())
1058 .unwrap_or_else(|| {
1059 let array = js_sys::Array::new();
1060 let _ = js_sys::Reflect::set(&perf, &sample_key, &array);
1061 array
1062 });
1063 samples.push(&wasm_bindgen::JsValue::from_f64(value));
1064 while samples.length() > 240 {
1065 samples.shift();
1066 }
1067}
1068
1069#[cfg(target_arch = "wasm32")]
1070fn post_web_runtime_event<T: serde::Serialize>(path: &str, value: &T) {
1071 let Some(window) = web_sys::window() else {
1072 return;
1073 };
1074 let Ok(body) = serde_json::to_string(value) else {
1075 return;
1076 };
1077 let init = web_sys::RequestInit::new();
1078 init.set_method("POST");
1079 init.set_mode(web_sys::RequestMode::SameOrigin);
1080 init.set_body(&wasm_bindgen::JsValue::from_str(&body));
1081 let Ok(request) = web_sys::Request::new_with_str_and_init(path, &init) else {
1082 return;
1083 };
1084 let _ = request.headers().set("content-type", "application/json");
1085 wasm_bindgen_futures::spawn_local(async move {
1086 let _ = wasm_bindgen_futures::JsFuture::from(window.fetch_with_request(&request)).await;
1087 });
1088}
1089
1090#[cfg(target_arch = "wasm32")]
1091fn web_bool_global(name: &str) -> bool {
1092 js_sys::Reflect::get(&js_sys::global(), &wasm_bindgen::JsValue::from_str(name))
1093 .ok()
1094 .and_then(|value| {
1095 value.as_bool().or_else(|| {
1096 value
1097 .as_string()
1098 .map(|s| matches!(s.as_str(), "1" | "true" | "yes"))
1099 })
1100 })
1101 .unwrap_or(false)
1102}
1103
1104#[cfg(target_os = "android")]
1105fn build_window(
1106 title: &str,
1107 background_test_mode: bool,
1108 target: &EventLoopWindowTarget,
1109 _web_mount_selector: Option<&str>,
1110) -> anyhow::Result<Arc<Window>> {
1111 let reported_scale_factor = target
1112 .primary_monitor()
1113 .map(|monitor| monitor.scale_factor());
1114 let window_attributes = build_window_attributes(
1115 title,
1116 background_test_mode,
1117 _web_mount_selector,
1118 reported_scale_factor,
1119 )?;
1120 Ok(Arc::new(target.create_window(window_attributes).map_err(
1121 |e| anyhow::anyhow!("Window build error: {}", e),
1122 )?))
1123}
1124
1125#[cfg(not(target_os = "android"))]
1126fn build_window_before_run(
1127 title: &str,
1128 background_test_mode: bool,
1129 event_loop: &EventLoop<TestEvent>,
1130 _web_mount_selector: Option<&str>,
1131) -> anyhow::Result<Arc<Window>> {
1132 let window_attributes =
1133 build_window_attributes(title, background_test_mode, _web_mount_selector, None)?;
1134 #[allow(deprecated)]
1135 Ok(Arc::new(
1136 event_loop
1137 .create_window(window_attributes)
1138 .map_err(|e| anyhow::anyhow!("Window build error: {}", e))?,
1139 ))
1140}
1141
1142fn build_window_attributes(
1143 title: &str,
1144 background_test_mode: bool,
1145 _web_mount_selector: Option<&str>,
1146 _reported_scale_factor: Option<f64>,
1147) -> anyhow::Result<WindowAttributes> {
1148 let mut window_attributes = WindowAttributes::default().with_title(title);
1149 #[cfg(target_os = "ios")]
1150 {
1151 let reported_scale_factor = _reported_scale_factor.unwrap_or(1.0);
1156 window_attributes = window_attributes.with_scale_factor(ios_effective_scale_factor(
1157 normalize_scale_factor(reported_scale_factor),
1158 ));
1159 }
1160 #[cfg(target_arch = "wasm32")]
1161 {
1162 window_attributes = window_attributes.with_prevent_default(true);
1163 window_attributes = if let Some(selector) = _web_mount_selector {
1164 window_attributes.with_canvas(Some(canvas_for_mount_selector(selector)?))
1165 } else {
1166 window_attributes.with_append(true)
1167 };
1168 }
1169 if background_test_mode {
1170 window_attributes = window_attributes.with_active(false).with_visible(false);
1171 } else if accessibility::window_must_start_hidden() {
1172 window_attributes = window_attributes.with_visible(false);
1176 }
1177 Ok(window_attributes)
1178}
1179
1180#[cfg(target_arch = "wasm32")]
1181fn canvas_for_mount_selector(selector: &str) -> anyhow::Result<web_sys::HtmlCanvasElement> {
1182 use wasm_bindgen::JsCast;
1183
1184 let window =
1185 web_sys::window().ok_or_else(|| anyhow::anyhow!("browser window is not available"))?;
1186 let document = window
1187 .document()
1188 .ok_or_else(|| anyhow::anyhow!("browser document is not available"))?;
1189 let element = document
1190 .query_selector(selector)
1191 .map_err(|error| {
1192 anyhow::anyhow!(
1193 "invalid web mount selector `{}`: {}",
1194 selector,
1195 js_error_to_string(error)
1196 )
1197 })?
1198 .ok_or_else(|| {
1199 anyhow::anyhow!(
1200 "web mount selector `{}` did not match any element",
1201 selector
1202 )
1203 })?;
1204
1205 if let Ok(canvas) = element.clone().dyn_into::<web_sys::HtmlCanvasElement>() {
1206 apply_web_canvas_style(&canvas)?;
1207 return Ok(canvas);
1208 }
1209
1210 let canvas = document
1211 .create_element("canvas")
1212 .map_err(|error| {
1213 anyhow::anyhow!("failed to create web canvas: {}", js_error_to_string(error))
1214 })?
1215 .dyn_into::<web_sys::HtmlCanvasElement>()
1216 .map_err(|_| anyhow::anyhow!("browser created a non-canvas element for `<canvas>`"))?;
1217 element.append_child(&canvas).map_err(|error| {
1218 anyhow::anyhow!(
1219 "failed to append web canvas to `{}`: {}",
1220 selector,
1221 js_error_to_string(error)
1222 )
1223 })?;
1224 apply_web_canvas_style(&canvas)?;
1225 Ok(canvas)
1226}
1227
1228#[cfg(target_arch = "wasm32")]
1229fn apply_web_canvas_style(canvas: &web_sys::HtmlCanvasElement) -> anyhow::Result<()> {
1230 let existing = canvas.get_attribute("style").unwrap_or_default();
1231 let suffix = "display:block;width:100%;height:100%;border:0;outline:none;user-select:none;-webkit-user-drag:none;touch-action:none;-webkit-tap-highlight-color:transparent;";
1232 let style = if existing.trim().is_empty() {
1233 suffix.to_string()
1234 } else {
1235 format!("{existing};{suffix}")
1236 };
1237 canvas.set_attribute("style", &style).map_err(|error| {
1238 anyhow::anyhow!("failed to style web canvas: {}", js_error_to_string(error))
1239 })?;
1240 Ok(())
1241}
1242
1243trait PlatformWindow {
1244 fn active_window(&self) -> Option<&Window>;
1245 fn active_window_arc(&self) -> Option<Arc<Window>>;
1246
1247 fn active_window_id(&self) -> Option<WindowId> {
1248 self.active_window().map(Window::id)
1249 }
1250}
1251
1252#[cfg(target_os = "android")]
1253impl PlatformWindow for Option<Arc<Window>> {
1254 fn active_window(&self) -> Option<&Window> {
1255 self.as_deref()
1256 }
1257
1258 fn active_window_arc(&self) -> Option<Arc<Window>> {
1259 self.clone()
1260 }
1261}
1262
1263#[cfg(not(target_os = "android"))]
1264impl PlatformWindow for Arc<Window> {
1265 fn active_window(&self) -> Option<&Window> {
1266 Some(self)
1267 }
1268
1269 fn active_window_arc(&self) -> Option<Arc<Window>> {
1270 Some(self.clone())
1271 }
1272}
1273
1274fn request_redraw_throttled(
1275 window: &Window,
1276 elwt: &EventLoopWindowTarget,
1277 last_redraw_at: &mut Instant,
1278 min_frame: Duration,
1279 redraw_pending: &mut bool,
1280) {
1281 let now = Instant::now();
1282 let next = *last_redraw_at + min_frame;
1283 if now >= next {
1284 *last_redraw_at = now;
1285 *redraw_pending = false;
1286 window.request_redraw();
1287 } else {
1288 *redraw_pending = true;
1289 elwt.set_control_flow(ControlFlow::WaitUntil(next));
1290 }
1291}
1292
1293fn frame_trace_enabled() -> bool {
1294 static ENABLED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
1295 *ENABLED.get_or_init(|| {
1296 std::env::var("FISSION_FRAME_TRACE")
1297 .map(|v| matches!(v.to_ascii_lowercase().as_str(), "1" | "true" | "yes"))
1298 .unwrap_or(false)
1299 })
1300}
1301
1302#[derive(Default)]
1303struct FrameTraceState {
1304 enabled: bool,
1305 redraw_reasons: Vec<String>,
1306}
1307
1308impl FrameTraceState {
1309 fn new(enabled: bool) -> Self {
1310 Self {
1311 enabled,
1312 redraw_reasons: Vec::new(),
1313 }
1314 }
1315
1316 fn note_redraw_reason(&mut self, reason: impl Into<String>) {
1317 if !self.enabled {
1318 return;
1319 }
1320 let reason = reason.into();
1321 if !self
1322 .redraw_reasons
1323 .iter()
1324 .any(|existing| existing == &reason)
1325 {
1326 self.redraw_reasons.push(reason);
1327 }
1328 }
1329
1330 fn take_redraw_reasons(&mut self) -> Vec<String> {
1331 if !self.enabled {
1332 return Vec::new();
1333 }
1334 std::mem::take(&mut self.redraw_reasons)
1335 }
1336
1337 fn emit(
1338 &self,
1339 phase: &str,
1340 frame: u64,
1341 active_animation_keys: &[String],
1342 invalidations: InvalidationSet,
1343 reasons: &[String],
1344 detail: &str,
1345 ) {
1346 if !self.enabled {
1347 return;
1348 }
1349 let active = if active_animation_keys.is_empty() {
1350 "none".to_string()
1351 } else {
1352 active_animation_keys.join(",")
1353 };
1354 let reasons = if reasons.is_empty() {
1355 "none".to_string()
1356 } else {
1357 reasons.join(",")
1358 };
1359 eprintln!(
1360 "[frame-trace] phase={} frame={} invalidation={} active=[{}] reasons=[{}] {}",
1361 phase,
1362 frame,
1363 invalidations.labels().join("+"),
1364 active,
1365 reasons,
1366 detail,
1367 );
1368 }
1369}
1370
1371fn request_redraw_logged(
1372 window: &Window,
1373 elwt: &EventLoopWindowTarget,
1374 last_redraw_at: &mut Instant,
1375 min_frame: Duration,
1376 redraw_pending: &mut bool,
1377 frame_trace: &mut FrameTraceState,
1378 reason: &str,
1379) {
1380 frame_trace.note_redraw_reason(reason);
1381 request_redraw_throttled(window, elwt, last_redraw_at, min_frame, redraw_pending);
1382}
1383
1384fn apply_authoritative_resize(
1385 window: &Window,
1386 elwt: &EventLoopWindowTarget,
1387 next_viewport: WindowViewportState,
1388 pending_resize: &mut Option<WindowViewportState>,
1389 resize_needs_settled_frame: &mut bool,
1390 pending_capture_settle: &mut bool,
1391 pending_screenshot_path: Option<&str>,
1392 live_resize: &mut LiveResizeController,
1393 invalidations: &mut InvalidationSet,
1394 last_redraw_at: &mut Instant,
1395 resize_frame: Duration,
1396 redraw_pending: &mut bool,
1397 frame_trace: &mut FrameTraceState,
1398 reason: &str,
1399) {
1400 *pending_resize = Some(next_viewport);
1401 *resize_needs_settled_frame = true;
1402 if pending_screenshot_path.is_some() {
1403 *pending_capture_settle = true;
1404 }
1405 live_resize.note_resize(Instant::now());
1406 invalidations.mark_composite();
1407 request_redraw_logged(
1408 window,
1409 elwt,
1410 last_redraw_at,
1411 resize_frame,
1412 redraw_pending,
1413 frame_trace,
1414 reason,
1415 );
1416}
1417
1418fn active_animation_keys(runtime: &Runtime) -> Vec<String> {
1419 let mut keys = runtime
1420 .runtime_state
1421 .motion
1422 .active
1423 .iter()
1424 .map(|((target, property), anim)| {
1425 let repeat = if anim.repeat { "repeat" } else { "finite" };
1426 format!("{}:{:?}:{}", target.as_u128(), property, repeat)
1427 })
1428 .collect::<Vec<_>>();
1429 keys.sort();
1430 keys
1431}
1432
1433fn repeating_animation_redraw_interval(
1434 animation_map: &fission_core::MotionStateMap,
1435 default_repeat_frame: Duration,
1436) -> Option<Duration> {
1437 animation_map
1438 .active
1439 .values()
1440 .filter(|anim| anim.repeat)
1441 .map(|anim| {
1442 anim.frame_interval_ms
1443 .filter(|ms| *ms > 0)
1444 .map(Duration::from_millis)
1445 .unwrap_or(default_repeat_frame)
1446 })
1447 .min()
1448}
1449
1450fn animation_redraw_interval(
1451 has_finite_animation: bool,
1452 repeat_animation_frame: Option<Duration>,
1453 has_playing_video: bool,
1454 min_frame: Duration,
1455) -> Option<Duration> {
1456 if has_finite_animation || has_playing_video {
1457 Some(min_frame)
1458 } else if let Some(repeat_frame) = repeat_animation_frame {
1459 Some(repeat_frame)
1460 } else {
1461 None
1462 }
1463}
1464
1465fn pending_work_redraw_interval(
1466 invalidations: InvalidationSet,
1467 pending_resize: bool,
1468 min_frame: Duration,
1469 resize_frame: Duration,
1470) -> Duration {
1471 if pending_resize && !invalidations.build && !invalidations.paint && !invalidations.composite {
1472 resize_frame
1473 } else {
1474 min_frame
1475 }
1476}
1477
1478fn resize_is_unsettled(pending_resize: bool, needs_settled_frame: bool, live_resize: bool) -> bool {
1479 pending_resize || needs_settled_frame || live_resize
1480}
1481
1482fn resolve_build_viewport(
1483 last_built_viewport: Option<LayoutSize>,
1484 target_viewport: LayoutSize,
1485 has_prev_ir: bool,
1486 invalidations: &mut InvalidationSet,
1487) -> LayoutSize {
1488 let built_viewport = last_built_viewport.unwrap_or(target_viewport);
1489 if built_viewport != target_viewport {
1490 invalidations.mark_build();
1492 }
1493
1494 if invalidations.build || !has_prev_ir || last_built_viewport.is_none() {
1495 target_viewport
1496 } else {
1497 built_viewport
1498 }
1499}
1500
1501#[derive(Debug)]
1502struct LiveResizeController {
1503 active_until: Option<Instant>,
1504 settle_delay: Duration,
1505}
1506
1507impl LiveResizeController {
1508 fn new(settle_delay: Duration) -> Self {
1509 Self {
1510 active_until: None,
1511 settle_delay,
1512 }
1513 }
1514
1515 fn note_resize(&mut self, now: Instant) {
1516 self.active_until = Some(now + self.settle_delay);
1517 }
1518
1519 fn is_live(&self, now: Instant) -> bool {
1520 self.active_until
1521 .map(|deadline| now < deadline)
1522 .unwrap_or(false)
1523 }
1524}
1525
1526fn process_pending_effects(
1531 runtime: &mut Runtime,
1532 effect_tx: &mpsc::Sender<AsyncMessage>,
1533 event_proxy: &EventLoopProxy<TestEvent>,
1534 async_registry: &AsyncRegistry,
1535 active_services: &mut HashMap<ServiceKey, ActiveServiceHandle>,
1536 service_bindings: &mut HashMap<ServiceBindingKey, ServiceBindings>,
1537 next_service_instance_id: &mut u64,
1538) -> bool {
1539 let pending = std::mem::take(&mut runtime.pending_effects);
1540 if pending.is_empty() {
1541 return false;
1542 }
1543
1544 let mut dispatched_callback = false;
1545 let wake = {
1546 let proxy = Arc::new(Mutex::new(event_proxy.clone()));
1547 Arc::new(move || {
1548 if let Ok(proxy) = proxy.lock() {
1549 let _ = proxy.send_event(TestEvent::Wake);
1550 }
1551 })
1552 };
1553
1554 for env in pending {
1555 match env.effect {
1556 Effect::Runtime(ref runtime_effect) => {
1557 diag::emit(
1558 diag::DiagCategory::Input,
1559 diag::DiagLevel::Debug,
1560 diag::DiagEventKind::InputEvent {
1561 kind: format!("runtime_effect:{:?}", runtime_effect),
1562 target: None,
1563 position: None,
1564 },
1565 );
1566 if runtime.queue_runtime_effect(runtime_effect.clone()) {
1567 dispatched_callback = true;
1568 (wake)();
1569 }
1570 }
1571 Effect::Capability(capability) => match capability {
1572 CapabilityInvocationPayload::Operation(op) => {
1573 if !async_registry.spawn_capability(
1574 &op.capability_name,
1575 env.req_id,
1576 op.request,
1577 env.on_ok.clone(),
1578 env.on_err.clone(),
1579 env.resource.clone(),
1580 effect_tx,
1581 wake.clone(),
1582 ) {
1583 let _ = effect_tx.send(AsyncMessage::CapabilityErr {
1584 capability_name: op.capability_name,
1585 req_id: env.req_id,
1586 payload: None,
1587 on_err: env.on_err.clone(),
1588 message: Some(
1589 "no async operation capability handler registered".into(),
1590 ),
1591 resource: env.resource.clone(),
1592 });
1593 (wake)();
1594 }
1595 }
1596 },
1597 Effect::Job(job) => {
1598 if !async_registry.spawn_job(
1599 &job.job_name,
1600 env.req_id,
1601 job.payload,
1602 env.on_ok.clone(),
1603 env.on_err.clone(),
1604 env.resource.clone(),
1605 effect_tx,
1606 wake.clone(),
1607 ) {
1608 let _ = effect_tx.send(AsyncMessage::JobErr {
1609 job_name: job.job_name,
1610 req_id: env.req_id,
1611 payload: None,
1612 on_err: env.on_err.clone(),
1613 message: Some("no async job handler registered".into()),
1614 resource: env.resource.clone(),
1615 });
1616 (wake)();
1617 }
1618 }
1619 Effect::StartService(start) => {
1620 let key = (start.service_name.clone(), start.slot_key.clone());
1621 if let Some(previous) = active_services.remove(&key) {
1622 let _ = previous
1623 .runtime
1624 .control_tx
1625 .send(ServiceControlMessage::Stop);
1626 }
1627
1628 let instance_id = *next_service_instance_id;
1629 *next_service_instance_id = next_service_instance_id.saturating_add(1);
1630 let bindings = env.service_bindings.clone().unwrap_or_default();
1631 service_bindings.insert(
1632 (
1633 start.service_name.clone(),
1634 start.slot_key.clone(),
1635 instance_id,
1636 ),
1637 bindings,
1638 );
1639
1640 match async_registry.spawn_service(
1641 &start.service_name,
1642 &start.slot_key,
1643 instance_id,
1644 start.config,
1645 env.resource.clone(),
1646 effect_tx,
1647 wake.clone(),
1648 ) {
1649 Some(handle) => {
1650 active_services.insert(key, ActiveServiceHandle { runtime: handle });
1651 }
1652 None => {
1653 let _ = service_bindings.remove(&(
1654 start.service_name.clone(),
1655 start.slot_key.clone(),
1656 instance_id,
1657 ));
1658 let _ = effect_tx.send(AsyncMessage::ServiceStartFailed {
1659 service_name: start.service_name,
1660 slot_key: start.slot_key,
1661 instance_id,
1662 payload: None,
1663 message: Some("no async service handler registered".into()),
1664 resource: env.resource.clone(),
1665 });
1666 (wake)();
1667 }
1668 }
1669 }
1670 Effect::ServiceCommand(command) => {
1671 let key = (command.service_name.clone(), command.slot_key.clone());
1672 if let Some(handle) = active_services.get(&key) {
1673 let _ = handle
1674 .runtime
1675 .control_tx
1676 .send(ServiceControlMessage::Command {
1677 req_id: env.req_id,
1678 payload: command.payload,
1679 on_ok: env.on_ok.clone(),
1680 on_err: env.on_err.clone(),
1681 });
1682 } else {
1683 let _ = effect_tx.send(AsyncMessage::ServiceCommandErr {
1684 service_name: command.service_name,
1685 slot_key: command.slot_key,
1686 instance_id: 0,
1687 req_id: env.req_id,
1688 payload: None,
1689 on_err: env.on_err.clone(),
1690 message: Some("service is not running".into()),
1691 resource: env.resource.clone(),
1692 });
1693 (wake)();
1694 }
1695 }
1696 Effect::StopService(stop) => {
1697 let key = (stop.service_name.clone(), stop.slot_key.clone());
1698 if let Some(handle) = active_services.remove(&key) {
1699 let _ = handle.runtime.control_tx.send(ServiceControlMessage::Stop);
1700 }
1701 }
1702 }
1703 }
1704
1705 dispatched_callback
1706}
1707
1708fn drain_effect_results(
1713 runtime: &mut Runtime,
1714 effect_rx: &mpsc::Receiver<AsyncMessage>,
1715 active_services: &mut HashMap<ServiceKey, ActiveServiceHandle>,
1716 service_bindings: &mut HashMap<ServiceBindingKey, ServiceBindings>,
1717) -> bool {
1718 let mut dispatched = false;
1719
1720 while let Ok(message) = effect_rx.try_recv() {
1721 match message {
1722 AsyncMessage::JobOk {
1723 job_name,
1724 req_id,
1725 payload,
1726 on_ok,
1727 resource,
1728 } => {
1729 if let Some(resource) = resource.as_ref() {
1730 if !runtime.is_resource_current(resource) {
1731 continue;
1732 }
1733 }
1734 if let Some(action) = on_ok {
1735 let _ = runtime.dispatch_with_input(
1736 action,
1737 WidgetId::from_u128(0),
1738 &ActionInput::JobOk {
1739 job_name,
1740 req_id,
1741 payload,
1742 },
1743 );
1744 dispatched = true;
1745 }
1746 }
1747 AsyncMessage::JobErr {
1748 job_name,
1749 req_id,
1750 payload,
1751 on_err,
1752 message,
1753 resource,
1754 } => {
1755 if let Some(resource) = resource.as_ref() {
1756 if !runtime.is_resource_current(resource) {
1757 continue;
1758 }
1759 }
1760 if let Some(action) = on_err {
1761 let _ = runtime.dispatch_with_input(
1762 action,
1763 WidgetId::from_u128(0),
1764 &ActionInput::JobErr {
1765 job_name,
1766 req_id,
1767 payload,
1768 message,
1769 },
1770 );
1771 dispatched = true;
1772 }
1773 }
1774 AsyncMessage::ServiceStarted {
1775 service_name,
1776 slot_key,
1777 instance_id,
1778 resource,
1779 } => {
1780 if let Some(resource) = resource.as_ref() {
1781 if !runtime.is_resource_current(resource) {
1782 continue;
1783 }
1784 }
1785 let key = (service_name.clone(), slot_key.clone());
1786 let Some(current) = active_services.get(&key) else {
1787 continue;
1788 };
1789 if current.runtime.instance_id != instance_id {
1790 continue;
1791 }
1792 if let Some(bindings) =
1793 service_bindings.get(&(service_name.clone(), slot_key.clone(), instance_id))
1794 {
1795 if let Some(action) = bindings.on_started.clone() {
1796 let _ = runtime.dispatch_with_input(
1797 action,
1798 WidgetId::from_u128(0),
1799 &ActionInput::ServiceStarted {
1800 service_name,
1801 slot_key,
1802 instance_id,
1803 },
1804 );
1805 dispatched = true;
1806 }
1807 }
1808 }
1809 AsyncMessage::ServiceStartFailed {
1810 service_name,
1811 slot_key,
1812 instance_id,
1813 payload,
1814 message,
1815 resource,
1816 } => {
1817 if let Some(resource) = resource.as_ref() {
1818 if !runtime.is_resource_current(resource) {
1819 service_bindings.remove(&(service_name, slot_key, instance_id));
1820 continue;
1821 }
1822 }
1823 let key = (service_name.clone(), slot_key.clone());
1824 let should_dispatch = active_services
1825 .get(&key)
1826 .map(|current| current.runtime.instance_id == instance_id)
1827 .unwrap_or(true);
1828 active_services.remove(&key);
1829 let bindings =
1830 service_bindings.remove(&(service_name.clone(), slot_key.clone(), instance_id));
1831 if should_dispatch {
1832 if let Some(action) = bindings.and_then(|bindings| bindings.on_start_failed) {
1833 let _ = runtime.dispatch_with_input(
1834 action,
1835 WidgetId::from_u128(0),
1836 &ActionInput::ServiceStartFailed {
1837 service_name,
1838 slot_key,
1839 payload,
1840 message,
1841 },
1842 );
1843 dispatched = true;
1844 }
1845 }
1846 }
1847 AsyncMessage::ServiceEvent {
1848 service_name,
1849 slot_key,
1850 instance_id,
1851 payload,
1852 resource,
1853 } => {
1854 if let Some(resource) = resource.as_ref() {
1855 if !runtime.is_resource_current(resource) {
1856 continue;
1857 }
1858 }
1859 let key = (service_name.clone(), slot_key.clone());
1860 let Some(current) = active_services.get(&key) else {
1861 continue;
1862 };
1863 if current.runtime.instance_id != instance_id {
1864 continue;
1865 }
1866 if let Some(bindings) =
1867 service_bindings.get(&(service_name.clone(), slot_key.clone(), instance_id))
1868 {
1869 if let Some(action) = bindings.on_event.clone() {
1870 let _ = runtime.dispatch_with_input(
1871 action,
1872 WidgetId::from_u128(0),
1873 &ActionInput::ServiceEvent {
1874 service_name,
1875 slot_key,
1876 instance_id,
1877 payload,
1878 },
1879 );
1880 dispatched = true;
1881 }
1882 }
1883 }
1884 AsyncMessage::ServiceStopped {
1885 service_name,
1886 slot_key,
1887 instance_id,
1888 resource,
1889 } => {
1890 if let Some(resource) = resource.as_ref() {
1891 if !runtime.is_resource_current(resource) {
1892 service_bindings.remove(&(service_name, slot_key, instance_id));
1893 continue;
1894 }
1895 }
1896 let key = (service_name.clone(), slot_key.clone());
1897 let should_dispatch = active_services
1898 .get(&key)
1899 .map(|current| current.runtime.instance_id == instance_id)
1900 .unwrap_or(true);
1901 if should_dispatch {
1902 active_services.remove(&key);
1903 }
1904 let bindings =
1905 service_bindings.remove(&(service_name.clone(), slot_key.clone(), instance_id));
1906 if should_dispatch {
1907 if let Some(action) = bindings.and_then(|bindings| bindings.on_stopped) {
1908 let _ = runtime.dispatch_with_input(
1909 action,
1910 WidgetId::from_u128(0),
1911 &ActionInput::ServiceStopped {
1912 service_name,
1913 slot_key,
1914 instance_id,
1915 },
1916 );
1917 dispatched = true;
1918 }
1919 }
1920 }
1921 AsyncMessage::ServiceCommandOk {
1922 service_name,
1923 slot_key,
1924 instance_id,
1925 req_id,
1926 payload,
1927 on_ok,
1928 resource,
1929 } => {
1930 if let Some(resource) = resource.as_ref() {
1931 if !runtime.is_resource_current(resource) {
1932 continue;
1933 }
1934 }
1935 let key = (service_name.clone(), slot_key.clone());
1936 let Some(current) = active_services.get(&key) else {
1937 continue;
1938 };
1939 if current.runtime.instance_id != instance_id {
1940 continue;
1941 }
1942 if let Some(action) = on_ok {
1943 let _ = runtime.dispatch_with_input(
1944 action,
1945 WidgetId::from_u128(0),
1946 &ActionInput::ServiceCommandOk {
1947 service_name,
1948 slot_key,
1949 instance_id,
1950 req_id,
1951 payload,
1952 },
1953 );
1954 dispatched = true;
1955 }
1956 }
1957 AsyncMessage::ServiceCommandErr {
1958 service_name,
1959 slot_key,
1960 instance_id,
1961 req_id,
1962 payload,
1963 on_err,
1964 message,
1965 resource,
1966 } => {
1967 if let Some(resource) = resource.as_ref() {
1968 if !runtime.is_resource_current(resource) {
1969 continue;
1970 }
1971 }
1972 let key = (service_name.clone(), slot_key.clone());
1973 if instance_id != 0 {
1974 let Some(current) = active_services.get(&key) else {
1975 continue;
1976 };
1977 if current.runtime.instance_id != instance_id {
1978 continue;
1979 }
1980 }
1981 if let Some(action) = on_err {
1982 let _ = runtime.dispatch_with_input(
1983 action,
1984 WidgetId::from_u128(0),
1985 &ActionInput::ServiceCommandErr {
1986 service_name,
1987 slot_key,
1988 instance_id,
1989 req_id,
1990 payload,
1991 message,
1992 },
1993 );
1994 dispatched = true;
1995 }
1996 }
1997 AsyncMessage::CapabilityOk {
1998 capability_name,
1999 req_id,
2000 payload,
2001 on_ok,
2002 resource,
2003 } => {
2004 if let Some(resource) = resource.as_ref() {
2005 if !runtime.is_resource_current(resource) {
2006 continue;
2007 }
2008 }
2009 if let Some(action) = on_ok {
2010 let _ = runtime.dispatch_with_input(
2011 action,
2012 WidgetId::from_u128(0),
2013 &ActionInput::CapabilityOk {
2014 capability: capability_name,
2015 req_id,
2016 payload,
2017 },
2018 );
2019 dispatched = true;
2020 }
2021 }
2022 AsyncMessage::CapabilityErr {
2023 capability_name,
2024 req_id,
2025 payload,
2026 on_err,
2027 message,
2028 resource,
2029 } => {
2030 if let Some(resource) = resource.as_ref() {
2031 if !runtime.is_resource_current(resource) {
2032 continue;
2033 }
2034 }
2035 if let Some(action) = on_err {
2036 let _ = runtime.dispatch_with_input(
2037 action,
2038 WidgetId::from_u128(0),
2039 &ActionInput::CapabilityErr {
2040 capability: capability_name,
2041 req_id,
2042 payload,
2043 message,
2044 },
2045 );
2046 dispatched = true;
2047 }
2048 }
2049 }
2050 }
2051
2052 dispatched
2053}
2054
2055fn focused_text_input_id(runtime: &Runtime, ir: Option<&CoreIR>) -> Option<WidgetId> {
2056 let focused = runtime.runtime_state.interaction.focused?;
2057 let ir = ir?;
2058 let mut current = Some(focused);
2059 while let Some(id) = current {
2060 let node = ir.nodes.get(&id)?;
2061 if let Op::Semantics(sem) = &node.op {
2062 if sem.role == fission_ir::Role::TextInput {
2063 return Some(id);
2064 }
2065 }
2066 current = node.parent;
2067 }
2068 None
2069}
2070
2071fn focused_text_input_config(runtime: &Runtime, ir: Option<&CoreIR>) -> Option<TextInputConfig> {
2072 let id = focused_text_input_id(runtime, ir)?;
2073 let ir = ir?;
2074 let node = ir.nodes.get(&id)?;
2075 match &node.op {
2076 Op::Semantics(semantics) => Some(TextInputConfig::from_semantics(semantics)),
2077 _ => None,
2078 }
2079}
2080
2081fn focused_custom_text_input(runtime: &Runtime, ir: Option<&CoreIR>) -> bool {
2082 let focused = match runtime.runtime_state.interaction.focused {
2083 Some(id) => id,
2084 None => return false,
2085 };
2086 let ir = match ir {
2087 Some(ir) => ir,
2088 None => return false,
2089 };
2090 let mut current = Some(focused);
2091 while let Some(id) = current {
2092 if let Some(any_ro) = ir.custom_render_objects.get(&id) {
2093 if let Some(render_obj) = downcast_render_object(any_ro) {
2094 if render_obj.accepts_text_input() {
2095 return true;
2096 }
2097 }
2098 }
2099 current = ir.nodes.get(&id).and_then(|node| node.parent);
2100 }
2101 false
2102}
2103
2104fn reset_text_input_caret(
2105 runtime: &mut Runtime,
2106 ir: Option<&CoreIR>,
2107 last_blink_toggle: &mut Instant,
2108) {
2109 if let Some(id) = focused_text_input_id(runtime, ir) {
2110 runtime.runtime_state.caret_visible.insert(id, true);
2111 *last_blink_toggle = Instant::now();
2112 }
2113}
2114
2115#[derive(Debug, Clone)]
2116struct PendingTextTrace {
2117 seq: u64,
2118 source: String,
2119 target: Option<WidgetId>,
2120 started_at: Instant,
2121 handled_at: Option<Instant>,
2122 effects_at: Option<Instant>,
2123 present_after_frame: u64,
2124}
2125
2126fn start_text_trace(
2127 enabled: bool,
2128 traces: &mut VecDeque<PendingTextTrace>,
2129 next_seq: &mut u64,
2130 source: String,
2131 target: Option<WidgetId>,
2132 presented_frames: u64,
2133) -> Option<u64> {
2134 if !enabled {
2135 return None;
2136 }
2137 *next_seq += 1;
2138 let seq = *next_seq;
2139 traces.push_back(PendingTextTrace {
2140 seq,
2141 source,
2142 target,
2143 started_at: Instant::now(),
2144 handled_at: None,
2145 effects_at: None,
2146 present_after_frame: presented_frames + 1,
2147 });
2148 Some(seq)
2149}
2150
2151fn mark_text_trace_handled(traces: &mut VecDeque<PendingTextTrace>, seq: Option<u64>) {
2152 if let Some(seq) = seq {
2153 if let Some(trace) = traces.iter_mut().rev().find(|trace| trace.seq == seq) {
2154 trace.handled_at = Some(Instant::now());
2155 }
2156 }
2157}
2158
2159fn mark_text_trace_effects(traces: &mut VecDeque<PendingTextTrace>, seq: Option<u64>) {
2160 if let Some(seq) = seq {
2161 if let Some(trace) = traces.iter_mut().rev().find(|trace| trace.seq == seq) {
2162 trace.effects_at = Some(Instant::now());
2163 }
2164 }
2165}
2166
2167fn set_text_trace_target(
2168 traces: &mut VecDeque<PendingTextTrace>,
2169 seq: Option<u64>,
2170 target: Option<WidgetId>,
2171) {
2172 if let Some(seq) = seq {
2173 if let Some(trace) = traces.iter_mut().rev().find(|trace| trace.seq == seq) {
2174 trace.target = target;
2175 }
2176 }
2177}
2178
2179fn cancel_text_trace(traces: &mut VecDeque<PendingTextTrace>, seq: Option<u64>) {
2180 if let Some(seq) = seq {
2181 traces.retain(|trace| trace.seq != seq);
2182 }
2183}
2184
2185fn flush_text_traces(
2186 enabled: bool,
2187 traces: &mut VecDeque<PendingTextTrace>,
2188 presented_frames: u64,
2189) {
2190 if !enabled {
2191 traces.clear();
2192 return;
2193 }
2194
2195 loop {
2196 let should_flush = traces
2197 .front()
2198 .map(|trace| trace.present_after_frame <= presented_frames)
2199 .unwrap_or(false);
2200 if !should_flush {
2201 break;
2202 }
2203
2204 let Some(trace) = traces.pop_front() else {
2205 break;
2206 };
2207 let now = Instant::now();
2208 let handled_at = trace.handled_at.unwrap_or(now);
2209 let effects_at = trace.effects_at.unwrap_or(handled_at);
2210 let total_ms = now.duration_since(trace.started_at).as_secs_f64() * 1000.0;
2211 let handle_ms = handled_at.duration_since(trace.started_at).as_secs_f64() * 1000.0;
2212 let effects_ms = effects_at.duration_since(handled_at).as_secs_f64() * 1000.0;
2213 let queue_ms = now.duration_since(effects_at).as_secs_f64() * 1000.0;
2214
2215 let target_u128 = trace.target.map(|id| id.as_u128());
2216 let msg = format!(
2217 "text_input_latency seq={} src={} handle_ms={:.2} effects_ms={:.2} queue_ms={:.2} total_ms={:.2} frame={}",
2218 trace.seq, trace.source, handle_ms, effects_ms, queue_ms, total_ms, presented_frames
2219 );
2220 eprintln!("[text-trace] {}", msg);
2221 diag::emit(
2222 diag::DiagCategory::Input,
2223 diag::DiagLevel::Info,
2224 diag::DiagEventKind::InputEvent {
2225 kind: msg,
2226 target: target_u128,
2227 position: None,
2228 },
2229 );
2230 }
2231}
2232
2233fn map_test_button(button: u8) -> PointerButton {
2239 match button {
2240 0 => PointerButton::Primary,
2241 1 => PointerButton::Secondary,
2242 2 => PointerButton::Middle,
2243 n => PointerButton::Other(n),
2244 }
2245}
2246
2247fn cursor_icon_for(cursor: MouseCursor) -> CursorIcon {
2248 match cursor {
2249 MouseCursor::Default => CursorIcon::Default,
2250 MouseCursor::Pointer => CursorIcon::Pointer,
2251 MouseCursor::Text => CursorIcon::Text,
2252 MouseCursor::Crosshair => CursorIcon::Crosshair,
2253 MouseCursor::Move => CursorIcon::Move,
2254 MouseCursor::NotAllowed => CursorIcon::NotAllowed,
2255 MouseCursor::Grab => CursorIcon::Grab,
2256 MouseCursor::Grabbing => CursorIcon::Grabbing,
2257 MouseCursor::Wait => CursorIcon::Wait,
2258 MouseCursor::Help => CursorIcon::Help,
2259 MouseCursor::VerticalText => CursorIcon::VerticalText,
2260 }
2261}
2262
2263fn sync_window_cursor(window: &Window, runtime: &Runtime) {
2264 window.set_cursor(cursor_icon_for(runtime.runtime_state.interaction.cursor()));
2265}
2266
2267const LINE_SCROLL_POINTS: f32 = 50.0;
2268
2269fn normalize_winit_scroll_delta(delta: &MouseScrollDelta, scale_factor: f64) -> (f32, f32) {
2270 let scale_factor = if scale_factor.is_finite() && scale_factor > 0.0 {
2271 scale_factor
2272 } else {
2273 1.0
2274 };
2275 match delta {
2276 MouseScrollDelta::LineDelta(x, y) => (-x * LINE_SCROLL_POINTS, -y * LINE_SCROLL_POINTS),
2280 MouseScrollDelta::PixelDelta(p) => {
2281 (-(p.x / scale_factor) as f32, -(p.y / scale_factor) as f32)
2282 }
2283 }
2284}
2285
2286fn physical_position_to_layout_point(
2287 position: PhysicalPosition<f64>,
2288 scale_factor: f64,
2289 content_origin: PhysicalPosition<i32>,
2290) -> LayoutPoint {
2291 let scale_factor = normalize_scale_factor(scale_factor);
2292 LayoutPoint::new(
2293 ((position.x - content_origin.x as f64) / scale_factor) as f32,
2294 ((position.y - content_origin.y as f64) / scale_factor) as f32,
2295 )
2296}
2297
2298fn window_content_origin_physical(window: &Window) -> PhysicalPosition<i32> {
2299 #[cfg(target_os = "ios")]
2300 {
2301 let _ = window;
2304 PhysicalPosition::new(0, 0)
2305 }
2306 #[cfg(not(target_os = "ios"))]
2307 {
2308 let _ = window;
2309 PhysicalPosition::new(0, 0)
2310 }
2311}
2312
2313fn window_physical_position_to_layout_point(
2314 window: &Window,
2315 position: PhysicalPosition<f64>,
2316) -> LayoutPoint {
2317 physical_position_to_layout_point(
2318 position,
2319 window.scale_factor(),
2320 window_content_origin_physical(window),
2321 )
2322}
2323
2324fn handle_cursor_moved(
2326 x: f32,
2327 y: f32,
2328 modifiers: u8,
2329 runtime: &mut Runtime,
2330 pipeline: &Pipeline,
2331 effect_result_tx: &mpsc::Sender<EffectResult>,
2332 event_proxy: &EventLoopProxy<TestEvent>,
2333 async_registry: &AsyncRegistry,
2334 active_services: &mut HashMap<ServiceKey, ActiveServiceHandle>,
2335 service_bindings: &mut HashMap<ServiceBindingKey, ServiceBindings>,
2336 next_service_instance_id: &mut u64,
2337 window: &Window,
2338 elwt: &EventLoopWindowTarget,
2339 last_redraw_at: &mut Instant,
2340 min_frame: Duration,
2341 redraw_pending: &mut bool,
2342 frame_trace: &mut FrameTraceState,
2343 invalidations: &mut InvalidationSet,
2344) {
2345 if let (Some(ir), Some(layout)) = (&pipeline.prev_ir, &pipeline.last_snapshot) {
2346 let point = LayoutPoint { x, y };
2347 let event = InputEvent::Pointer(PointerEvent::Move { point, modifiers });
2348 if let Err(e) = runtime.handle_input(event, ir, layout) {
2349 eprintln!("Input handling error: {:?}", e);
2350 }
2351 sync_window_cursor(window, runtime);
2352 invalidations.mark_build();
2353 if process_pending_effects(
2354 runtime,
2355 effect_result_tx,
2356 event_proxy,
2357 async_registry,
2358 active_services,
2359 service_bindings,
2360 next_service_instance_id,
2361 ) {
2362 invalidations.mark_build();
2363 request_redraw_logged(
2364 window,
2365 elwt,
2366 last_redraw_at,
2367 min_frame,
2368 redraw_pending,
2369 frame_trace,
2370 "pointer_move:effects",
2371 );
2372 }
2373 request_redraw_logged(
2374 window,
2375 elwt,
2376 last_redraw_at,
2377 min_frame,
2378 redraw_pending,
2379 frame_trace,
2380 "pointer_move",
2381 );
2382 }
2383}
2384
2385fn handle_mouse_button(
2388 x: f32,
2389 y: f32,
2390 button: PointerButton,
2391 is_pressed: bool,
2392 modifiers: u8,
2393 runtime: &mut Runtime,
2394 pipeline: &Pipeline,
2395 effect_result_tx: &mpsc::Sender<EffectResult>,
2396 event_proxy: &EventLoopProxy<TestEvent>,
2397 async_registry: &AsyncRegistry,
2398 active_services: &mut HashMap<ServiceKey, ActiveServiceHandle>,
2399 service_bindings: &mut HashMap<ServiceBindingKey, ServiceBindings>,
2400 next_service_instance_id: &mut u64,
2401 window: &Window,
2402 elwt: &EventLoopWindowTarget,
2403 last_redraw_at: &mut Instant,
2404 min_frame: Duration,
2405 redraw_pending: &mut bool,
2406 text_trace_enabled: bool,
2407 pending_text_traces: &mut VecDeque<PendingTextTrace>,
2408 next_text_trace_seq: &mut u64,
2409 presented_frames: u64,
2410 last_blink_toggle: &mut Instant,
2411 frame_trace: &mut FrameTraceState,
2412 invalidations: &mut InvalidationSet,
2413) {
2414 if let (Some(ir), Some(layout)) = (&pipeline.prev_ir, &pipeline.last_snapshot) {
2415 let point = LayoutPoint { x, y };
2416 let pointer_event = if is_pressed {
2417 PointerEvent::Down {
2418 point,
2419 button,
2420 modifiers,
2421 }
2422 } else {
2423 PointerEvent::Up {
2424 point,
2425 button,
2426 modifiers,
2427 }
2428 };
2429 let input_event = InputEvent::Pointer(pointer_event);
2430
2431 let trace_seq = if text_trace_enabled && is_pressed {
2432 start_text_trace(
2433 text_trace_enabled,
2434 pending_text_traces,
2435 next_text_trace_seq,
2436 "pointer_down".to_string(),
2437 None,
2438 presented_frames,
2439 )
2440 } else {
2441 None
2442 };
2443
2444 if let Err(e) = runtime.handle_input(input_event, ir, layout) {
2445 eprintln!("Input handling error: {:?}", e);
2446 }
2447 sync_window_cursor(window, runtime);
2448 invalidations.mark_build();
2449
2450 mark_text_trace_handled(pending_text_traces, trace_seq);
2451 if process_pending_effects(
2452 runtime,
2453 effect_result_tx,
2454 event_proxy,
2455 async_registry,
2456 active_services,
2457 service_bindings,
2458 next_service_instance_id,
2459 ) {
2460 mark_text_trace_effects(pending_text_traces, trace_seq);
2461 invalidations.mark_build();
2462 request_redraw_logged(
2463 window,
2464 elwt,
2465 last_redraw_at,
2466 min_frame,
2467 redraw_pending,
2468 frame_trace,
2469 if is_pressed {
2470 "pointer_down:effects"
2471 } else {
2472 "pointer_up:effects"
2473 },
2474 );
2475 }
2476 if is_pressed {
2477 let target = focused_text_input_id(runtime, pipeline.prev_ir.as_ref());
2478 if target.is_some() {
2479 set_text_trace_target(pending_text_traces, trace_seq, target);
2480 } else {
2481 cancel_text_trace(pending_text_traces, trace_seq);
2482 }
2483 reset_text_input_caret(runtime, pipeline.prev_ir.as_ref(), last_blink_toggle);
2484 }
2485 request_redraw_logged(
2486 window,
2487 elwt,
2488 last_redraw_at,
2489 min_frame,
2490 redraw_pending,
2491 frame_trace,
2492 if is_pressed {
2493 "pointer_down"
2494 } else {
2495 "pointer_up"
2496 },
2497 );
2498 }
2499}
2500
2501fn handle_scroll(
2503 point_x: f32,
2504 point_y: f32,
2505 delta_x: f32,
2506 delta_y: f32,
2507 modifiers: u8,
2508 runtime: &mut Runtime,
2509 pipeline: &Pipeline,
2510 effect_result_tx: &mpsc::Sender<EffectResult>,
2511 event_proxy: &EventLoopProxy<TestEvent>,
2512 async_registry: &AsyncRegistry,
2513 active_services: &mut HashMap<ServiceKey, ActiveServiceHandle>,
2514 service_bindings: &mut HashMap<ServiceBindingKey, ServiceBindings>,
2515 next_service_instance_id: &mut u64,
2516 window: &Window,
2517 elwt: &EventLoopWindowTarget,
2518 last_redraw_at: &mut Instant,
2519 min_frame: Duration,
2520 redraw_pending: &mut bool,
2521 frame_trace: &mut FrameTraceState,
2522 invalidations: &mut InvalidationSet,
2523) {
2524 if let (Some(ir), Some(layout)) = (&pipeline.prev_ir, &pipeline.last_snapshot) {
2525 let point = LayoutPoint {
2526 x: point_x,
2527 y: point_y,
2528 };
2529 let scroll_delta = LayoutPoint {
2530 x: delta_x,
2531 y: delta_y,
2532 };
2533 let event = InputEvent::Pointer(PointerEvent::Scroll {
2534 point,
2535 delta: scroll_delta,
2536 modifiers,
2537 });
2538 if let Err(e) = runtime.handle_input(event, ir, layout) {
2539 eprintln!("Scroll error: {:?}", e);
2540 }
2541 sync_window_cursor(window, runtime);
2542 invalidations.mark_build();
2546 if process_pending_effects(
2547 runtime,
2548 effect_result_tx,
2549 event_proxy,
2550 async_registry,
2551 active_services,
2552 service_bindings,
2553 next_service_instance_id,
2554 ) {
2555 invalidations.mark_build();
2556 request_redraw_logged(
2557 window,
2558 elwt,
2559 last_redraw_at,
2560 min_frame,
2561 redraw_pending,
2562 frame_trace,
2563 "scroll:effects",
2564 );
2565 }
2566 request_redraw_logged(
2567 window,
2568 elwt,
2569 last_redraw_at,
2570 min_frame,
2571 redraw_pending,
2572 frame_trace,
2573 "scroll",
2574 );
2575 }
2576}
2577
2578fn handle_cursor_left(
2579 last_cursor_position: Option<PhysicalPosition<f64>>,
2580 runtime: &mut Runtime,
2581 pipeline: &Pipeline,
2582 effect_result_tx: &mpsc::Sender<EffectResult>,
2583 event_proxy: &EventLoopProxy<TestEvent>,
2584 async_registry: &AsyncRegistry,
2585 active_services: &mut HashMap<ServiceKey, ActiveServiceHandle>,
2586 service_bindings: &mut HashMap<ServiceBindingKey, ServiceBindings>,
2587 next_service_instance_id: &mut u64,
2588 window: &Window,
2589 elwt: &EventLoopWindowTarget,
2590 last_redraw_at: &mut Instant,
2591 min_frame: Duration,
2592 redraw_pending: &mut bool,
2593 frame_trace: &mut FrameTraceState,
2594 invalidations: &mut InvalidationSet,
2595) {
2596 if let Some(ir) = &pipeline.prev_ir {
2597 let point = last_cursor_position
2598 .map(|position| window_physical_position_to_layout_point(window, position));
2599 match runtime.clear_hover_state(ir, point) {
2600 Ok(changed) => {
2601 sync_window_cursor(window, runtime);
2602 if changed {
2603 invalidations.mark_build();
2604 if process_pending_effects(
2605 runtime,
2606 effect_result_tx,
2607 event_proxy,
2608 async_registry,
2609 active_services,
2610 service_bindings,
2611 next_service_instance_id,
2612 ) {
2613 invalidations.mark_build();
2614 request_redraw_logged(
2615 window,
2616 elwt,
2617 last_redraw_at,
2618 min_frame,
2619 redraw_pending,
2620 frame_trace,
2621 "cursor_left:effects",
2622 );
2623 }
2624 request_redraw_logged(
2625 window,
2626 elwt,
2627 last_redraw_at,
2628 min_frame,
2629 redraw_pending,
2630 frame_trace,
2631 "cursor_left",
2632 );
2633 }
2634 }
2635 Err(error) => eprintln!("Cursor-left handling error: {:?}", error),
2636 }
2637 } else {
2638 sync_window_cursor(window, runtime);
2639 }
2640}
2641
2642fn parse_key_code(key: &str) -> KeyCode {
2644 match key {
2645 "Enter" => KeyCode::Enter,
2646 "Escape" => KeyCode::Escape,
2647 "Tab" => KeyCode::Tab,
2648 "Backspace" => KeyCode::Backspace,
2649 "Delete" => KeyCode::Delete,
2650 "Left" => KeyCode::Left,
2651 "Right" => KeyCode::Right,
2652 "Up" => KeyCode::Up,
2653 "Down" => KeyCode::Down,
2654 "Home" => KeyCode::Home,
2655 "End" => KeyCode::End,
2656 "PageUp" => KeyCode::PageUp,
2657 "PageDown" => KeyCode::PageDown,
2658 "Space" => KeyCode::Space,
2659 s if s.len() == 1 => KeyCode::Char(s.chars().next().unwrap()),
2660 _ => KeyCode::Space,
2661 }
2662}
2663
2664fn handle_key_down<S: GlobalState>(
2669 code: KeyCode,
2670 modifiers: u8,
2671 runtime: &mut Runtime,
2672 pipeline: &Pipeline,
2673 effect_result_tx: &mpsc::Sender<EffectResult>,
2674 event_proxy: &EventLoopProxy<TestEvent>,
2675 async_registry: &AsyncRegistry,
2676 active_services: &mut HashMap<ServiceKey, ActiveServiceHandle>,
2677 service_bindings: &mut HashMap<ServiceBindingKey, ServiceBindings>,
2678 next_service_instance_id: &mut u64,
2679 window: &Window,
2680 elwt: &EventLoopWindowTarget,
2681 last_redraw_at: &mut Instant,
2682 min_frame: Duration,
2683 redraw_pending: &mut bool,
2684 text_trace_enabled: bool,
2685 pending_text_traces: &mut VecDeque<PendingTextTrace>,
2686 next_text_trace_seq: &mut u64,
2687 presented_frames: u64,
2688 last_blink_toggle: &mut Instant,
2689 key_handler: Option<&KeyHandler<S>>,
2690 frame_trace: &mut FrameTraceState,
2691 invalidations: &mut InvalidationSet,
2692) -> bool {
2693 let ir_and_snap = match (&pipeline.prev_ir, &pipeline.last_snapshot) {
2694 (Some(ir), Some(snap)) => Some((ir, snap)),
2695 _ => None,
2696 };
2697
2698 if let Some(handler) = key_handler {
2700 let handler = handler.clone();
2701 if let Some(state) = runtime.get_global_state_mut::<S>() {
2702 if handler(state, &code, modifiers) {
2703 if process_pending_effects(
2704 runtime,
2705 effect_result_tx,
2706 event_proxy,
2707 async_registry,
2708 active_services,
2709 service_bindings,
2710 next_service_instance_id,
2711 ) {
2712 invalidations.mark_build();
2713 request_redraw_logged(
2714 window,
2715 elwt,
2716 last_redraw_at,
2717 min_frame,
2718 redraw_pending,
2719 frame_trace,
2720 "key_handler:effects",
2721 );
2722 }
2723 invalidations.mark_build();
2724 request_redraw_logged(
2725 window,
2726 elwt,
2727 last_redraw_at,
2728 min_frame,
2729 redraw_pending,
2730 frame_trace,
2731 "key_handler",
2732 );
2733 return true;
2734 }
2735 }
2736 }
2737
2738 if let Some((ir, layout)) = ir_and_snap {
2739 let target = focused_text_input_id(runtime, pipeline.prev_ir.as_ref());
2740 let trace_seq = start_text_trace(
2741 text_trace_enabled && target.is_some(),
2742 pending_text_traces,
2743 next_text_trace_seq,
2744 format!("keyboard:{:?}", code),
2745 target,
2746 presented_frames,
2747 );
2748 let input_event = InputEvent::Keyboard(FissionKeyEvent::Down {
2749 key_code: code,
2750 modifiers,
2751 });
2752 if let Err(e) = runtime.handle_input(input_event, ir, layout) {
2753 eprintln!("Keyboard error: {:?}", e);
2754 }
2755 invalidations.mark_build();
2756 mark_text_trace_handled(pending_text_traces, trace_seq);
2757 if process_pending_effects(
2758 runtime,
2759 effect_result_tx,
2760 event_proxy,
2761 async_registry,
2762 active_services,
2763 service_bindings,
2764 next_service_instance_id,
2765 ) {
2766 mark_text_trace_effects(pending_text_traces, trace_seq);
2767 invalidations.mark_build();
2768 request_redraw_logged(
2769 window,
2770 elwt,
2771 last_redraw_at,
2772 min_frame,
2773 redraw_pending,
2774 frame_trace,
2775 "keyboard:effects",
2776 );
2777 }
2778 reset_text_input_caret(runtime, pipeline.prev_ir.as_ref(), last_blink_toggle);
2779 request_redraw_logged(
2780 window,
2781 elwt,
2782 last_redraw_at,
2783 min_frame,
2784 redraw_pending,
2785 frame_trace,
2786 "keyboard",
2787 );
2788 }
2789
2790 false
2791}
2792
2793fn rects_intersect(a: LayoutRect, b: LayoutRect) -> bool {
2794 a.x() < b.right() && a.right() > b.x() && a.y() < b.bottom() && a.bottom() > b.y()
2795}
2796
2797fn visual_rect_for_node(
2798 ir: &CoreIR,
2799 snap: &fission_layout::LayoutSnapshot,
2800 scroll: &fission_core::ScrollStateMap,
2801 node_id: WidgetId,
2802) -> Option<LayoutRect> {
2803 let mut rect = snap.get_node_rect(node_id)?;
2804 let mut current = ir.nodes.get(&node_id).and_then(|node| node.parent);
2805 while let Some(parent_id) = current {
2806 let Some(parent) = ir.nodes.get(&parent_id) else {
2807 break;
2808 };
2809 if let fission_ir::Op::Layout(fission_ir::LayoutOp::Scroll { direction, .. }) = &parent.op {
2810 let offset = scroll.get_offset(parent_id);
2811 match direction {
2812 fission_ir::FlexDirection::Row => rect.origin.x -= offset,
2813 fission_ir::FlexDirection::Column => rect.origin.y -= offset,
2814 }
2815 }
2816 current = parent.parent;
2817 }
2818 Some(rect)
2819}
2820
2821fn rect_visible_in_scroll_ancestors(
2822 ir: &CoreIR,
2823 snap: &fission_layout::LayoutSnapshot,
2824 scroll: &fission_core::ScrollStateMap,
2825 node_id: WidgetId,
2826 rect: LayoutRect,
2827) -> bool {
2828 let viewport = LayoutRect::new(
2829 0.0,
2830 0.0,
2831 snap.viewport_size.width,
2832 snap.viewport_size.height,
2833 );
2834 if !rects_intersect(rect, viewport) {
2835 return false;
2836 }
2837
2838 let mut current = ir.nodes.get(&node_id).and_then(|node| node.parent);
2839 while let Some(parent_id) = current {
2840 let Some(parent) = ir.nodes.get(&parent_id) else {
2841 break;
2842 };
2843 if matches!(
2844 parent.op,
2845 fission_ir::Op::Layout(fission_ir::LayoutOp::Scroll { .. })
2846 | fission_ir::Op::Layout(fission_ir::LayoutOp::Clip { .. })
2847 ) {
2848 let Some(parent_rect) = visual_rect_for_node(ir, snap, scroll, parent_id) else {
2849 return false;
2850 };
2851 if !rects_intersect(rect, parent_rect) {
2852 return false;
2853 }
2854 }
2855 current = parent.parent;
2856 }
2857
2858 true
2859}
2860
2861fn build_get_text_response(
2863 pipeline: &Pipeline,
2864 scroll: &fission_core::ScrollStateMap,
2865) -> fission_test_driver::TestResponse {
2866 use fission_test_driver::{TestResponse, TextItem};
2867 let mut items = Vec::new();
2868 if let (Some(ir), Some(snap)) = (pipeline.prev_ir.as_ref(), pipeline.last_snapshot.as_ref()) {
2869 let mut reachable = std::collections::HashSet::new();
2870 let mut stack = ir.root.into_iter().collect::<Vec<_>>();
2871 while let Some(node_id) = stack.pop() {
2872 if !reachable.insert(node_id) {
2873 continue;
2874 }
2875 if let Some(node) = ir.nodes.get(&node_id) {
2876 stack.extend(node.children.iter().copied());
2877 }
2878 }
2879
2880 for id in reachable {
2881 let Some(node) = ir.nodes.get(&id) else {
2882 continue;
2883 };
2884 let text_content = match &node.op {
2885 fission_ir::Op::Paint(fission_ir::PaintOp::DrawText { text, .. }) => {
2886 Some(text.clone())
2887 }
2888 fission_ir::Op::Paint(fission_ir::PaintOp::DrawRichText { runs, .. }) => {
2889 Some(runs.iter().map(|r| r.text.clone()).collect::<String>())
2890 }
2891 _ => None,
2892 };
2893 if let Some(text) = text_content {
2894 if text.is_empty() {
2895 continue;
2896 }
2897 let check_id = node.parent.unwrap_or(id);
2898 let rect = visual_rect_for_node(ir, snap, scroll, check_id)
2899 .or_else(|| visual_rect_for_node(ir, snap, scroll, id));
2900 let (x, y, w, h) = rect
2901 .filter(|r| rect_visible_in_scroll_ancestors(ir, snap, scroll, id, *r))
2902 .map(|r| (r.x(), r.y(), r.width(), r.height()))
2903 .unwrap_or((0.0, 0.0, 0.0, 0.0));
2904 if w <= 0.0 || h <= 0.0 {
2905 continue;
2906 }
2907 items.push(TextItem {
2908 text,
2909 x,
2910 y,
2911 width: w,
2912 height: h,
2913 });
2914 }
2915 }
2916 }
2917 TestResponse::Text { items }
2918}
2919
2920fn find_visible_text_center(
2921 pipeline: &Pipeline,
2922 scroll: &fission_core::ScrollStateMap,
2923 text: &str,
2924) -> Option<(f32, f32)> {
2925 let fission_test_driver::TestResponse::Text { items } =
2926 build_get_text_response(pipeline, scroll)
2927 else {
2928 return None;
2929 };
2930 items
2931 .into_iter()
2932 .find(|item| item.text.contains(text) && item.width > 0.0 && item.height > 0.0)
2933 .map(|item| (item.x + item.width / 2.0, item.y + item.height / 2.0))
2934}
2935
2936fn build_get_tree_response(
2938 pipeline: &Pipeline,
2939 scroll: &fission_core::ScrollStateMap,
2940) -> fission_test_driver::TestResponse {
2941 use fission_test_driver::{SemanticNode, TestResponse};
2942 let mut nodes = Vec::new();
2943 if let (Some(ir), Some(snap)) = (&pipeline.prev_ir, &pipeline.last_snapshot) {
2944 for (id, node) in &ir.nodes {
2945 if let fission_ir::Op::Semantics(sem) = &node.op {
2946 let rect = visual_rect_for_node(ir, snap, scroll, *id)
2947 .filter(|r| rect_visible_in_scroll_ancestors(ir, snap, scroll, *id, *r));
2948 let (x, y, w, h) = rect
2949 .map(|r| (r.x(), r.y(), r.width(), r.height()))
2950 .unwrap_or((0.0, 0.0, 0.0, 0.0));
2951 if w <= 0.0 || h <= 0.0 {
2952 continue;
2953 }
2954 nodes.push(SemanticNode {
2955 role: format!("{:?}", sem.role),
2956 label: sem.label.clone(),
2957 value: sem.value.clone(),
2958 focusable: sem.focusable,
2959 x,
2960 y,
2961 width: w,
2962 height: h,
2963 });
2964 }
2965 }
2966 }
2967 TestResponse::Tree { nodes }
2968}
2969
2970fn handle_tap_text(
2972 text: &str,
2973 runtime: &mut Runtime,
2974 pipeline: &Pipeline,
2975) -> fission_test_driver::TestResponse {
2976 use fission_test_driver::TestResponse;
2977 if let (Some(ir), Some(snap)) = (pipeline.prev_ir.as_ref(), pipeline.last_snapshot.as_ref()) {
2978 if let Some((cx, cy)) =
2979 find_visible_text_center(pipeline, &runtime.runtime_state.scroll, text)
2980 {
2981 let point = LayoutPoint::new(cx, cy);
2982 let _ = runtime.handle_input(
2983 InputEvent::Pointer(PointerEvent::Down {
2984 point,
2985 button: PointerButton::Primary,
2986 modifiers: 0,
2987 }),
2988 ir,
2989 snap,
2990 );
2991 let _ = runtime.handle_input(
2992 InputEvent::Pointer(PointerEvent::Up {
2993 point,
2994 button: PointerButton::Primary,
2995 modifiers: 0,
2996 }),
2997 ir,
2998 snap,
2999 );
3000 TestResponse::Ok {}
3001 } else {
3002 TestResponse::Error {
3003 message: format!("text '{}' not found", text),
3004 }
3005 }
3006 } else {
3007 TestResponse::Error {
3008 message: "no frame rendered yet".into(),
3009 }
3010 }
3011}
3012
3013fn wrap_portal_for_viewport(
3014 id: Option<WidgetId>,
3015 node: fission_core::Widget,
3016 env: &Env,
3017) -> fission_core::Widget {
3018 let builder = fission_core::ui::Container::new(node)
3019 .width(env.viewport_size.width)
3020 .height(env.viewport_size.height);
3021 if let Some(id) = id {
3022 builder.id(fission_ir::WidgetId::derived(id.as_u128(), &[0x0000_F001]))
3023 } else {
3024 builder.into()
3025 }
3026}
3027
3028fn texture_plan_fits_device_limits(
3029 plan: &crate::pipeline::CompositorTexturePlan,
3030 scale_factor: f64,
3031 max_texture_dimension_2d: u32,
3032) -> bool {
3033 if plan.scene.is_some() {
3034 let width = ((plan.bounds.size.width as f64 * scale_factor).ceil() as u32).max(1);
3035 let height = ((plan.bounds.size.height as f64 * scale_factor).ceil() as u32).max(1);
3036 if width > max_texture_dimension_2d || height > max_texture_dimension_2d {
3037 return false;
3038 }
3039 }
3040 plan.children
3041 .iter()
3042 .all(|child| texture_plan_fits_device_limits(child, scale_factor, max_texture_dimension_2d))
3043}
3044
3045fn texture_plans_fit_device_limits(
3046 plans: &[crate::pipeline::CompositorTexturePlan],
3047 scale_factor: f64,
3048 max_texture_dimension_2d: u32,
3049) -> bool {
3050 plans
3051 .iter()
3052 .all(|plan| texture_plan_fits_device_limits(plan, scale_factor, max_texture_dimension_2d))
3053}
3054
3055pub type KeyHandler<S> = Arc<dyn Fn(&mut S, &fission_core::KeyCode, u8) -> bool + Send + Sync>;
3056pub type FrameHook<S> = Arc<dyn Fn(&mut S) -> bool + Send + Sync>;
3057
3058pub struct WinitApp<S: GlobalState, W>
3059where
3060 W: Clone + Into<Widget>,
3061{
3062 runtime: Runtime,
3063 layout_engine: LayoutEngine,
3064 root_widget: W,
3065 env: Env,
3066 pipeline: Pipeline,
3067 measurer: Arc<VelloTextMeasurer>,
3068 sync_env: Option<Arc<dyn Fn(&S, &mut Env) + Send + Sync>>,
3069 key_handler: Option<KeyHandler<S>>,
3070 frame_hook: Option<FrameHook<S>>,
3071 title: String,
3072 web_mount_selector: Option<String>,
3073 test_control_port: Option<u16>,
3074 effect_result_tx: mpsc::Sender<AsyncMessage>,
3076 effect_result_rx: mpsc::Receiver<AsyncMessage>,
3077 async_registry: AsyncRegistry,
3078 startup_action: Option<ActionEnvelope>,
3079 #[cfg(feature = "tray")]
3080 tray_config: Option<tray::TrayConfig<S>>,
3081 deep_link_config: DeepLinkConfig,
3082 startup_deep_links: Vec<DeepLink>,
3083 startup_notification_responses: Vec<NotificationResponse>,
3084 _phantom: std::marker::PhantomData<S>,
3085}
3086
3087impl<S, W> WinitApp<S, W>
3088where
3089 S: GlobalState + Default,
3090 W: Clone + Into<Widget> + 'static,
3091{
3092 pub fn new(root_widget: W) -> Self {
3093 Self::new_with_global_state(root_widget, S::default())
3094 }
3095
3096 pub fn new_with_global_state(root_widget: W, global_state: S) -> Self {
3097 let mut runtime = Runtime::default();
3098 runtime.add_global_state(Box::new(global_state)).unwrap();
3099
3100 const DEFAULT_FONT_FAMILY: &str = "Fission Default";
3101 let font_cx = Arc::new(Mutex::new(build_font_context()));
3102 {
3103 let mut font_cx = font_cx.lock().unwrap();
3104 let font_data = fonts::default_font_bytes().to_vec();
3105 let info_override = FontInfoOverride {
3106 family_name: Some(DEFAULT_FONT_FAMILY),
3107 ..Default::default()
3108 };
3109 font_cx
3110 .collection
3111 .register_fonts(Blob::from(font_data), Some(info_override));
3112 }
3113 let measurer = Arc::new(VelloTextMeasurer::new_with_default_family(
3114 font_cx.clone(),
3115 DEFAULT_FONT_FAMILY,
3116 ));
3117 let env = Env::new(measurer.clone() as Arc<dyn fission_layout::TextMeasurer>);
3118 let clipboard: Arc<dyn fission_core::env::Clipboard> = Arc::new(DesktopClipboard::new());
3119
3120 let layout_engine = LayoutEngine::new().with_measurer(measurer.clone());
3121 let runtime = runtime
3122 .with_measurer(measurer.clone())
3123 .with_clipboard(clipboard);
3124
3125 let (effect_result_tx, effect_result_rx) = mpsc::channel();
3126 let mut async_registry = AsyncRegistry::new();
3127 register_builtin_operation_capabilities(&mut async_registry);
3128
3129 Self {
3130 runtime,
3131 layout_engine,
3132 root_widget,
3133 env,
3134 pipeline: Pipeline::new(),
3135 measurer,
3136 sync_env: None,
3137 key_handler: None,
3138 frame_hook: None,
3139 title: "Fission".into(),
3140 web_mount_selector: None,
3141 test_control_port: None,
3142 effect_result_tx,
3143 effect_result_rx,
3144 async_registry,
3145 startup_action: None,
3146 #[cfg(feature = "tray")]
3147 tray_config: None,
3148 deep_link_config: DeepLinkConfig::default(),
3149 startup_deep_links: Vec::new(),
3150 startup_notification_responses: Vec::new(),
3151 _phantom: std::marker::PhantomData,
3152 }
3153 }
3154
3155 pub fn with_global_state(mut self, global_state: S) -> Self {
3156 *self.runtime.get_global_state_mut::<S>().expect(
3157 "Fission global state must be registered before WinitApp::with_global_state is called",
3158 ) = global_state;
3159 self
3160 }
3161
3162 pub fn with_key_handler<F>(mut self, handler: F) -> Self
3163 where
3164 F: Fn(&mut S, &fission_core::KeyCode, u8) -> bool + Send + Sync + 'static,
3165 {
3166 self.key_handler = Some(Arc::new(handler));
3167 self
3168 }
3169
3170 pub fn with_title(mut self, title: impl Into<String>) -> Self {
3171 self.title = title.into();
3172 self.env.window.title = fission_core::WindowTitle::plain(self.title.clone());
3173 self
3174 }
3175
3176 pub fn with_test_control_port(mut self, port: u16) -> Self {
3177 self.test_control_port = Some(port);
3178 self
3179 }
3180
3181 pub fn with_mount_selector(mut self, selector: impl Into<String>) -> Self {
3182 self.web_mount_selector = Some(selector.into());
3183 self
3184 }
3185
3186 pub fn with_state_init<F>(mut self, init: F) -> Self
3188 where
3189 F: FnOnce(&mut S),
3190 {
3191 if let Some(state) = self.runtime.get_global_state_mut::<S>() {
3192 init(state);
3193 }
3194 self
3195 }
3196
3197 pub fn with_env(mut self, env: Env) -> Self {
3198 self.env = env;
3199 self
3200 }
3201
3202 pub fn with_design_system<D: fission_theme::DesignSystem>(
3203 mut self,
3204 mode: fission_theme::DesignMode,
3205 ) -> Self {
3206 self.env.theme = D::theme(mode);
3207 self
3208 }
3209
3210 pub fn with_sync_env<F>(mut self, f: F) -> Self
3211 where
3212 F: Fn(&S, &mut Env) + Send + Sync + 'static,
3213 {
3214 self.sync_env = Some(Arc::new(f));
3215 self
3216 }
3217
3218 pub fn with_frame_hook<F>(mut self, f: F) -> Self
3222 where
3223 F: Fn(&mut S) -> bool + Send + Sync + 'static,
3224 {
3225 self.frame_hook = Some(Arc::new(f));
3226 self
3227 }
3228
3229 pub fn with_async<F>(mut self, configure: F) -> Self
3230 where
3231 F: FnOnce(&mut AsyncRegistry),
3232 {
3233 configure(&mut self.async_registry);
3234 self
3235 }
3236
3237 pub fn with_notification_host<H>(mut self, host: H) -> Self
3243 where
3244 H: NotificationHost,
3245 {
3246 notifications::register_notification_capabilities(&mut self.async_registry, Arc::new(host));
3247 self
3248 }
3249
3250 pub fn with_nfc_host<H>(mut self, host: H) -> Self
3256 where
3257 H: NfcHost,
3258 {
3259 nfc::register_nfc_capabilities(&mut self.async_registry, Arc::new(host));
3260 self
3261 }
3262
3263 pub fn with_biometric_host<H>(mut self, host: H) -> Self
3269 where
3270 H: BiometricHost,
3271 {
3272 biometric::register_biometric_capabilities(&mut self.async_registry, Arc::new(host));
3273 self
3274 }
3275
3276 pub fn with_passkey_host<H>(mut self, host: H) -> Self
3283 where
3284 H: PasskeyHost,
3285 {
3286 passkey::register_passkey_capabilities(&mut self.async_registry, Arc::new(host));
3287 self
3288 }
3289
3290 pub fn with_bluetooth_host<H>(mut self, host: H) -> Self
3296 where
3297 H: BluetoothHost,
3298 {
3299 bluetooth::register_bluetooth_capabilities(&mut self.async_registry, Arc::new(host));
3300 self
3301 }
3302
3303 pub fn with_barcode_scanner_host<H>(mut self, host: H) -> Self
3309 where
3310 H: BarcodeScannerHost,
3311 {
3312 barcode::register_barcode_scanner_capabilities(&mut self.async_registry, Arc::new(host));
3313 self
3314 }
3315
3316 pub fn with_camera_host<H>(mut self, host: H) -> Self
3322 where
3323 H: CameraHost,
3324 {
3325 camera::register_camera_capabilities(&mut self.async_registry, Arc::new(host));
3326 self
3327 }
3328
3329 pub fn with_clipboard_host<H>(mut self, host: H) -> Self
3335 where
3336 H: ClipboardHost,
3337 {
3338 clipboard::register_clipboard_capabilities(&mut self.async_registry, Arc::new(host));
3339 self
3340 }
3341
3342 pub fn with_geolocation_host<H>(mut self, host: H) -> Self
3348 where
3349 H: GeolocationHost,
3350 {
3351 geolocation::register_geolocation_capabilities(&mut self.async_registry, Arc::new(host));
3352 self
3353 }
3354
3355 pub fn with_haptic_host<H>(mut self, host: H) -> Self
3360 where
3361 H: HapticHost,
3362 {
3363 haptics::register_haptic_capabilities(&mut self.async_registry, Arc::new(host));
3364 self
3365 }
3366
3367 pub fn with_microphone_host<H>(mut self, host: H) -> Self
3372 where
3373 H: MicrophoneHost,
3374 {
3375 microphone::register_microphone_capabilities(&mut self.async_registry, Arc::new(host));
3376 self
3377 }
3378
3379 pub fn with_wifi_host<H>(mut self, host: H) -> Self
3385 where
3386 H: WifiHost,
3387 {
3388 wifi::register_wifi_capabilities(&mut self.async_registry, Arc::new(host));
3389 self
3390 }
3391
3392 pub fn with_volume_host<H>(mut self, host: H) -> Self
3398 where
3399 H: VolumeHost,
3400 {
3401 volume::register_volume_capabilities(&mut self.async_registry, Arc::new(host));
3402 self
3403 }
3404
3405 pub fn with_startup_action<A: Action>(mut self, action: A) -> Self {
3406 self.startup_action = Some(action.into());
3407 self
3408 }
3409
3410 #[cfg(feature = "tray")]
3411 pub fn with_tray(mut self, config: tray::TrayConfig<S>) -> Self {
3412 self.tray_config = Some(config);
3413 self
3414 }
3415
3416 pub fn with_deep_link_config(mut self, config: DeepLinkConfig) -> Self {
3422 self.deep_link_config = config;
3423 self
3424 }
3425
3426 pub fn with_deep_link_scheme(mut self, scheme: impl Into<String>) -> Self {
3431 self.deep_link_config = self.deep_link_config.scheme(scheme);
3432 self
3433 }
3434
3435 pub fn with_deep_link_domain(mut self, domain: impl Into<String>) -> Self {
3440 self.deep_link_config = self.deep_link_config.domain(domain);
3441 self
3442 }
3443
3444 pub fn with_startup_deep_link(mut self, link: DeepLink) -> Self {
3449 self.startup_deep_links.push(link);
3450 self
3451 }
3452
3453 pub fn with_startup_notification_response(mut self, response: NotificationResponse) -> Self {
3459 self.startup_notification_responses.push(response);
3460 self
3461 }
3462
3463 pub fn on_deep_link<H>(mut self, handler: H) -> Self
3469 where
3470 H: fission_core::registry::IntoHandler<S, DeepLinkReceived> + Send + Sync + 'static,
3471 {
3472 let mut registry = ActionRegistry::<S>::new();
3473 registry.register(handler);
3474 self.runtime.absorb_persistent_registry(registry);
3475 self
3476 }
3477
3478 pub fn on_notification_response<H>(mut self, handler: H) -> Self
3484 where
3485 H: fission_core::registry::IntoHandler<S, NotificationResponseReceived>
3486 + Send
3487 + Sync
3488 + 'static,
3489 {
3490 let mut registry = ActionRegistry::<S>::new();
3491 registry.register(handler);
3492 self.runtime.absorb_persistent_registry(registry);
3493 self
3494 }
3495
3496 pub fn with_route_handler(
3502 mut self,
3503 handler: fission_core::registry::Handler<S, fission_core::ShellRouteChanged>,
3504 ) -> Self {
3505 let mut registry = ActionRegistry::<S>::new();
3506 registry.register::<fission_core::ShellRouteChanged, _>(handler);
3507 self.runtime.absorb_persistent_registry(registry);
3508 self
3509 }
3510
3511 pub fn register_reducer(
3512 &mut self,
3513 action_id: ActionId,
3514 reducer: fission_core::action::Reducer<S>,
3515 ) -> Result<()> {
3516 self.runtime.register_reducer::<S>(action_id, reducer)
3517 }
3518
3519 pub fn absorb_registry(&mut self, registry: fission_core::ActionRegistry<S>) {
3520 self.runtime.absorb_persistent_registry(registry);
3521 }
3522
3523 pub fn run(self) -> Result<()> {
3524 self.run_inner(
3525 #[cfg(target_os = "android")]
3526 None,
3527 )
3528 }
3529
3530 #[cfg(target_os = "android")]
3531 pub fn run_with_android_app(self, android_app: AndroidApp) -> Result<()> {
3532 self.run_inner(Some(android_app))
3533 }
3534
3535 fn run_inner(
3536 mut self,
3537 #[cfg(target_os = "android")] android_app: Option<AndroidApp>,
3538 ) -> Result<()> {
3539 diag::emit(
3540 diag::DiagCategory::Frame,
3541 diag::DiagLevel::Info,
3542 diag::DiagEventKind::FrameStart { root: None },
3543 );
3544 diag::init_from_env();
3545
3546 let background_test_mode = std::env::var_os("FISSION_BACKGROUND_TEST").is_some();
3549 let mut event_loop_builder = EventLoop::<TestEvent>::with_user_event();
3550 #[cfg(target_os = "android")]
3551 if let Some(app) = android_app.as_ref() {
3552 android_capabilities::register_android_operation_capabilities(
3553 &mut self.async_registry,
3554 app,
3555 );
3556 }
3557 #[cfg(target_os = "android")]
3558 if let Some(app) = android_app {
3559 event_loop_builder.with_android_app(app);
3560 }
3561 #[cfg(target_os = "macos")]
3562 if background_test_mode {
3563 event_loop_builder.with_activation_policy(ActivationPolicy::Accessory);
3564 event_loop_builder.with_activate_ignoring_other_apps(false);
3565 event_loop_builder.with_default_menu(false);
3566 }
3567 let event_loop = event_loop_builder
3568 .build()
3569 .map_err(|e| anyhow::anyhow!("Event loop error: {}", e))?;
3570 let event_proxy = event_loop.create_proxy();
3571 let mut accessibility_bridge = AccessibilityBridge::new(event_proxy.clone());
3572 #[cfg(feature = "tray")]
3573 let tray_event_rx = self
3574 .tray_config
3575 .as_ref()
3576 .map(|_| tray::install_event_forwarders(event_proxy.clone()));
3577 #[cfg(feature = "tray")]
3578 let tray_config = self.tray_config.clone();
3579 let window_title = self.title.clone();
3580 let web_mount_selector = self.web_mount_selector;
3581 let ime_handler = Arc::new(DesktopImeHandler::default());
3582 self.runtime = self.runtime.with_ime_handler(ime_handler.clone());
3583
3584 #[cfg(not(target_os = "android"))]
3585 let platform_window = build_window_before_run(
3586 &window_title,
3587 background_test_mode,
3588 &event_loop,
3589 web_mount_selector.as_deref(),
3590 )?;
3591 #[cfg(not(target_os = "android"))]
3592 ime_handler.set_window(Some(platform_window.clone()));
3593 #[cfg(target_os = "android")]
3594 let mut platform_window: Option<Arc<Window>> = None;
3595
3596 #[cfg(target_os = "android")]
3599 if std::env::var_os("WGPU_BACKEND").is_none() {
3600 eprintln!("fission-shell-winit: forcing WGPU_BACKEND=gl on Android");
3601 std::env::set_var("WGPU_BACKEND", "gl");
3602 }
3603 #[cfg(not(target_arch = "wasm32"))]
3604 let mut render_cx = RenderContext::new();
3605 #[cfg(not(target_arch = "wasm32"))]
3606 let mut render_state: Option<RenderState<'_>> = None;
3607 #[cfg(target_arch = "wasm32")]
3608 let mut web_renderer: Option<WebRenderer> = None;
3609 #[cfg(target_arch = "wasm32")]
3610 let pending_webgpu_init: PendingWebGpuInit = Rc::new(RefCell::new(None));
3611 #[cfg(target_arch = "wasm32")]
3612 let mut webgpu_init_in_flight = false;
3613 #[cfg(target_arch = "wasm32")]
3614 let mut web_renderer_reported = false;
3615 #[cfg(not(target_arch = "wasm32"))]
3616 let mut scene = Scene::new();
3617 #[cfg(not(target_arch = "wasm32"))]
3618 let mut retained_scene_cache = RetainedSceneCache::default();
3619
3620 #[cfg(not(target_os = "android"))]
3621 platform_window.request_redraw();
3622
3623 let mut startup_deep_links = self.startup_deep_links.clone();
3624 startup_deep_links.extend(collect_startup_deep_links(&self.deep_link_config));
3625 let startup_notification_responses = self.startup_notification_responses.clone();
3626
3627 let mut runtime = self.runtime;
3628 for link in startup_deep_links {
3629 runtime.dispatch(DeepLinkReceived { link }.into(), WidgetId::from_u128(0))?;
3630 }
3631 for response in startup_notification_responses {
3632 runtime.dispatch(
3633 NotificationResponseReceived { response }.into(),
3634 WidgetId::from_u128(0),
3635 )?;
3636 }
3637 let mut layout_engine = self.layout_engine;
3638 let root_widget = self.root_widget;
3639 let mut env = self.env;
3640 env.window.title = fission_core::WindowTitle::plain(window_title.clone());
3641 let mut applied_window_title = window_title.clone();
3642 let mut pipeline = self.pipeline;
3643 let measurer = self.measurer;
3644 let effect_result_tx = self.effect_result_tx;
3645 let effect_result_rx = self.effect_result_rx;
3646 let async_registry = self.async_registry;
3647 let startup_action = self.startup_action;
3648 let mut startup_dispatched = false;
3649 let mut next_service_instance_id = 1_u64;
3650 let mut active_services: HashMap<ServiceKey, ActiveServiceHandle> = HashMap::new();
3651 let mut service_bindings: HashMap<ServiceBindingKey, ServiceBindings> = HashMap::new();
3652
3653 #[cfg(not(target_os = "android"))]
3654 let video_backend = create_video_backend(Some(&platform_window));
3655 #[cfg(target_os = "android")]
3656 let video_backend = create_video_backend(platform_window.as_deref());
3657 #[cfg(not(target_os = "android"))]
3658 let web_backend = PlatformWebBackend::new(Some(&platform_window));
3659 #[cfg(target_os = "android")]
3660 let web_backend = PlatformWebBackend::new(platform_window.as_deref());
3661 let mut players: HashMap<WidgetId, ActivePlayer> = HashMap::new();
3662
3663 let mut last_cursor_position: Option<PhysicalPosition<f64>> = None;
3664 let mut active_primary_touch: Option<u64> = None;
3665 let mut touch_positions: HashMap<u64, PhysicalPosition<f64>> = HashMap::new();
3666 let max_fps = std::env::var("FISSION_MAX_FPS")
3667 .ok()
3668 .and_then(|v| v.parse::<u32>().ok())
3669 .filter(|v| *v > 0)
3670 .unwrap_or(60);
3671 let min_frame = Duration::from_secs_f32(1.0 / max_fps as f32);
3672 let repeat_animation_fps = std::env::var("FISSION_REPEAT_ANIMATION_FPS")
3673 .ok()
3674 .and_then(|v| v.parse::<u32>().ok())
3675 .filter(|v| *v > 0)
3676 .map(|v| v.min(max_fps))
3677 .unwrap_or(10);
3678 let repeat_animation_frame = Duration::from_secs_f32(1.0 / repeat_animation_fps as f32);
3679 let resize_fps = std::env::var("FISSION_RESIZE_FPS")
3680 .ok()
3681 .and_then(|v| v.parse::<u32>().ok())
3682 .filter(|v| *v > 0)
3683 .map(|v| v.min(max_fps))
3684 .unwrap_or(60);
3685 let resize_frame = Duration::from_secs_f32(1.0 / resize_fps as f32);
3686 let resize_settle_delay = Duration::from_millis(
3687 std::env::var("FISSION_RESIZE_SETTLE_MS")
3688 .ok()
3689 .and_then(|v| v.parse::<u64>().ok())
3690 .filter(|v| *v > 0)
3691 .unwrap_or(90),
3692 );
3693 let mut last_redraw_at = Instant::now()
3694 .checked_sub(min_frame)
3695 .unwrap_or_else(Instant::now);
3696 let mut redraw_pending = false;
3697 let mut last_frame_time = Instant::now();
3698 let blink_enabled = std::env::var("FISSION_TEXTINPUT_BLINK")
3699 .map(|v| !matches!(v.to_ascii_lowercase().as_str(), "0" | "false" | "no"))
3700 .unwrap_or(true);
3701 let blink_period = Duration::from_millis(
3702 std::env::var("FISSION_TEXTINPUT_BLINK_MS")
3703 .ok()
3704 .and_then(|v| v.parse::<u64>().ok())
3705 .filter(|v| *v > 0)
3706 .unwrap_or(530),
3707 );
3708 let mut last_blink_toggle = Instant::now();
3709 let mut blink_focus_id: Option<WidgetId> = None;
3710 let text_trace_enabled = std::env::var("FISSION_TEXT_TRACE")
3711 .map(|v| matches!(v.to_ascii_lowercase().as_str(), "1" | "true" | "yes"))
3712 .unwrap_or(false);
3713 let mut frame_trace = FrameTraceState::new(frame_trace_enabled());
3714 let mut presented_frames: u64 = 0;
3715 let mut next_text_trace_seq: u64 = 0;
3716 let mut pending_text_traces: VecDeque<PendingTextTrace> = VecDeque::new();
3717 #[cfg(target_arch = "wasm32")]
3718 let mut pending_web_input_at: Option<Instant> = None;
3719 let mut current_mods: u8 = 0;
3720
3721 #[cfg(not(target_arch = "wasm32"))]
3726 let test_control_port = self.test_control_port.or_else(|| {
3727 std::env::var("FISSION_TEST_CONTROL_PORT")
3728 .ok()
3729 .and_then(|v| v.parse::<u16>().ok())
3730 });
3731 #[cfg(all(target_os = "android", not(target_arch = "wasm32")))]
3732 let pending_test_events = test_control::create_pending_event_queue();
3733 #[cfg(not(target_arch = "wasm32"))]
3734 let test_control_enabled = test_control_port
3735 .map(|port| {
3736 #[cfg(target_os = "android")]
3737 let injector = test_control::EventInjector::Queue {
3738 queue: pending_test_events.clone(),
3739 wake_proxy: Some(event_proxy.clone()),
3740 };
3741 #[cfg(not(target_os = "android"))]
3742 let injector = test_control::EventInjector::Proxy(event_proxy.clone());
3743 test_control::spawn_server(port, injector);
3744 true
3745 })
3746 .unwrap_or(false);
3747 #[cfg(target_arch = "wasm32")]
3748 let test_control_enabled = false;
3749 #[cfg(not(target_os = "android"))]
3750 let _ = test_control_enabled;
3751 let mut pending_screenshot_path: Option<String> = None;
3753 let mut pending_screenshot_response_tx: Option<test_control::ResponseSender> = None;
3754 #[cfg(not(target_os = "android"))]
3755 let mut window_viewport = WindowViewportState::from_window(&platform_window);
3756 #[cfg(target_os = "android")]
3757 let mut window_viewport: Option<WindowViewportState> = None;
3758 #[cfg(not(target_os = "android"))]
3759 let mut pending_resize = Some(window_viewport);
3760 #[cfg(target_os = "android")]
3761 let mut pending_resize = None;
3762 let mut resize_needs_settled_frame = pending_resize.is_some();
3763 let mut pending_capture_settle = false;
3764 let mut last_built_viewport: Option<LayoutSize> = None;
3765 let mut live_resize = LiveResizeController::new(resize_settle_delay);
3766 #[cfg(feature = "tray")]
3767 let mut active_tray: Option<tray::ActiveTray<S>> = None;
3768 let mut invalidations = InvalidationSet {
3769 build: true,
3770 layout: true,
3771 paint: true,
3772 composite: true,
3773 };
3774 let mut vello_image_cache_generation = fission_render_vello::image_cache_generation();
3775 let mut software_image_cache_generation = software_renderer::image_cache_generation();
3776
3777 let event_handler = move |event: Event<TestEvent>, elwt: &EventLoopWindowTarget| {
3778 elwt.set_control_flow(ControlFlow::Wait);
3779 let debug_android_events = cfg!(target_os = "android")
3780 && std::env::var_os("FISSION_DEBUG_ANDROID_EVENTS").is_some();
3781
3782 let mut handle_test_event = |test_event: TestEvent| {
3783 if debug_android_events {
3784 eprintln!("[android-events] user_event={test_event:?}");
3785 }
3786 match test_event {
3787 TestEvent::MouseMove { x, y } => {
3788 let Some(window) = platform_window.active_window() else {
3789 return;
3790 };
3791 let scale_factor = window.scale_factor();
3792 last_cursor_position = Some(PhysicalPosition::new(
3793 (x as f64) * scale_factor,
3794 (y as f64) * scale_factor,
3795 ));
3796 handle_cursor_moved(
3797 x,
3798 y,
3799 0,
3800 &mut runtime,
3801 &pipeline,
3802 &effect_result_tx,
3803 &event_proxy,
3804 &async_registry,
3805 &mut active_services,
3806 &mut service_bindings,
3807 &mut next_service_instance_id,
3808 window,
3809 elwt,
3810 &mut last_redraw_at,
3811 min_frame,
3812 &mut redraw_pending,
3813 &mut frame_trace,
3814 &mut invalidations,
3815 );
3816 }
3817 TestEvent::MouseDown { x, y, button } => {
3818 let Some(window) = platform_window.active_window() else {
3819 return;
3820 };
3821 let btn = map_test_button(button);
3822 handle_mouse_button(
3823 x,
3824 y,
3825 btn,
3826 true,
3827 0,
3828 &mut runtime,
3829 &pipeline,
3830 &effect_result_tx,
3831 &event_proxy,
3832 &async_registry,
3833 &mut active_services,
3834 &mut service_bindings,
3835 &mut next_service_instance_id,
3836 window,
3837 elwt,
3838 &mut last_redraw_at,
3839 min_frame,
3840 &mut redraw_pending,
3841 text_trace_enabled,
3842 &mut pending_text_traces,
3843 &mut next_text_trace_seq,
3844 presented_frames,
3845 &mut last_blink_toggle,
3846 &mut frame_trace,
3847 &mut invalidations,
3848 );
3849 }
3850 TestEvent::MouseUp { x, y, button } => {
3851 let Some(window) = platform_window.active_window() else {
3852 return;
3853 };
3854 let btn = map_test_button(button);
3855 handle_mouse_button(
3856 x,
3857 y,
3858 btn,
3859 false,
3860 0,
3861 &mut runtime,
3862 &pipeline,
3863 &effect_result_tx,
3864 &event_proxy,
3865 &async_registry,
3866 &mut active_services,
3867 &mut service_bindings,
3868 &mut next_service_instance_id,
3869 window,
3870 elwt,
3871 &mut last_redraw_at,
3872 min_frame,
3873 &mut redraw_pending,
3874 text_trace_enabled,
3875 &mut pending_text_traces,
3876 &mut next_text_trace_seq,
3877 presented_frames,
3878 &mut last_blink_toggle,
3879 &mut frame_trace,
3880 &mut invalidations,
3881 );
3882 }
3883 TestEvent::KeyDown {
3884 key_code,
3885 modifiers,
3886 } => {
3887 let Some(window) = platform_window.active_window() else {
3888 return;
3889 };
3890 let code = parse_key_code(&key_code);
3891 handle_key_down::<S>(
3892 code,
3893 modifiers,
3894 &mut runtime,
3895 &pipeline,
3896 &effect_result_tx,
3897 &event_proxy,
3898 &async_registry,
3899 &mut active_services,
3900 &mut service_bindings,
3901 &mut next_service_instance_id,
3902 window,
3903 elwt,
3904 &mut last_redraw_at,
3905 min_frame,
3906 &mut redraw_pending,
3907 text_trace_enabled,
3908 &mut pending_text_traces,
3909 &mut next_text_trace_seq,
3910 presented_frames,
3911 &mut last_blink_toggle,
3912 self.key_handler.as_ref(),
3913 &mut frame_trace,
3914 &mut invalidations,
3915 );
3916 }
3917 TestEvent::KeyUp { .. } => {
3918 let Some(window) = platform_window.active_window() else {
3919 return;
3920 };
3921 request_redraw_logged(
3922 window,
3923 elwt,
3924 &mut last_redraw_at,
3925 min_frame,
3926 &mut redraw_pending,
3927 &mut frame_trace,
3928 "test_key_up",
3929 );
3930 }
3931 TestEvent::TextInput { text } => {
3932 let Some(window) = platform_window.active_window() else {
3933 return;
3934 };
3935 if let (Some(ir), Some(layout)) =
3936 (&pipeline.prev_ir, &pipeline.last_snapshot)
3937 {
3938 let target = focused_text_input_id(&runtime, pipeline.prev_ir.as_ref());
3939 if target.is_some()
3940 || focused_custom_text_input(&runtime, pipeline.prev_ir.as_ref())
3941 {
3942 let trace_seq = start_text_trace(
3943 text_trace_enabled && target.is_some(),
3944 &mut pending_text_traces,
3945 &mut next_text_trace_seq,
3946 format!("test_text_input:{}", text.chars().count()),
3947 target,
3948 presented_frames,
3949 );
3950 runtime
3951 .handle_input(
3952 InputEvent::Ime(fission_core::event::ImeEvent::Commit {
3953 text: text.clone(),
3954 }),
3955 ir,
3956 layout,
3957 )
3958 .ok();
3959 invalidations.mark_build();
3960 mark_text_trace_handled(&mut pending_text_traces, trace_seq);
3961 if process_pending_effects(
3962 &mut runtime,
3963 &effect_result_tx,
3964 &event_proxy,
3965 &async_registry,
3966 &mut active_services,
3967 &mut service_bindings,
3968 &mut next_service_instance_id,
3969 ) {
3970 mark_text_trace_effects(&mut pending_text_traces, trace_seq);
3971 invalidations.mark_build();
3972 }
3973 request_redraw_logged(
3974 window,
3975 elwt,
3976 &mut last_redraw_at,
3977 min_frame,
3978 &mut redraw_pending,
3979 &mut frame_trace,
3980 "test_text_input",
3981 );
3982 } else {
3983 for ch in text.chars() {
3984 let key = if ch == ' ' {
3985 KeyCode::Space
3986 } else if ch == '\n' {
3987 KeyCode::Enter
3988 } else {
3989 KeyCode::Char(ch)
3990 };
3991 handle_key_down::<S>(
3992 key,
3993 0,
3994 &mut runtime,
3995 &pipeline,
3996 &effect_result_tx,
3997 &event_proxy,
3998 &async_registry,
3999 &mut active_services,
4000 &mut service_bindings,
4001 &mut next_service_instance_id,
4002 window,
4003 elwt,
4004 &mut last_redraw_at,
4005 min_frame,
4006 &mut redraw_pending,
4007 text_trace_enabled,
4008 &mut pending_text_traces,
4009 &mut next_text_trace_seq,
4010 presented_frames,
4011 &mut last_blink_toggle,
4012 self.key_handler.as_ref(),
4013 &mut frame_trace,
4014 &mut invalidations,
4015 );
4016 }
4017 }
4018 }
4019 }
4020 TestEvent::ImePreedit { text, cursor } => {
4021 let Some(window) = platform_window.active_window() else {
4022 return;
4023 };
4024 if let (Some(ir), Some(layout)) =
4025 (&pipeline.prev_ir, &pipeline.last_snapshot)
4026 {
4027 let target = focused_text_input_id(&runtime, pipeline.prev_ir.as_ref());
4028 let trace_seq = start_text_trace(
4029 text_trace_enabled && target.is_some(),
4030 &mut pending_text_traces,
4031 &mut next_text_trace_seq,
4032 format!("test_ime_preedit:{}", text.chars().count()),
4033 target,
4034 presented_frames,
4035 );
4036 runtime
4037 .handle_input(
4038 InputEvent::Ime(fission_core::event::ImeEvent::Preedit {
4039 text,
4040 cursor,
4041 }),
4042 ir,
4043 layout,
4044 )
4045 .ok();
4046 invalidations.mark_build();
4047 mark_text_trace_handled(&mut pending_text_traces, trace_seq);
4048 request_redraw_logged(
4049 window,
4050 elwt,
4051 &mut last_redraw_at,
4052 min_frame,
4053 &mut redraw_pending,
4054 &mut frame_trace,
4055 "test_ime_preedit",
4056 );
4057 }
4058 }
4059 TestEvent::ImeCommit { text } => {
4060 let Some(window) = platform_window.active_window() else {
4061 return;
4062 };
4063 if let (Some(ir), Some(layout)) =
4064 (&pipeline.prev_ir, &pipeline.last_snapshot)
4065 {
4066 let target = focused_text_input_id(&runtime, pipeline.prev_ir.as_ref());
4067 let trace_seq = start_text_trace(
4068 text_trace_enabled && target.is_some(),
4069 &mut pending_text_traces,
4070 &mut next_text_trace_seq,
4071 format!("test_ime_commit:{}", text.chars().count()),
4072 target,
4073 presented_frames,
4074 );
4075 runtime
4076 .handle_input(
4077 InputEvent::Ime(fission_core::event::ImeEvent::Commit { text }),
4078 ir,
4079 layout,
4080 )
4081 .ok();
4082 invalidations.mark_build();
4083 mark_text_trace_handled(&mut pending_text_traces, trace_seq);
4084 request_redraw_logged(
4085 window,
4086 elwt,
4087 &mut last_redraw_at,
4088 min_frame,
4089 &mut redraw_pending,
4090 &mut frame_trace,
4091 "test_ime_commit",
4092 );
4093 }
4094 }
4095 TestEvent::ImeCancel => {
4096 let Some(window) = platform_window.active_window() else {
4097 return;
4098 };
4099 if let (Some(ir), Some(layout)) =
4100 (&pipeline.prev_ir, &pipeline.last_snapshot)
4101 {
4102 let target = focused_text_input_id(&runtime, pipeline.prev_ir.as_ref());
4103 let trace_seq = start_text_trace(
4104 text_trace_enabled && target.is_some(),
4105 &mut pending_text_traces,
4106 &mut next_text_trace_seq,
4107 "test_ime_cancel".to_string(),
4108 target,
4109 presented_frames,
4110 );
4111 runtime
4112 .handle_input(
4113 InputEvent::Ime(fission_core::event::ImeEvent::Cancel),
4114 ir,
4115 layout,
4116 )
4117 .ok();
4118 invalidations.mark_build();
4119 mark_text_trace_handled(&mut pending_text_traces, trace_seq);
4120 request_redraw_logged(
4121 window,
4122 elwt,
4123 &mut last_redraw_at,
4124 min_frame,
4125 &mut redraw_pending,
4126 &mut frame_trace,
4127 "test_ime_cancel",
4128 );
4129 }
4130 }
4131 TestEvent::Scroll { x, y, dx, dy } => {
4132 let Some(window) = platform_window.active_window() else {
4133 return;
4134 };
4135 handle_scroll(
4136 x,
4137 y,
4138 dx,
4139 dy,
4140 0,
4141 &mut runtime,
4142 &pipeline,
4143 &effect_result_tx,
4144 &event_proxy,
4145 &async_registry,
4146 &mut active_services,
4147 &mut service_bindings,
4148 &mut next_service_instance_id,
4149 window,
4150 elwt,
4151 &mut last_redraw_at,
4152 min_frame,
4153 &mut redraw_pending,
4154 &mut frame_trace,
4155 &mut invalidations,
4156 );
4157 }
4158 TestEvent::Resize { width, height } => {
4159 let Some(window) = platform_window.active_window() else {
4160 return;
4161 };
4162 if width > 0 && height > 0 {
4163 let requested_logical_size =
4164 LayoutSize::new(width as f32, height as f32);
4165 let current_viewport = pending_resize
4166 .unwrap_or_else(|| WindowViewportState::from_window(window))
4167 .with_logical_size(requested_logical_size);
4168 #[cfg(not(any(target_os = "android", target_os = "ios")))]
4169 {
4170 let _ = window.request_inner_size(
4171 native_window_size_for_logical_viewport(requested_logical_size),
4172 );
4173 }
4174 #[cfg(not(target_os = "android"))]
4175 {
4176 window_viewport = current_viewport;
4177 }
4178 #[cfg(target_os = "android")]
4179 {
4180 window_viewport = Some(current_viewport);
4181 }
4182 apply_authoritative_resize(
4183 window,
4184 elwt,
4185 current_viewport,
4186 &mut pending_resize,
4187 &mut resize_needs_settled_frame,
4188 &mut pending_capture_settle,
4189 pending_screenshot_path.as_deref(),
4190 &mut live_resize,
4191 &mut invalidations,
4192 &mut last_redraw_at,
4193 resize_frame,
4194 &mut redraw_pending,
4195 &mut frame_trace,
4196 "test_resize",
4197 );
4198 }
4199 }
4200 TestEvent::TapText { text, response_tx } => {
4201 let Some(window) = platform_window.active_window() else {
4202 let _ = response_tx.send(fission_test_driver::TestResponse::Error {
4203 message: "window not ready".into(),
4204 });
4205 return;
4206 };
4207 let resp = handle_tap_text(&text, &mut runtime, &pipeline);
4208 if matches!(resp, fission_test_driver::TestResponse::Ok { .. }) {
4209 invalidations.mark_build();
4210 if process_pending_effects(
4211 &mut runtime,
4212 &effect_result_tx,
4213 &event_proxy,
4214 &async_registry,
4215 &mut active_services,
4216 &mut service_bindings,
4217 &mut next_service_instance_id,
4218 ) {
4219 invalidations.mark_build();
4220 }
4221 }
4222 let _ = response_tx.send(resp);
4223 request_redraw_logged(
4224 window,
4225 elwt,
4226 &mut last_redraw_at,
4227 min_frame,
4228 &mut redraw_pending,
4229 &mut frame_trace,
4230 "test_tap_text",
4231 );
4232 }
4233 TestEvent::Screenshot { path, response_tx } => {
4234 let Some(window) = platform_window.active_window() else {
4235 let _ = response_tx.send(fission_test_driver::TestResponse::Error {
4236 message: "window not ready".into(),
4237 });
4238 return;
4239 };
4240 pending_screenshot_path = Some(path);
4241 pending_screenshot_response_tx = Some(response_tx);
4242 pending_capture_settle = resize_is_unsettled(
4243 pending_resize.is_some(),
4244 resize_needs_settled_frame,
4245 live_resize.is_live(Instant::now()),
4246 );
4247 window.request_redraw();
4248 }
4249 TestEvent::CaptureScreenshot { response_tx } => {
4250 let Some(window) = platform_window.active_window() else {
4251 let _ = response_tx.send(fission_test_driver::TestResponse::Error {
4252 message: "window not ready".into(),
4253 });
4254 return;
4255 };
4256 pending_screenshot_path = Some("__capture__".into());
4257 pending_screenshot_response_tx = Some(response_tx);
4258 pending_capture_settle = resize_is_unsettled(
4259 pending_resize.is_some(),
4260 resize_needs_settled_frame,
4261 live_resize.is_live(Instant::now()),
4262 );
4263 window.request_redraw();
4264 }
4265 TestEvent::GetText { response_tx } => {
4266 let resp =
4267 build_get_text_response(&pipeline, &runtime.runtime_state.scroll);
4268 let _ = response_tx.send(resp);
4269 }
4270 TestEvent::GetTree { response_tx } => {
4271 let resp =
4272 build_get_tree_response(&pipeline, &runtime.runtime_state.scroll);
4273 let _ = response_tx.send(resp);
4274 }
4275 TestEvent::Pump { response_tx } => {
4276 let Some(window) = platform_window.active_window() else {
4277 let _ = response_tx.send(fission_test_driver::TestResponse::Error {
4278 message: "window not ready".into(),
4279 });
4280 return;
4281 };
4282 pending_screenshot_path = Some("__pump__".into());
4283 pending_screenshot_response_tx = Some(response_tx);
4284 pending_capture_settle = resize_is_unsettled(
4285 pending_resize.is_some(),
4286 resize_needs_settled_frame,
4287 live_resize.is_live(Instant::now()),
4288 );
4289 window.request_redraw();
4290 }
4291 TestEvent::Wake => {
4292 if let Some(window) = platform_window.active_window() {
4293 if accessibility_bridge.drain_events(
4294 &mut runtime,
4295 pipeline.prev_ir.as_ref(),
4296 pipeline.last_snapshot.as_ref(),
4297 ) {
4298 invalidations.mark_build();
4299 if process_pending_effects(
4300 &mut runtime,
4301 &effect_result_tx,
4302 &event_proxy,
4303 &async_registry,
4304 &mut active_services,
4305 &mut service_bindings,
4306 &mut next_service_instance_id,
4307 ) {
4308 invalidations.mark_build();
4309 }
4310 }
4311 request_redraw_logged(
4312 window,
4313 elwt,
4314 &mut last_redraw_at,
4315 min_frame,
4316 &mut redraw_pending,
4317 &mut frame_trace,
4318 "wake",
4319 );
4320 }
4321 }
4322 TestEvent::Wait { ms: _, response_tx } => {
4323 let _ = response_tx.send(fission_test_driver::TestResponse::Ok {});
4324 }
4325 TestEvent::Quit => {
4326 elwt.exit();
4327 }
4328 }
4329 };
4330
4331 #[cfg(target_os = "android")]
4332 let mut drain_pending_test_events = || loop {
4333 let pending = {
4334 let mut pending = pending_test_events
4335 .lock()
4336 .expect("pending test events lock poisoned");
4337 pending.pop_front()
4338 };
4339 let Some(test_event) = pending else {
4340 break;
4341 };
4342 if debug_android_events {
4343 eprintln!("[android-debug] draining_test_queue");
4344 }
4345 handle_test_event(test_event);
4346 };
4347
4348 match event {
4349 #[cfg(feature = "tray")]
4350 Event::NewEvents(StartCause::Init) => {
4351 if active_tray.is_none() {
4352 if let Some(config) = tray_config.clone() {
4353 match tray::ActiveTray::build(config) {
4354 Ok(tray) => {
4355 active_tray = Some(tray);
4356 }
4357 Err(error) => {
4358 eprintln!("Fission tray setup error: {error:?}");
4359 }
4360 }
4361 }
4362 }
4363 }
4364 Event::Resumed => {
4365 if debug_android_events {
4366 eprintln!("[android-events] resumed");
4367 }
4368 #[cfg(target_os = "android")]
4369 if platform_window.is_none() {
4370 match build_window(
4371 &window_title,
4372 background_test_mode,
4373 elwt,
4374 web_mount_selector.as_deref(),
4375 ) {
4376 Ok(new_window) => {
4377 ime_handler.set_window(Some(new_window.clone()));
4378 sync_window_cursor(&new_window, &runtime);
4379 platform_window = Some(new_window);
4380 }
4381 Err(err) => {
4382 eprintln!("window build error: {err}");
4383 elwt.exit();
4384 return;
4385 }
4386 }
4387 }
4388 let Some(window) = platform_window.active_window() else {
4389 return;
4390 };
4391 accessibility_bridge.ensure_adapter(elwt, window);
4392 if accessibility::window_must_start_hidden() && !background_test_mode {
4393 window.set_visible(true);
4394 }
4395 let current_viewport = WindowViewportState::from_window(window);
4396 #[cfg(not(target_os = "android"))]
4397 {
4398 window_viewport = current_viewport;
4399 }
4400 #[cfg(target_os = "android")]
4401 {
4402 window_viewport = Some(current_viewport);
4403 }
4404 #[cfg(not(target_arch = "wasm32"))]
4405 if render_state.is_none()
4406 && current_viewport.physical_size.width > 0
4407 && current_viewport.physical_size.height > 0
4408 {
4409 if let Some(render_window) = platform_window.active_window_arc() {
4410 match create_render_state(
4411 &mut render_cx,
4412 render_window,
4413 current_viewport,
4414 ) {
4415 Ok(mut state) => {
4416 if let Err(err) = present_startup_clear_frame(
4417 &mut state,
4418 &render_cx,
4419 theme_background_wgpu_color(&env),
4420 ) {
4421 eprintln!("startup clear frame failed: {err}");
4422 }
4423 render_state = Some(state);
4424 }
4425 Err(err) => {
4426 eprintln!("render surface not ready on resume: {err}");
4427 }
4428 }
4429 }
4430 }
4431 pending_resize = Some(current_viewport);
4432 resize_needs_settled_frame = true;
4433 if pending_screenshot_path.is_some() {
4434 pending_capture_settle = true;
4435 }
4436 invalidations.mark_composite();
4437 request_redraw_logged(
4438 window,
4439 elwt,
4440 &mut last_redraw_at,
4441 min_frame,
4442 &mut redraw_pending,
4443 &mut frame_trace,
4444 "app_resumed",
4445 );
4446 }
4447 Event::Suspended => {
4448 #[cfg(not(target_arch = "wasm32"))]
4449 {
4450 render_state = None;
4451 }
4452 #[cfg(target_arch = "wasm32")]
4453 {
4454 web_renderer = None;
4455 webgpu_init_in_flight = false;
4456 *pending_webgpu_init.borrow_mut() = None;
4457 web_renderer_reported = false;
4458 }
4459 #[cfg(target_os = "android")]
4460 {
4461 ime_handler.set_window(None);
4462 platform_window = None;
4463 window_viewport = None;
4464 pending_resize = None;
4465 resize_needs_settled_frame = false;
4466 pending_capture_settle = false;
4467 last_built_viewport = None;
4468 last_cursor_position = None;
4469 active_primary_touch = None;
4470 touch_positions.clear();
4471 }
4472 }
4473 Event::UserEvent(test_event) => {
4477 #[cfg(target_os = "android")]
4478 if matches!(test_event, TestEvent::Wake) {
4479 if debug_android_events {
4480 eprintln!("[android-debug] wake_received");
4481 }
4482 drain_pending_test_events();
4483 return;
4484 }
4485 handle_test_event(test_event)
4486 }
4487
4488 Event::AboutToWait => {
4492 let Some(window) = platform_window.active_window() else {
4493 elwt.set_control_flow(ControlFlow::Wait);
4494 return;
4495 };
4496 #[cfg(target_os = "android")]
4497 drain_pending_test_events();
4498 #[cfg(feature = "tray")]
4499 if let (Some(rx), Some(active)) = (tray_event_rx.as_ref(), active_tray.as_ref())
4500 {
4501 while let Ok(event) = rx.try_recv() {
4502 match active.handle_event(event, window, &mut runtime) {
4503 Ok(outcome) => {
4504 if outcome.quit {
4505 elwt.exit();
4506 return;
4507 }
4508 if outcome.redraw {
4509 invalidations.mark_build();
4510 if process_pending_effects(
4511 &mut runtime,
4512 &effect_result_tx,
4513 &event_proxy,
4514 &async_registry,
4515 &mut active_services,
4516 &mut service_bindings,
4517 &mut next_service_instance_id,
4518 ) {
4519 invalidations.mark_build();
4520 }
4521 request_redraw_logged(
4522 window,
4523 elwt,
4524 &mut last_redraw_at,
4525 min_frame,
4526 &mut redraw_pending,
4527 &mut frame_trace,
4528 "tray_menu_action",
4529 );
4530 }
4531 }
4532 Err(error) => {
4533 eprintln!("Fission tray event error: {error:?}");
4534 }
4535 }
4536 }
4537 }
4538 let now = Instant::now();
4539
4540 let mut surfaces = pipeline.video_surfaces.clone();
4542 let mut active_nodes = std::collections::HashSet::new();
4543
4544 for surface in &mut surfaces {
4545 active_nodes.insert(surface.widget_id);
4546
4547 if !players.contains_key(&surface.widget_id) {
4549 if let Some(state) =
4550 runtime.runtime_state.video.states.get(&surface.widget_id)
4551 {
4552 let source = &state.asset_source;
4553 if !source.is_empty() {
4554 let player = video_backend.create_player(source);
4555 surface.surface_id = player.surface_id();
4556 if let Some(state) = runtime
4557 .runtime_state
4558 .video
4559 .states
4560 .get_mut(&surface.widget_id)
4561 {
4562 state.surface_id = Some(surface.surface_id);
4563 }
4564 players.insert(
4565 surface.widget_id,
4566 ActivePlayer {
4567 player,
4568 last_status: None,
4569 last_rate: None,
4570 last_volume: None,
4571 last_muted: None,
4572 },
4573 );
4574 }
4575 }
4576 } else if let Some(active_player) = players.get(&surface.widget_id) {
4577 surface.surface_id = active_player.player.surface_id();
4578 }
4579 }
4580
4581 players.retain(|id, _| active_nodes.contains(id));
4583
4584 video_backend.present_surfaces(&surfaces);
4586 let web_surfaces = pipeline.web_surfaces.clone();
4587 web_backend.present_surfaces(&web_surfaces);
4588
4589 for (widget_id, active_player) in players.iter_mut() {
4591 if let Some(video_state) =
4592 runtime.runtime_state.video.states.get_mut(widget_id)
4593 {
4594 let player = &mut active_player.player;
4595
4596 if active_player.last_status != Some(video_state.status) {
4598 match video_state.status {
4599 VideoStatus::Playing => player.play(),
4600 VideoStatus::Paused => player.pause(),
4601 VideoStatus::Stopped => player.stop(),
4602 _ => {}
4603 }
4604 active_player.last_status = Some(video_state.status);
4605 }
4606
4607 for event in player.poll_events() {
4609 match event {
4610 VideoEvent::Ready { duration } => {
4611 video_state.duration_ms = Some(duration);
4612 if video_state.status == VideoStatus::Playing {
4613 player.play();
4614 }
4615 }
4616 VideoEvent::Ended => {
4617 video_state.status = VideoStatus::Ended;
4618 active_player.last_status = Some(VideoStatus::Ended);
4619 request_redraw_logged(
4620 &window,
4621 elwt,
4622 &mut last_redraw_at,
4623 min_frame,
4624 &mut redraw_pending,
4625 &mut frame_trace,
4626 "video_ended",
4627 );
4628 }
4629 VideoEvent::Error(e) => {
4630 eprintln!(
4631 "Video playback error for {:?}: {:?}",
4632 widget_id, e
4633 );
4634 video_state.status = VideoStatus::Error;
4635 active_player.last_status = Some(VideoStatus::Error);
4636 request_redraw_logged(
4637 &window,
4638 elwt,
4639 &mut last_redraw_at,
4640 min_frame,
4641 &mut redraw_pending,
4642 &mut frame_trace,
4643 "video_error",
4644 );
4645 }
4646 }
4647 }
4648 video_state.position_ms = player.position();
4650
4651 if active_player.last_rate != Some(video_state.rate) {
4652 player.set_rate(video_state.rate);
4653 active_player.last_rate = Some(video_state.rate);
4654 }
4655 if active_player.last_volume != Some(video_state.volume) {
4656 player.set_volume(video_state.volume);
4657 active_player.last_volume = Some(video_state.volume);
4658 }
4659 if active_player.last_muted != Some(video_state.muted) {
4660 player.set_muted(video_state.muted);
4661 active_player.last_muted = Some(video_state.muted);
4662 }
4663
4664 if let Some(seek_pos) = video_state.pending_seek.take() {
4665 player.seek_to(seek_pos);
4666 }
4667 }
4668 }
4669
4670 let has_finite_animation = runtime
4671 .runtime_state
4672 .motion
4673 .active
4674 .values()
4675 .any(|anim| !anim.repeat);
4676 let resize_unsettled = resize_is_unsettled(
4677 pending_resize.is_some(),
4678 resize_needs_settled_frame,
4679 live_resize.is_live(now),
4680 );
4681 let repeat_animation_interval = if resize_unsettled || pending_capture_settle {
4682 None
4683 } else {
4684 repeating_animation_redraw_interval(
4685 &runtime.runtime_state.motion,
4686 repeat_animation_frame,
4687 )
4688 };
4689 let has_playing_video = players.iter().any(|(widget_id, _)| {
4690 runtime
4691 .runtime_state
4692 .video
4693 .states
4694 .get(widget_id)
4695 .map(|state| state.status == VideoStatus::Playing)
4696 .unwrap_or(false)
4697 });
4698 let animation_frame = animation_redraw_interval(
4699 has_finite_animation,
4700 repeat_animation_interval,
4701 has_playing_video,
4702 min_frame,
4703 );
4704
4705 ime_handler.set_text_input_config(focused_text_input_config(
4706 &runtime,
4707 pipeline.prev_ir.as_ref(),
4708 ));
4709 let focused_text_input =
4710 focused_text_input_id(&runtime, pipeline.prev_ir.as_ref());
4711 if focused_text_input != blink_focus_id {
4712 if let Some(prev) = blink_focus_id {
4713 runtime.runtime_state.caret_visible.remove(&prev);
4714 }
4715 blink_focus_id = focused_text_input;
4716 if let Some(id) = blink_focus_id {
4717 runtime.runtime_state.caret_visible.insert(id, true);
4718 last_blink_toggle = now;
4719 invalidations.mark_build();
4720 request_redraw_logged(
4721 &window,
4722 elwt,
4723 &mut last_redraw_at,
4724 min_frame,
4725 &mut redraw_pending,
4726 &mut frame_trace,
4727 "caret_focus_changed",
4728 );
4729 }
4730 }
4731
4732 if blink_enabled {
4734 if let Some(id) = blink_focus_id {
4735 if now.duration_since(last_blink_toggle) >= blink_period {
4736 let visible = runtime
4737 .runtime_state
4738 .caret_visible
4739 .get(&id)
4740 .copied()
4741 .unwrap_or(true);
4742 runtime.runtime_state.caret_visible.insert(id, !visible);
4743 last_blink_toggle = now;
4744 invalidations.mark_build();
4745 request_redraw_logged(
4746 &window,
4747 elwt,
4748 &mut last_redraw_at,
4749 min_frame,
4750 &mut redraw_pending,
4751 &mut frame_trace,
4752 "caret_blink",
4753 );
4754 }
4755 }
4756 }
4757
4758 let blink_wake_at = if blink_enabled && blink_focus_id.is_some() {
4759 Some(last_blink_toggle + blink_period)
4760 } else {
4761 None
4762 };
4763
4764 let effect_results_dispatched = drain_effect_results(
4767 &mut runtime,
4768 &effect_result_rx,
4769 &mut active_services,
4770 &mut service_bindings,
4771 );
4772 if effect_results_dispatched {
4773 invalidations.mark_build();
4774 if process_pending_effects(
4777 &mut runtime,
4778 &effect_result_tx,
4779 &event_proxy,
4780 &async_registry,
4781 &mut active_services,
4782 &mut service_bindings,
4783 &mut next_service_instance_id,
4784 ) {
4785 invalidations.mark_build();
4786 request_redraw_logged(
4787 &window,
4788 elwt,
4789 &mut last_redraw_at,
4790 min_frame,
4791 &mut redraw_pending,
4792 &mut frame_trace,
4793 "effect_continuation",
4794 );
4795 }
4796 request_redraw_logged(
4797 &window,
4798 elwt,
4799 &mut last_redraw_at,
4800 min_frame,
4801 &mut redraw_pending,
4802 &mut frame_trace,
4803 "effect_result",
4804 );
4805 }
4806
4807 let frame_hook_wants_redraw = if let Some(ref hook) = self.frame_hook {
4809 let hook = hook.clone();
4810 if let Some(state) = runtime.get_global_state_mut::<S>() {
4811 hook(state)
4812 } else {
4813 false
4814 }
4815 } else {
4816 false
4817 };
4818 if frame_hook_wants_redraw {
4819 invalidations.mark_build();
4820 request_redraw_logged(
4821 &window,
4822 elwt,
4823 &mut last_redraw_at,
4824 min_frame,
4825 &mut redraw_pending,
4826 &mut frame_trace,
4827 "frame_hook",
4828 );
4829 }
4830
4831 let next_vello_image_generation =
4832 fission_render_vello::image_cache_generation();
4833 let next_software_image_generation =
4834 software_renderer::image_cache_generation();
4835 let image_cache_changed = next_vello_image_generation
4836 != vello_image_cache_generation
4837 || next_software_image_generation != software_image_cache_generation;
4838 if image_cache_changed {
4839 vello_image_cache_generation = next_vello_image_generation;
4840 software_image_cache_generation = next_software_image_generation;
4841 #[cfg(not(target_arch = "wasm32"))]
4842 retained_scene_cache.clear();
4843 #[cfg(target_arch = "wasm32")]
4844 if let Some(WebRenderer::WebGpu(presenter)) = web_renderer.as_mut() {
4845 presenter.retained_scene_cache.clear();
4846 }
4847 invalidations.mark_paint();
4848 let stats = fission_render_vello::image_cache_stats();
4849 diag::emit(
4850 diag::DiagCategory::Raster,
4851 diag::DiagLevel::Debug,
4852 diag::DiagEventKind::ImageCacheSummary {
4853 renderer: "vello".to_string(),
4854 entries: stats.entries,
4855 weighted_bytes: stats.weighted_bytes,
4856 max_bytes: stats.max_bytes,
4857 pending: stats.pending,
4858 hits: stats.hits,
4859 misses: stats.misses,
4860 loads_started: stats.loads_started,
4861 loads_completed: stats.loads_completed,
4862 loads_failed: stats.loads_failed,
4863 evictions: stats.evictions,
4864 offscreen_skips: stats.offscreen_skips,
4865 },
4866 );
4867 let stats = software_renderer::image_cache_stats();
4868 diag::emit(
4869 diag::DiagCategory::Raster,
4870 diag::DiagLevel::Debug,
4871 diag::DiagEventKind::ImageCacheSummary {
4872 renderer: "software".to_string(),
4873 entries: stats.entries,
4874 weighted_bytes: stats.weighted_bytes,
4875 max_bytes: stats.max_bytes,
4876 pending: stats.pending,
4877 hits: stats.hits,
4878 misses: stats.misses,
4879 loads_started: stats.loads_started,
4880 loads_completed: stats.loads_completed,
4881 loads_failed: stats.loads_failed,
4882 evictions: stats.evictions,
4883 offscreen_skips: 0,
4884 },
4885 );
4886 request_redraw_logged(
4887 &window,
4888 elwt,
4889 &mut last_redraw_at,
4890 min_frame,
4891 &mut redraw_pending,
4892 &mut frame_trace,
4893 "image_cache",
4894 );
4895 }
4896 let image_cache_pending = fission_render_vello::image_cache_has_pending()
4897 || software_renderer::image_cache_has_pending();
4898
4899 let frame_hook_wake_at = if self.frame_hook.is_some() {
4904 Some(now + Duration::from_secs(2))
4905 } else {
4906 None
4907 };
4908
4909 let has_pending_work = effect_results_dispatched
4910 || frame_hook_wants_redraw
4911 || image_cache_changed
4912 || invalidations.any()
4913 || resize_unsettled
4914 || pending_capture_settle;
4915 let active_keys = active_animation_keys(&runtime);
4916
4917 if has_pending_work {
4918 let pending_frame = pending_work_redraw_interval(
4919 invalidations,
4920 resize_unsettled || pending_capture_settle,
4921 min_frame,
4922 resize_frame,
4923 );
4924 let redraw_reason = if resize_unsettled {
4925 "pending_resize"
4926 } else if pending_capture_settle {
4927 "pending_capture_settle"
4928 } else if invalidations.build {
4929 "pending_work:build"
4930 } else if invalidations.layout {
4931 "pending_work:layout"
4932 } else if invalidations.paint {
4933 "pending_work:paint"
4934 } else if invalidations.composite {
4935 "pending_work:composite"
4936 } else if effect_results_dispatched {
4937 "pending_work:effects"
4938 } else if frame_hook_wants_redraw {
4939 "pending_work:frame_hook"
4940 } else {
4941 "pending_work"
4942 };
4943 request_redraw_logged(
4944 &window,
4945 elwt,
4946 &mut last_redraw_at,
4947 pending_frame,
4948 &mut redraw_pending,
4949 &mut frame_trace,
4950 redraw_reason,
4951 );
4952 let reasons = frame_trace.take_redraw_reasons();
4953 frame_trace.emit(
4954 "about_to_wait",
4955 presented_frames + 1,
4956 &active_keys,
4957 invalidations,
4958 &reasons,
4959 &format!(
4960 "schedule=pending interval_ms={} pending_resize={} redraw_pending={} highest={}",
4961 pending_frame.as_millis(),
4962 resize_unsettled || pending_capture_settle,
4963 redraw_pending,
4964 invalidations.highest_class(),
4965 ),
4966 );
4967 let mut wake_at = last_redraw_at + pending_frame;
4968 if let Some(blink_at) = blink_wake_at {
4969 if blink_at < wake_at {
4970 wake_at = blink_at;
4971 }
4972 }
4973 if let Some(hook_at) = frame_hook_wake_at {
4974 if hook_at < wake_at {
4975 wake_at = hook_at;
4976 }
4977 }
4978 elwt.set_control_flow(ControlFlow::WaitUntil(wake_at));
4979 } else if let Some(animation_frame) = animation_frame {
4980 request_redraw_logged(
4981 &window,
4982 elwt,
4983 &mut last_redraw_at,
4984 animation_frame,
4985 &mut redraw_pending,
4986 &mut frame_trace,
4987 if has_finite_animation {
4988 "animation:finite"
4989 } else if has_playing_video {
4990 "animation:video"
4991 } else {
4992 "animation:repeat"
4993 },
4994 );
4995 let reasons = frame_trace.take_redraw_reasons();
4996 frame_trace.emit(
4997 "about_to_wait",
4998 presented_frames + 1,
4999 &active_keys,
5000 invalidations,
5001 &reasons,
5002 &format!(
5003 "schedule=animation interval_ms={} pending_resize={} redraw_pending={} highest={}",
5004 animation_frame.as_millis(),
5005 resize_unsettled || pending_capture_settle,
5006 redraw_pending,
5007 invalidations.highest_class(),
5008 ),
5009 );
5010 let mut wake_at = last_redraw_at + animation_frame;
5011 if let Some(blink_at) = blink_wake_at {
5012 if blink_at < wake_at {
5013 wake_at = blink_at;
5014 }
5015 }
5016 if let Some(hook_at) = frame_hook_wake_at {
5017 if hook_at < wake_at {
5018 wake_at = hook_at;
5019 }
5020 }
5021 elwt.set_control_flow(ControlFlow::WaitUntil(wake_at));
5022 } else if image_cache_pending {
5023 let wake_at = now + Duration::from_millis(50);
5024 elwt.set_control_flow(ControlFlow::WaitUntil(wake_at));
5025 } else if let Some(blink_at) = blink_wake_at {
5026 let reasons = frame_trace.take_redraw_reasons();
5027 frame_trace.emit(
5028 "about_to_wait",
5029 presented_frames + 1,
5030 &active_keys,
5031 invalidations,
5032 &reasons,
5033 "schedule=blink_wait pending_resize=false redraw_pending=false highest=none",
5034 );
5035 let mut wake_at = blink_at;
5036 if let Some(hook_at) = frame_hook_wake_at {
5037 if hook_at < wake_at {
5038 wake_at = hook_at;
5039 }
5040 }
5041 elwt.set_control_flow(ControlFlow::WaitUntil(wake_at));
5042 } else if let Some(hook_at) = frame_hook_wake_at {
5043 let reasons = frame_trace.take_redraw_reasons();
5044 frame_trace.emit(
5045 "about_to_wait",
5046 presented_frames + 1,
5047 &active_keys,
5048 invalidations,
5049 &reasons,
5050 "schedule=hook_wait pending_resize=false redraw_pending=false highest=none",
5051 );
5052 elwt.set_control_flow(ControlFlow::WaitUntil(hook_at));
5053 } else {
5054 let reasons = frame_trace.take_redraw_reasons();
5055 frame_trace.emit(
5056 "about_to_wait",
5057 presented_frames + 1,
5058 &active_keys,
5059 invalidations,
5060 &reasons,
5061 "schedule=idle pending_resize=false redraw_pending=false highest=none",
5062 );
5063 #[cfg(target_os = "android")]
5064 if test_control_enabled {
5065 elwt.set_control_flow(ControlFlow::Poll);
5066 } else {
5067 elwt.set_control_flow(ControlFlow::WaitUntil(
5068 now + Duration::from_millis(16),
5069 ));
5070 }
5071 #[cfg(not(target_os = "android"))]
5072 elwt.set_control_flow(ControlFlow::Wait);
5073 }
5074 }
5075
5076 Event::WindowEvent { window_id, event }
5080 if platform_window.active_window_id() == Some(window_id) =>
5081 {
5082 let Some(window) = platform_window.active_window() else {
5083 return;
5084 };
5085 accessibility_bridge.process_window_event(window, &event);
5086 match event {
5087 WindowEvent::Resized(size) => {
5088 if size.width > 0 && size.height > 0 {
5089 #[cfg(target_os = "ios")]
5090 let next_viewport = WindowViewportState::from_window(window);
5091 #[cfg(not(target_os = "ios"))]
5092 let next_viewport = pending_resize
5093 .unwrap_or_else(|| WindowViewportState::from_window(window))
5094 .with_physical_size(size);
5095 #[cfg(not(target_os = "android"))]
5096 {
5097 window_viewport = next_viewport;
5098 }
5099 #[cfg(target_os = "android")]
5100 {
5101 window_viewport = Some(next_viewport);
5102 }
5103 apply_authoritative_resize(
5104 &window,
5105 elwt,
5106 next_viewport,
5107 &mut pending_resize,
5108 &mut resize_needs_settled_frame,
5109 &mut pending_capture_settle,
5110 pending_screenshot_path.as_deref(),
5111 &mut live_resize,
5112 &mut invalidations,
5113 &mut last_redraw_at,
5114 resize_frame,
5115 &mut redraw_pending,
5116 &mut frame_trace,
5117 "window_resized",
5118 );
5119 }
5120 }
5121 WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
5122 #[cfg(target_os = "ios")]
5123 let _ = scale_factor;
5124 #[cfg(target_os = "ios")]
5125 let next_viewport = WindowViewportState::from_window(window);
5126 #[cfg(not(target_os = "ios"))]
5127 let next_viewport = pending_resize
5128 .unwrap_or_else(|| WindowViewportState::from_window(window))
5129 .with_scale_factor(scale_factor);
5130 #[cfg(not(target_os = "android"))]
5131 {
5132 window_viewport = next_viewport;
5133 }
5134 #[cfg(target_os = "android")]
5135 {
5136 window_viewport = Some(next_viewport);
5137 }
5138 apply_authoritative_resize(
5139 &window,
5140 elwt,
5141 next_viewport,
5142 &mut pending_resize,
5143 &mut resize_needs_settled_frame,
5144 &mut pending_capture_settle,
5145 pending_screenshot_path.as_deref(),
5146 &mut live_resize,
5147 &mut invalidations,
5148 &mut last_redraw_at,
5149 resize_frame,
5150 &mut redraw_pending,
5151 &mut frame_trace,
5152 "scale_factor_changed",
5153 );
5154 }
5155 WindowEvent::RedrawRequested => {
5156 if debug_android_events {
5157 eprintln!("[android-events] redraw_requested");
5158 }
5159 redraw_pending = false;
5160 diag::begin_frame(None);
5161 let now = Instant::now();
5162 let dt = now.duration_since(last_frame_time);
5163 last_frame_time = now;
5164 let dt_ms = dt.as_millis() as u64;
5165 let pre_tick_active = active_animation_keys(&runtime);
5166 match runtime.tick(dt_ms) {
5167 Ok(tick_result) => {
5168 let tick_invalidations = pipeline
5169 .classify_animation_updates(&tick_result.changed_motions);
5170 invalidations.merge(tick_invalidations);
5171 let reasons = if tick_result.changed_motions.is_empty() {
5172 Vec::new()
5173 } else {
5174 tick_result
5175 .changed_motions
5176 .iter()
5177 .map(|(target, property)| {
5178 format!(
5179 "tick:{}:{:?}:{}",
5180 target.as_u128(),
5181 property,
5182 tick_invalidations.highest_class()
5183 )
5184 })
5185 .collect::<Vec<_>>()
5186 };
5187 frame_trace.emit(
5188 "redraw_requested",
5189 presented_frames + 1,
5190 &pre_tick_active,
5191 tick_invalidations,
5192 &reasons,
5193 &format!("dt_ms={}", dt_ms),
5194 );
5195 }
5196 Err(e) => {
5197 eprintln!("Runtime tick error: {:?}", e);
5198 }
5199 }
5200 if process_pending_effects(
5201 &mut runtime,
5202 &effect_result_tx,
5203 &event_proxy,
5204 &async_registry,
5205 &mut active_services,
5206 &mut service_bindings,
5207 &mut next_service_instance_id,
5208 ) {
5209 invalidations.mark_build();
5210 request_redraw_logged(
5211 &window,
5212 elwt,
5213 &mut last_redraw_at,
5214 min_frame,
5215 &mut redraw_pending,
5216 &mut frame_trace,
5217 "redraw:effects",
5218 );
5219 }
5220 let viewport_state = pending_resize.unwrap_or_else(|| {
5221 #[cfg(not(target_os = "android"))]
5222 {
5223 window_viewport
5224 }
5225 #[cfg(target_os = "android")]
5226 {
5227 window_viewport
5228 .unwrap_or_else(|| WindowViewportState::from_window(window))
5229 }
5230 });
5231 #[cfg(not(target_os = "android"))]
5232 {
5233 window_viewport = viewport_state;
5234 }
5235 #[cfg(target_os = "android")]
5236 {
5237 window_viewport = Some(viewport_state);
5238 }
5239 let swapchain_size = viewport_state.physical_size;
5240 if swapchain_size.width == 0 || swapchain_size.height == 0 {
5241 diag::end_frame(diag::FrameStats::default());
5242 return;
5243 }
5244
5245 let scale_factor = viewport_state.scale_factor;
5246 let pending_layout_viewport = viewport_state.logical_size();
5247 let render_target_size = (swapchain_size.width, swapchain_size.height);
5248
5249 #[cfg(target_arch = "wasm32")]
5250 if web_renderer.is_none() {
5251 let request = web_renderer_request();
5252 if matches!(request, RendererRequest::Canvas2dSoftware) {
5253 match WebCanvasPresenter::new(window) {
5254 Ok(mut presenter) => {
5255 presenter.report = RendererReport::new(
5256 "canvas2d-software",
5257 request,
5258 None,
5259 None,
5260 Some("forced_by_renderer_request".to_string()),
5261 render_target_size.0,
5262 render_target_size.1,
5263 scale_factor,
5264 );
5265 web_renderer = Some(WebRenderer::Canvas2d(presenter));
5266 }
5267 Err(err) => {
5268 eprintln!("web canvas not ready yet: {err}");
5269 request_redraw_logged(
5270 &window,
5271 elwt,
5272 &mut last_redraw_at,
5273 min_frame,
5274 &mut redraw_pending,
5275 &mut frame_trace,
5276 "web_canvas_pending",
5277 );
5278 diag::end_frame(diag::FrameStats::default());
5279 return;
5280 }
5281 }
5282 } else if let Some(result) = pending_webgpu_init.borrow_mut().take()
5283 {
5284 match result {
5285 Ok(presenter) => {
5286 web_renderer = Some(WebRenderer::WebGpu(presenter));
5287 }
5288 Err(error) if request.is_explicit_gpu() => {
5289 eprintln!(
5290 "webgpu-vello renderer requested but initialization failed: {error}"
5291 );
5292 diag::end_frame(diag::FrameStats::default());
5293 panic!(
5294 "webgpu-vello renderer requested but initialization failed: {error}"
5295 );
5296 }
5297 Err(error) => match WebCanvasPresenter::new(window) {
5298 Ok(mut presenter) => {
5299 presenter.report = RendererReport::new(
5300 "canvas2d-software",
5301 request,
5302 None,
5303 None,
5304 Some(format!(
5305 "webgpu_vello_init_failed:{error}"
5306 )),
5307 render_target_size.0,
5308 render_target_size.1,
5309 scale_factor,
5310 );
5311 web_renderer =
5312 Some(WebRenderer::Canvas2d(presenter));
5313 }
5314 Err(err) => {
5315 eprintln!(
5316 "web renderer fallback failed; webgpu error: {error}; canvas error: {err}"
5317 );
5318 request_redraw_logged(
5319 &window,
5320 elwt,
5321 &mut last_redraw_at,
5322 min_frame,
5323 &mut redraw_pending,
5324 &mut frame_trace,
5325 "web_canvas_pending",
5326 );
5327 diag::end_frame(diag::FrameStats::default());
5328 return;
5329 }
5330 },
5331 }
5332 } else {
5333 if !webgpu_init_in_flight {
5334 match window.canvas() {
5335 Some(canvas) => {
5336 let pending = pending_webgpu_init.clone();
5337 let proxy = event_proxy.clone();
5338 let init_viewport = viewport_state;
5339 webgpu_init_in_flight = true;
5340 wasm_bindgen_futures::spawn_local(async move {
5341 let result = create_webgpu_presenter(
5342 canvas,
5343 init_viewport,
5344 request,
5345 )
5346 .await
5347 .map_err(|error| error.to_string());
5348 *pending.borrow_mut() = Some(result);
5349 let _ = proxy.send_event(TestEvent::Wake);
5350 });
5351 }
5352 None => {
5353 eprintln!("web canvas not ready yet");
5354 }
5355 }
5356 }
5357 request_redraw_logged(
5358 &window,
5359 elwt,
5360 &mut last_redraw_at,
5361 min_frame,
5362 &mut redraw_pending,
5363 &mut frame_trace,
5364 "webgpu_renderer_pending",
5365 );
5366 diag::end_frame(diag::FrameStats::default());
5367 return;
5368 }
5369
5370 if !web_renderer_reported {
5371 if let Some(renderer) = web_renderer.as_ref() {
5372 publish_web_renderer_report(renderer.report());
5373 web_renderer_reported = true;
5374 }
5375 }
5376 }
5377
5378 #[cfg(not(target_arch = "wasm32"))]
5379 {
5380 if render_state.is_none() {
5381 let Some(render_window) = platform_window.active_window_arc()
5382 else {
5383 diag::end_frame(diag::FrameStats::default());
5384 return;
5385 };
5386 match create_render_state(
5387 &mut render_cx,
5388 render_window,
5389 viewport_state,
5390 ) {
5391 Ok(state) => {
5392 render_state = Some(state);
5393 }
5394 Err(err) => {
5395 eprintln!("render surface not ready yet: {err}");
5396 request_redraw_logged(
5397 &window,
5398 elwt,
5399 &mut last_redraw_at,
5400 min_frame,
5401 &mut redraw_pending,
5402 &mut frame_trace,
5403 "render_surface_pending",
5404 );
5405 diag::end_frame(diag::FrameStats::default());
5406 return;
5407 }
5408 }
5409 }
5410 let render_state = render_state.as_mut().expect("render state");
5411
5412 let mut surface_target_replaced = false;
5413 if swapchain_size.width != render_state.surface.config.width
5414 || swapchain_size.height != render_state.surface.config.height
5415 {
5416 render_cx.resize_surface(
5417 &mut render_state.surface,
5418 swapchain_size.width,
5419 swapchain_size.height,
5420 );
5421 let device_handle =
5422 &render_cx.devices[render_state.surface.dev_id];
5423 render_state.surface.config.alpha_mode =
5424 wgpu::CompositeAlphaMode::PostMultiplied;
5425 render_state.surface.surface.configure(
5426 &device_handle.device,
5427 &render_state.surface.config,
5428 );
5429 sync_tracked_target_texture_size_to_surface(
5430 &mut render_state.target_texture_size,
5431 swapchain_size,
5432 );
5433 surface_target_replaced = true;
5434 }
5435 if surface_target_replaced
5436 || render_target_size != render_state.target_texture_size
5437 {
5438 recreate_target_texture(
5439 &mut render_state.surface,
5440 &render_cx,
5441 render_target_size.0,
5442 render_target_size.1,
5443 );
5444 #[cfg(feature = "three-d")]
5445 {
5446 let device_handle =
5447 &render_cx.devices[render_state.surface.dev_id];
5448 render_state.scene3d_renderer.resize(
5450 &device_handle.device,
5451 render_target_size.0,
5452 render_target_size.1,
5453 );
5454 }
5455 render_state.target_texture_size = render_target_size;
5456 }
5457 }
5458
5459 let resize_settled =
5460 resize_needs_settled_frame && !live_resize.is_live(now);
5461 let target_viewport = pending_layout_viewport;
5462 let build_viewport = resolve_build_viewport(
5463 last_built_viewport,
5464 target_viewport,
5465 pipeline.prev_ir.is_some(),
5466 &mut invalidations,
5467 );
5468 env.viewport_size = build_viewport;
5469 env.window_insets =
5470 window_safe_area_insets(window, viewport_state.scale_factor);
5471
5472 if let Some(sync) = &self.sync_env {
5473 let state = runtime.get_global_state::<S>().unwrap();
5474 sync(state, &mut env);
5475 }
5476 let desired_window_title = env.window.title.plain_text();
5477 if desired_window_title != applied_window_title {
5478 if let Some(window) = platform_window.active_window() {
5479 window.set_title(desired_window_title);
5480 }
5481 applied_window_title = desired_window_title.to_string();
5482 }
5483
5484 if invalidations.build || pipeline.prev_ir.is_none() {
5485 let (
5486 node_tree,
5487 registry,
5488 resources,
5489 motion_declarations,
5490 videos,
5491 web_views,
5492 portals,
5493 ) = {
5494 let state = runtime.get_global_state::<S>().unwrap();
5495 let view = View::new(
5496 state,
5497 &runtime.runtime_state,
5498 &env,
5499 pipeline.last_snapshot.as_ref(),
5500 );
5501 let mut ctx = BuildCtx::new();
5502 let node = fission_core::build::enter(&mut ctx, &view, || {
5503 root_widget.clone().into()
5504 });
5505 let resources = ctx.take_resources();
5506 let motion_declarations = ctx.take_motion_declarations();
5507 let videos = ctx.take_video_registrations();
5508 let web_views = ctx.take_web_registrations();
5509 let portals_with_ids = ctx.take_portals();
5510
5511 let portals = portals_with_ids
5512 .into_iter()
5513 .map(|(id, node)| wrap_portal_for_viewport(id, node, &env))
5514 .collect::<Vec<_>>();
5515
5516 diag::emit(
5517 diag::DiagCategory::Layout,
5518 diag::DiagLevel::Debug,
5519 diag::DiagEventKind::PortalsComposed {
5520 portal_count: portals.len() as u32,
5521 },
5522 );
5523 (
5524 node,
5525 ctx.registry,
5526 resources,
5527 motion_declarations,
5528 videos,
5529 web_views,
5530 portals,
5531 )
5532 };
5533
5534 #[cfg(feature = "tray")]
5535 let tray_registry = if let Some(tray) = active_tray.as_mut() {
5536 match tray.refresh_menu(&runtime, &env, &pipeline) {
5537 Ok(registry) => Some(registry),
5538 Err(err) => {
5539 eprintln!("Runtime tray menu rebuild error: {:?}", err);
5540 None
5541 }
5542 }
5543 } else {
5544 None
5545 };
5546
5547 runtime.clear_reducers();
5548 runtime.absorb_registry(registry);
5549 #[cfg(feature = "tray")]
5550 if let Some(registry) = tray_registry {
5551 runtime.absorb_registry(registry);
5552 }
5553 if let Err(err) = runtime.reconcile_resources(resources) {
5554 eprintln!("Runtime resource reconciliation error: {:?}", err);
5555 }
5556 if !startup_dispatched {
5557 if let Some(action) = startup_action.clone() {
5558 if let Err(err) =
5559 runtime.dispatch(action, WidgetId::from_u128(0))
5560 {
5561 eprintln!("Startup action error: {:?}", err);
5562 }
5563 }
5564 startup_dispatched = true;
5565 }
5566 runtime.sync_motion_declarations(
5567 &motion_declarations,
5568 pipeline.last_snapshot.as_ref(),
5569 );
5570 runtime.sync_video_nodes(&videos);
5571 runtime.sync_web_nodes(&web_views);
5572
5573 let final_root: fission_core::Widget = fission_core::ui::Overlay {
5574 id: None,
5575 content: node_tree,
5576 overlay: fission_core::ui::ZStack {
5577 children: portals,
5578 ..Default::default()
5579 }
5580 .into(),
5581 }
5582 .into();
5583
5584 let mut lower_cx = InternalLoweringCx::new(
5585 &env,
5586 &runtime.runtime_state,
5587 runtime.measurer.as_ref(),
5588 pipeline.last_snapshot.as_ref(),
5589 );
5590 let root_id = fission_core::internal::lower_widget(
5591 &final_root,
5592 &mut lower_cx,
5593 );
5594 lower_cx.ir.root = Some(root_id);
5595
5596 let pipeline_invalidations = pipeline.replace_ir(lower_cx.ir, &env);
5597 invalidations.merge(pipeline_invalidations);
5598 last_built_viewport = Some(build_viewport);
5599 }
5600
5601 let _layout_updates = match pipeline.ensure_layout(
5602 LayoutRect::new(
5603 0.0,
5604 0.0,
5605 target_viewport.width,
5606 target_viewport.height,
5607 ),
5608 &mut layout_engine,
5609 &runtime.runtime_state.scroll,
5610 ) {
5611 Ok(updates) => updates,
5612 Err(e) => {
5613 eprintln!("Layout error: {:?}", e);
5614 diag::end_frame(diag::FrameStats::default());
5615 return;
5616 }
5617 };
5618
5619 if let (Some(ir), Some(layout)) =
5620 (pipeline.prev_ir.as_ref(), pipeline.last_snapshot.as_ref())
5621 {
5622 if runtime.post_layout_hook(ir, layout) {
5623 invalidations.mark_layout();
5624 }
5625 }
5626 if let (Some(ir), Some(layout)) =
5627 (pipeline.prev_ir.as_ref(), pipeline.last_snapshot.as_ref())
5628 {
5629 accessibility_bridge.update_tree(
5630 ir,
5631 layout,
5632 &runtime,
5633 scale_factor,
5634 );
5635 }
5636
5637 match pipeline.prepare_current(
5638 target_viewport,
5639 target_viewport,
5640 false,
5641 &runtime.runtime_state.scroll,
5642 &runtime.runtime_state.motion,
5643 &runtime.runtime_state.video,
5644 &runtime.runtime_state.web,
5645 ) {
5646 Ok(_stats) => {
5647 #[cfg(target_arch = "wasm32")]
5648 {
5649 let Some(renderer) = web_renderer.as_mut() else {
5650 eprintln!("web renderer is unavailable");
5651 diag::end_frame(diag::FrameStats::default());
5652 return;
5653 };
5654 let active_renderer = renderer.active_name().to_string();
5655 match renderer {
5656 WebRenderer::Canvas2d(presenter) => {
5657 let retained_scene = pipeline
5658 .retained_scene()
5659 .expect(
5660 "retained render scene missing before render",
5661 );
5662 let rgba = SoftwareRenderer::render(
5663 retained_scene,
5664 render_target_size.0,
5665 render_target_size.1,
5666 fission_render::Color {
5667 r: env.theme.tokens.colors.background.r,
5668 g: env.theme.tokens.colors.background.g,
5669 b: env.theme.tokens.colors.background.b,
5670 a: env.theme.tokens.colors.background.a,
5671 },
5672 scale_factor as f32,
5673 )
5674 .expect("failed to rasterize software web frame");
5675
5676 if let Err(err) = presenter.present(
5677 &rgba,
5678 render_target_size.0,
5679 render_target_size.1,
5680 scale_factor,
5681 ) {
5682 eprintln!(
5683 "failed to present web canvas frame: {err}"
5684 );
5685 diag::end_frame(diag::FrameStats::default());
5686 return;
5687 }
5688 }
5689 WebRenderer::WebGpu(presenter) => {
5690 if swapchain_size.width
5691 != presenter.render_state.surface.config.width
5692 || swapchain_size.height
5693 != presenter
5694 .render_state
5695 .surface
5696 .config
5697 .height
5698 {
5699 presenter.render_cx.resize_surface(
5700 &mut presenter.render_state.surface,
5701 swapchain_size.width,
5702 swapchain_size.height,
5703 );
5704 let device_handle =
5705 &presenter.render_cx.devices
5706 [presenter.render_state.surface.dev_id];
5707 presenter
5708 .render_state
5709 .surface
5710 .config
5711 .alpha_mode =
5712 wgpu::CompositeAlphaMode::PostMultiplied;
5713 presenter
5714 .render_state
5715 .surface
5716 .surface
5717 .configure(
5718 &device_handle.device,
5719 &presenter.render_state.surface.config,
5720 );
5721 sync_tracked_target_texture_size_to_surface(
5722 &mut presenter
5723 .render_state
5724 .target_texture_size,
5725 swapchain_size,
5726 );
5727 }
5728 if render_target_size
5729 != presenter.render_state.target_texture_size
5730 {
5731 recreate_target_texture(
5732 &mut presenter.render_state.surface,
5733 &presenter.render_cx,
5734 render_target_size.0,
5735 render_target_size.1,
5736 );
5737 presenter.render_state.target_texture_size =
5738 render_target_size;
5739 }
5740
5741 let surface_texture = match presenter
5742 .render_state
5743 .surface
5744 .surface
5745 .get_current_texture()
5746 {
5747 Ok(texture) => texture,
5748 Err(err) => {
5749 eprintln!(
5750 "failed to get webgpu surface texture: {err}"
5751 );
5752 diag::end_frame(diag::FrameStats::default());
5753 return;
5754 }
5755 };
5756 let device_handle = &presenter.render_cx.devices
5757 [presenter.render_state.surface.dev_id];
5758
5759 let clear_color = vello::wgpu::Color {
5760 r: env.theme.tokens.colors.background.r as f64
5761 / 255.0,
5762 g: env.theme.tokens.colors.background.g as f64
5763 / 255.0,
5764 b: env.theme.tokens.colors.background.b as f64
5765 / 255.0,
5766 a: env.theme.tokens.colors.background.a as f64
5767 / 255.0,
5768 };
5769 match &mut presenter.render_state.main_renderer {
5770 MainRenderer::Vello {
5771 renderer,
5772 texture_compositor,
5773 } => {
5774 let texture_plans =
5775 pipeline.texture_compositor_plans();
5776 let texture_plans_fit_limits =
5777 texture_plans_fit_device_limits(
5778 texture_plans,
5779 scale_factor,
5780 device_handle
5781 .device
5782 .limits()
5783 .max_texture_dimension_2d,
5784 );
5785 let has_active_scroll_offsets = runtime
5786 .runtime_state
5787 .scroll
5788 .offsets
5789 .values()
5790 .any(|offset| offset.abs() > 0.5);
5791 let enable_texture_compositor =
5792 web_bool_global(
5793 "FISSION_ENABLE_TEXTURE_COMPOSITOR",
5794 );
5795 if !enable_texture_compositor
5796 || texture_plans.is_empty()
5797 || !texture_plans_fit_limits
5798 || has_active_scroll_offsets
5799 {
5800 let render_params =
5801 vello::RenderParams {
5802 base_color:
5803 vello::peniko::Color::from_rgba8(
5804 env.theme
5805 .tokens
5806 .colors
5807 .background
5808 .r,
5809 env.theme
5810 .tokens
5811 .colors
5812 .background
5813 .g,
5814 env.theme
5815 .tokens
5816 .colors
5817 .background
5818 .b,
5819 env.theme
5820 .tokens
5821 .colors
5822 .background
5823 .a,
5824 ),
5825 width: render_target_size.0,
5826 height: render_target_size.1,
5827 antialiasing_method:
5828 vello::AaConfig::Area,
5829 };
5830
5831 presenter.scene.reset();
5832 let retained_scene = pipeline
5833 .retained_scene()
5834 .expect(
5835 "retained render scene missing before render",
5836 );
5837 let workload_profile =
5838 workload_profile_for_scene(
5839 retained_scene,
5840 render_target_size.0,
5841 render_target_size.1,
5842 scale_factor,
5843 );
5844 let mut renderer_wrapper =
5845 VelloRenderer::new(
5846 &mut presenter.scene,
5847 measurer.clone(),
5848 &mut presenter
5849 .retained_scene_cache,
5850 scale_factor,
5851 );
5852 renderer_wrapper
5853 .render_scene(retained_scene)
5854 .expect(
5855 "failed to encode retained scene",
5856 );
5857 renderer
5858 .render_to_texture_with_workload_profile(
5859 &device_handle.device,
5860 &device_handle.queue,
5861 &presenter.scene,
5862 &presenter
5863 .render_state
5864 .surface
5865 .target_view,
5866 &render_params,
5867 Some(&workload_profile),
5868 )
5869 .expect(
5870 "failed to render webgpu frame",
5871 );
5872 } else {
5873 let force_full_compositor_redraw =
5874 invalidations.build
5875 || invalidations.layout
5876 || invalidations.paint;
5877 let _compositor_stats =
5878 texture_compositor
5879 .render_layers(
5880 &device_handle.device,
5881 &device_handle.queue,
5882 renderer,
5883 &mut presenter
5884 .retained_scene_cache,
5885 measurer.clone(),
5886 scale_factor,
5887 render_target_size.0,
5888 render_target_size.1,
5889 pipeline
5890 .texture_compositor_root_transform(),
5891 texture_plans,
5892 force_full_compositor_redraw,
5893 clear_color,
5894 &presenter
5895 .render_state
5896 .surface
5897 .target_view,
5898 )
5899 .expect(
5900 "failed to composite webgpu texture layers",
5901 );
5902 }
5903 }
5904 MainRenderer::Software => {}
5905 }
5906
5907 let surface_view =
5908 surface_texture.texture.create_view(
5909 &wgpu::TextureViewDescriptor::default(),
5910 );
5911 let mut encoder =
5912 device_handle.device.create_command_encoder(
5913 &wgpu::CommandEncoderDescriptor {
5914 label: Some("WebGPU Surface Blit"),
5915 },
5916 );
5917 presenter.render_state.surface.blitter.copy(
5918 &device_handle.device,
5919 &mut encoder,
5920 &presenter.render_state.surface.target_view,
5921 &surface_view,
5922 );
5923 device_handle.queue.submit(Some(encoder.finish()));
5924 surface_texture.present();
5925 }
5926 }
5927
5928 let capture_ready =
5929 !pending_capture_settle || resize_settled;
5930 if capture_ready {
5931 pending_capture_settle = false;
5932 let _ = pending_screenshot_path.take();
5933 let _ = pending_screenshot_response_tx.take();
5934 }
5935
5936 pending_resize = None;
5937 if resize_settled {
5938 resize_needs_settled_frame = false;
5939 }
5940 invalidations = InvalidationSet::default();
5941
5942 presented_frames = presented_frames.saturating_add(1);
5943 flush_text_traces(
5944 text_trace_enabled,
5945 &mut pending_text_traces,
5946 presented_frames,
5947 );
5948
5949 let total_ms = now.elapsed().as_secs_f64() * 1000.0;
5950 publish_web_frame_perf(&active_renderer, total_ms);
5951 if let Some(input_at) = pending_web_input_at.take() {
5952 publish_web_input_latency(
5953 &active_renderer,
5954 input_at.elapsed().as_secs_f64() * 1000.0,
5955 );
5956 }
5957
5958 diag::end_frame(diag::FrameStats::default());
5959 }
5960 #[cfg(not(target_arch = "wasm32"))]
5961 {
5962 let render_state =
5963 render_state.as_mut().expect("render state");
5964 let surface_texture = render_state
5965 .surface
5966 .surface
5967 .get_current_texture()
5968 .expect("failed to get texture");
5969 let device_handle =
5970 &render_cx.devices[render_state.surface.dev_id];
5971
5972 let clear_color = vello::wgpu::Color {
5973 r: env.theme.tokens.colors.background.r as f64 / 255.0,
5974 g: env.theme.tokens.colors.background.g as f64 / 255.0,
5975 b: env.theme.tokens.colors.background.b as f64 / 255.0,
5976 a: env.theme.tokens.colors.background.a as f64 / 255.0,
5977 };
5978 match &mut render_state.main_renderer {
5979 MainRenderer::Vello {
5980 renderer,
5981 texture_compositor,
5982 } => {
5983 let texture_plans =
5984 pipeline.texture_compositor_plans();
5985 let texture_plans_fit_limits =
5986 texture_plans_fit_device_limits(
5987 texture_plans,
5988 scale_factor,
5989 device_handle
5990 .device
5991 .limits()
5992 .max_texture_dimension_2d,
5993 );
5994 let has_active_scroll_offsets = runtime
5995 .runtime_state
5996 .scroll
5997 .offsets
5998 .values()
5999 .any(|offset| offset.abs() > 0.5);
6000 let enable_texture_compositor = std::env::var(
6001 "FISSION_ENABLE_TEXTURE_COMPOSITOR",
6002 )
6003 .ok()
6004 .as_deref()
6005 == Some("1");
6006 if !enable_texture_compositor
6007 || texture_plans.is_empty()
6008 || !texture_plans_fit_limits
6009 || has_active_scroll_offsets
6010 {
6011 let render_params = vello::RenderParams {
6012 base_color:
6013 vello::peniko::Color::from_rgba8(
6014 env.theme
6015 .tokens
6016 .colors
6017 .background
6018 .r,
6019 env.theme
6020 .tokens
6021 .colors
6022 .background
6023 .g,
6024 env.theme
6025 .tokens
6026 .colors
6027 .background
6028 .b,
6029 env.theme
6030 .tokens
6031 .colors
6032 .background
6033 .a,
6034 ),
6035 width: render_target_size.0,
6036 height: render_target_size.1,
6037 antialiasing_method: vello::AaConfig::Area,
6038 };
6039
6040 scene.reset();
6041 let retained_scene = pipeline
6042 .retained_scene()
6043 .expect(
6044 "retained render scene missing before render",
6045 );
6046 let workload_profile =
6047 workload_profile_for_scene(
6048 retained_scene,
6049 render_target_size.0,
6050 render_target_size.1,
6051 scale_factor,
6052 );
6053 let mut renderer_wrapper = VelloRenderer::new(
6054 &mut scene,
6055 measurer.clone(),
6056 &mut retained_scene_cache,
6057 scale_factor,
6058 );
6059 renderer_wrapper
6060 .render_scene(retained_scene)
6061 .expect("failed to encode retained scene");
6062 renderer
6063 .render_to_texture_with_workload_profile(
6064 &device_handle.device,
6065 &device_handle.queue,
6066 &scene,
6067 &render_state.surface.target_view,
6068 &render_params,
6069 Some(&workload_profile),
6070 )
6071 .expect("failed to render");
6072 } else {
6073 let force_full_compositor_redraw = invalidations
6074 .build
6075 || invalidations.layout
6076 || invalidations.paint;
6077 let _compositor_stats = texture_compositor
6078 .render_layers(
6079 &device_handle.device,
6080 &device_handle.queue,
6081 renderer,
6082 &mut retained_scene_cache,
6083 measurer.clone(),
6084 scale_factor,
6085 render_target_size.0,
6086 render_target_size.1,
6087 pipeline
6088 .texture_compositor_root_transform(
6089 ),
6090 texture_plans,
6091 force_full_compositor_redraw,
6092 clear_color,
6093 &render_state.surface.target_view,
6094 )
6095 .expect(
6096 "failed to composite texture layers",
6097 );
6098 }
6099 }
6100 MainRenderer::Software => {
6101 let retained_scene = pipeline
6102 .retained_scene()
6103 .expect(
6104 "retained render scene missing before render",
6105 );
6106 let rgba = SoftwareRenderer::render(
6107 retained_scene,
6108 render_target_size.0,
6109 render_target_size.1,
6110 fission_render::Color {
6111 r: env.theme.tokens.colors.background.r,
6112 g: env.theme.tokens.colors.background.g,
6113 b: env.theme.tokens.colors.background.b,
6114 a: env.theme.tokens.colors.background.a,
6115 },
6116 scale_factor as f32,
6117 )
6118 .expect("failed to rasterize software frame");
6119 device_handle.queue.write_texture(
6120 wgpu::TexelCopyTextureInfo {
6121 texture: &render_state
6122 .surface
6123 .target_texture,
6124 mip_level: 0,
6125 origin: wgpu::Origin3d::ZERO,
6126 aspect: wgpu::TextureAspect::All,
6127 },
6128 &rgba,
6129 wgpu::TexelCopyBufferLayout {
6130 offset: 0,
6131 bytes_per_row: Some(
6132 render_target_size.0 * 4,
6133 ),
6134 rows_per_image: Some(render_target_size.1),
6135 },
6136 wgpu::Extent3d {
6137 width: render_target_size.0,
6138 height: render_target_size.1,
6139 depth_or_array_layers: 1,
6140 },
6141 );
6142 }
6143 }
6144
6145 #[cfg(feature = "three-d")]
6146 {
6147 for (_, rect, payload) in &pipeline.scene_3d_surfaces {
6148 if let Ok(primitives) = bincode::deserialize::<
6149 Vec<fission_3d::Primitive3D>,
6150 >(
6151 payload
6152 ) {
6153 let scene3d = fission_3d::Scene3D {
6154 width: Some(rect.size.width),
6155 height: Some(rect.size.height),
6156 primitives,
6157 };
6158 let scale = scale_factor as f32;
6159 render_state.scene3d_renderer.render_in_rect(
6160 &device_handle.device,
6161 &device_handle.queue,
6162 &render_state.surface.target_view,
6163 &scene3d,
6164 fission_3d::render::Scene3DViewport {
6165 x: rect.origin.x * scale,
6166 y: rect.origin.y * scale,
6167 width: rect.size.width * scale,
6168 height: rect.size.height * scale,
6169 },
6170 );
6171 }
6172 }
6173 }
6174
6175 let surface_view = surface_texture
6176 .texture
6177 .create_view(&wgpu::TextureViewDescriptor::default());
6178
6179 let mut encoder =
6180 device_handle.device.create_command_encoder(
6181 &wgpu::CommandEncoderDescriptor {
6182 label: Some("Surface Blit"),
6183 },
6184 );
6185
6186 render_state.surface.blitter.copy(
6187 &device_handle.device,
6188 &mut encoder,
6189 &render_state.surface.target_view,
6190 &surface_view,
6191 );
6192
6193 device_handle.queue.submit(Some(encoder.finish()));
6194
6195 let capture_ready =
6196 !pending_capture_settle || resize_settled;
6197 if capture_ready {
6198 pending_capture_settle = false;
6199 }
6200 if capture_ready {
6201 if let Some(path) = pending_screenshot_path.take() {
6202 let screenshot_dimensions =
6203 layout_size_to_image_dimensions(
6204 target_viewport,
6205 );
6206 if let Some(tx) =
6207 pending_screenshot_response_tx.take()
6208 {
6209 if path == "__pump__" {
6210 let _ = tx.send(
6211 fission_test_driver::TestResponse::Ok {},
6212 );
6213 } else if path == "__capture__" {
6214 let resp = gpu_screenshot(
6215 &device_handle.device,
6216 &device_handle.queue,
6217 &render_state.surface.target_texture,
6218 render_target_size.0,
6219 render_target_size.1,
6220 screenshot_dimensions.0,
6221 screenshot_dimensions.1,
6222 None,
6223 );
6224 let _ = tx.send(resp);
6225 } else {
6226 let resp = gpu_screenshot(
6227 &device_handle.device,
6228 &device_handle.queue,
6229 &render_state.surface.target_texture,
6230 render_target_size.0,
6231 render_target_size.1,
6232 screenshot_dimensions.0,
6233 screenshot_dimensions.1,
6234 Some(&path),
6235 );
6236 let _ = tx.send(resp);
6237 }
6238 }
6239 }
6240 }
6241
6242 surface_texture.present();
6243 pending_resize = None;
6244 if resize_settled {
6245 resize_needs_settled_frame = false;
6246 }
6247 invalidations = InvalidationSet::default();
6248
6249 presented_frames = presented_frames.saturating_add(1);
6250 flush_text_traces(
6251 text_trace_enabled,
6252 &mut pending_text_traces,
6253 presented_frames,
6254 );
6255
6256 diag::emit(
6257 diag::DiagCategory::Frame,
6258 diag::DiagLevel::Debug,
6259 diag::DiagEventKind::FramePerformance {
6260 renderer: render_state
6261 .renderer_report
6262 .active
6263 .clone(),
6264 total_ms: now.elapsed().as_secs_f64() * 1000.0,
6265 },
6266 );
6267 diag::end_frame(diag::FrameStats::default());
6268 }
6269 }
6270 Err(e) => {
6271 eprintln!("Pipeline error: {:?}", e);
6272 diag::end_frame(diag::FrameStats::default());
6273 }
6274 }
6275 }
6276 WindowEvent::CloseRequested => {
6277 #[cfg(feature = "tray")]
6278 if active_tray
6279 .as_ref()
6280 .map(|tray| {
6281 tray.close_behavior() == tray::WindowCloseBehavior::HideToTray
6282 })
6283 .unwrap_or(false)
6284 {
6285 tray::hide_window_to_tray(window);
6286 return;
6287 }
6288 elwt.exit();
6289 }
6290 WindowEvent::CursorMoved { position, .. } => {
6293 last_cursor_position = Some(position);
6294 let point = window_physical_position_to_layout_point(window, position);
6295 handle_cursor_moved(
6296 point.x,
6297 point.y,
6298 current_mods,
6299 &mut runtime,
6300 &pipeline,
6301 &effect_result_tx,
6302 &event_proxy,
6303 &async_registry,
6304 &mut active_services,
6305 &mut service_bindings,
6306 &mut next_service_instance_id,
6307 &window,
6308 elwt,
6309 &mut last_redraw_at,
6310 min_frame,
6311 &mut redraw_pending,
6312 &mut frame_trace,
6313 &mut invalidations,
6314 );
6315 }
6316 WindowEvent::CursorLeft { .. } => {
6317 handle_cursor_left(
6318 last_cursor_position,
6319 &mut runtime,
6320 &pipeline,
6321 &effect_result_tx,
6322 &event_proxy,
6323 &async_registry,
6324 &mut active_services,
6325 &mut service_bindings,
6326 &mut next_service_instance_id,
6327 &window,
6328 elwt,
6329 &mut last_redraw_at,
6330 min_frame,
6331 &mut redraw_pending,
6332 &mut frame_trace,
6333 &mut invalidations,
6334 );
6335 last_cursor_position = None;
6336 }
6337 WindowEvent::MouseInput { state, button, .. } => {
6338 #[cfg(target_arch = "wasm32")]
6339 pending_web_input_at.get_or_insert_with(Instant::now);
6340 if let Some(position) = last_cursor_position {
6341 let point =
6342 window_physical_position_to_layout_point(window, position);
6343 if let Some(btn) = map_mouse_button(button) {
6344 let is_pressed = state.is_pressed();
6345 handle_mouse_button(
6346 point.x,
6347 point.y,
6348 btn,
6349 is_pressed,
6350 current_mods,
6351 &mut runtime,
6352 &pipeline,
6353 &effect_result_tx,
6354 &event_proxy,
6355 &async_registry,
6356 &mut active_services,
6357 &mut service_bindings,
6358 &mut next_service_instance_id,
6359 &window,
6360 elwt,
6361 &mut last_redraw_at,
6362 min_frame,
6363 &mut redraw_pending,
6364 text_trace_enabled,
6365 &mut pending_text_traces,
6366 &mut next_text_trace_seq,
6367 presented_frames,
6368 &mut last_blink_toggle,
6369 &mut frame_trace,
6370 &mut invalidations,
6371 );
6372 }
6373 }
6374 }
6375 WindowEvent::MouseWheel { delta, .. } => {
6376 #[cfg(target_arch = "wasm32")]
6377 pending_web_input_at.get_or_insert_with(Instant::now);
6378 if let Some(position) = last_cursor_position {
6379 let scale_factor = window.scale_factor();
6380 let point =
6381 window_physical_position_to_layout_point(window, position);
6382
6383 let (dx, dy) = normalize_winit_scroll_delta(&delta, scale_factor);
6384
6385 if std::env::var("FISSION_SCROLL_TRACE").ok().as_deref()
6386 == Some("1")
6387 {
6388 eprintln!(
6389 "[scroll-trace] mousewheel raw={:?} point=({:.1},{:.1}) delta=({:.1},{:.1})",
6390 delta, point.x, point.y, dx, dy
6391 );
6392 }
6393 handle_scroll(
6394 point.x,
6395 point.y,
6396 dx,
6397 dy,
6398 current_mods,
6399 &mut runtime,
6400 &pipeline,
6401 &effect_result_tx,
6402 &event_proxy,
6403 &async_registry,
6404 &mut active_services,
6405 &mut service_bindings,
6406 &mut next_service_instance_id,
6407 &window,
6408 elwt,
6409 &mut last_redraw_at,
6410 min_frame,
6411 &mut redraw_pending,
6412 &mut frame_trace,
6413 &mut invalidations,
6414 );
6415 }
6416 }
6417 WindowEvent::Touch(touch) => {
6418 #[cfg(target_arch = "wasm32")]
6419 pending_web_input_at.get_or_insert_with(Instant::now);
6420 let current_position = touch.location;
6421 let position = match touch.phase {
6425 TouchPhase::Ended | TouchPhase::Cancelled => touch_positions
6426 .get(&touch.id)
6427 .copied()
6428 .unwrap_or(current_position),
6429 TouchPhase::Started | TouchPhase::Moved => current_position,
6430 };
6431 last_cursor_position = Some(position);
6432
6433 let point = window_physical_position_to_layout_point(window, position);
6434
6435 match touch.phase {
6436 TouchPhase::Started => {
6437 touch_positions.insert(touch.id, position);
6438 if active_primary_touch.is_none() {
6439 active_primary_touch = Some(touch.id);
6440 }
6441 if active_primary_touch == Some(touch.id) {
6442 handle_cursor_moved(
6443 point.x,
6444 point.y,
6445 current_mods,
6446 &mut runtime,
6447 &pipeline,
6448 &effect_result_tx,
6449 &event_proxy,
6450 &async_registry,
6451 &mut active_services,
6452 &mut service_bindings,
6453 &mut next_service_instance_id,
6454 &window,
6455 elwt,
6456 &mut last_redraw_at,
6457 min_frame,
6458 &mut redraw_pending,
6459 &mut frame_trace,
6460 &mut invalidations,
6461 );
6462 handle_mouse_button(
6463 point.x,
6464 point.y,
6465 PointerButton::Primary,
6466 true,
6467 current_mods,
6468 &mut runtime,
6469 &pipeline,
6470 &effect_result_tx,
6471 &event_proxy,
6472 &async_registry,
6473 &mut active_services,
6474 &mut service_bindings,
6475 &mut next_service_instance_id,
6476 &window,
6477 elwt,
6478 &mut last_redraw_at,
6479 min_frame,
6480 &mut redraw_pending,
6481 text_trace_enabled,
6482 &mut pending_text_traces,
6483 &mut next_text_trace_seq,
6484 presented_frames,
6485 &mut last_blink_toggle,
6486 &mut frame_trace,
6487 &mut invalidations,
6488 );
6489 }
6490 }
6491 TouchPhase::Moved => {
6492 touch_positions.insert(touch.id, position);
6493 if active_primary_touch == Some(touch.id) {
6494 handle_cursor_moved(
6495 point.x,
6496 point.y,
6497 current_mods,
6498 &mut runtime,
6499 &pipeline,
6500 &effect_result_tx,
6501 &event_proxy,
6502 &async_registry,
6503 &mut active_services,
6504 &mut service_bindings,
6505 &mut next_service_instance_id,
6506 &window,
6507 elwt,
6508 &mut last_redraw_at,
6509 min_frame,
6510 &mut redraw_pending,
6511 &mut frame_trace,
6512 &mut invalidations,
6513 );
6514 }
6515 }
6516 TouchPhase::Ended | TouchPhase::Cancelled => {
6517 if active_primary_touch == Some(touch.id) {
6518 handle_cursor_moved(
6519 point.x,
6520 point.y,
6521 current_mods,
6522 &mut runtime,
6523 &pipeline,
6524 &effect_result_tx,
6525 &event_proxy,
6526 &async_registry,
6527 &mut active_services,
6528 &mut service_bindings,
6529 &mut next_service_instance_id,
6530 &window,
6531 elwt,
6532 &mut last_redraw_at,
6533 min_frame,
6534 &mut redraw_pending,
6535 &mut frame_trace,
6536 &mut invalidations,
6537 );
6538 handle_mouse_button(
6539 point.x,
6540 point.y,
6541 PointerButton::Primary,
6542 false,
6543 current_mods,
6544 &mut runtime,
6545 &pipeline,
6546 &effect_result_tx,
6547 &event_proxy,
6548 &async_registry,
6549 &mut active_services,
6550 &mut service_bindings,
6551 &mut next_service_instance_id,
6552 &window,
6553 elwt,
6554 &mut last_redraw_at,
6555 min_frame,
6556 &mut redraw_pending,
6557 text_trace_enabled,
6558 &mut pending_text_traces,
6559 &mut next_text_trace_seq,
6560 presented_frames,
6561 &mut last_blink_toggle,
6562 &mut frame_trace,
6563 &mut invalidations,
6564 );
6565 active_primary_touch = None;
6566 }
6567 touch_positions.remove(&touch.id);
6568 }
6569 }
6570 }
6571 WindowEvent::ModifiersChanged(modifiers) => {
6572 current_mods = 0;
6573 if modifiers.state().shift_key() {
6574 current_mods |= 1;
6575 }
6576 if modifiers.state().alt_key() {
6577 current_mods |= 2;
6578 }
6579 if modifiers.state().control_key() {
6580 current_mods |= 4;
6581 }
6582 if modifiers.state().super_key() {
6583 current_mods |= 8;
6584 }
6585 }
6586 WindowEvent::KeyboardInput { event, .. } => {
6587 #[cfg(target_arch = "wasm32")]
6588 pending_web_input_at.get_or_insert_with(Instant::now);
6589 if event.state.is_pressed() {
6590 use winit::keyboard::{Key, NamedKey};
6591 let key_code = match event.logical_key {
6592 Key::Named(NamedKey::Space) => Some(KeyCode::Space),
6593 Key::Named(NamedKey::Enter) => Some(KeyCode::Enter),
6594 Key::Named(NamedKey::Escape) => Some(KeyCode::Escape),
6595 Key::Named(NamedKey::Backspace) => Some(KeyCode::Backspace),
6596 Key::Named(NamedKey::Delete) => Some(KeyCode::Delete),
6597 Key::Named(NamedKey::Tab) => Some(KeyCode::Tab),
6598 Key::Named(NamedKey::ArrowLeft) => Some(KeyCode::Left),
6599 Key::Named(NamedKey::ArrowRight) => Some(KeyCode::Right),
6600 Key::Named(NamedKey::ArrowUp) => Some(KeyCode::Up),
6601 Key::Named(NamedKey::ArrowDown) => Some(KeyCode::Down),
6602 Key::Named(NamedKey::Home) => Some(KeyCode::Home),
6603 Key::Named(NamedKey::End) => Some(KeyCode::End),
6604 Key::Named(NamedKey::PageUp) => Some(KeyCode::PageUp),
6605 Key::Named(NamedKey::PageDown) => Some(KeyCode::PageDown),
6606 _ => {
6607 if let Some(text) = &event.text {
6608 text.chars().next().map(KeyCode::Char)
6609 } else {
6610 None
6611 }
6612 }
6613 };
6614
6615 if let Some(code) = key_code {
6616 handle_key_down::<S>(
6617 code,
6618 current_mods,
6619 &mut runtime,
6620 &pipeline,
6621 &effect_result_tx,
6622 &event_proxy,
6623 &async_registry,
6624 &mut active_services,
6625 &mut service_bindings,
6626 &mut next_service_instance_id,
6627 &window,
6628 elwt,
6629 &mut last_redraw_at,
6630 min_frame,
6631 &mut redraw_pending,
6632 text_trace_enabled,
6633 &mut pending_text_traces,
6634 &mut next_text_trace_seq,
6635 presented_frames,
6636 &mut last_blink_toggle,
6637 self.key_handler.as_ref(),
6638 &mut frame_trace,
6639 &mut invalidations,
6640 );
6641 }
6642 }
6643 }
6644 WindowEvent::Ime(ime) => {
6645 #[cfg(target_arch = "wasm32")]
6646 pending_web_input_at.get_or_insert_with(Instant::now);
6647 if let (Some(ir), Some(layout)) =
6648 (&pipeline.prev_ir, &pipeline.last_snapshot)
6649 {
6650 let (input_event, source) = match ime {
6651 Ime::Commit(text) => (
6652 Some(InputEvent::Ime(
6653 fission_core::event::ImeEvent::Commit {
6654 text: text.clone(),
6655 },
6656 )),
6657 Some(format!("ime_commit:{}", text.chars().count())),
6658 ),
6659 Ime::Preedit(text, cursor) => (
6660 Some(InputEvent::Ime(
6661 fission_core::event::ImeEvent::Preedit {
6662 text: text.clone(),
6663 cursor,
6664 },
6665 )),
6666 Some(format!("ime_preedit:{}", text.chars().count())),
6667 ),
6668 Ime::Disabled => (
6669 Some(InputEvent::Ime(
6670 fission_core::event::ImeEvent::Cancel,
6671 )),
6672 Some("ime_cancel".to_string()),
6673 ),
6674 _ => (None, None),
6675 };
6676
6677 if let Some(e) = input_event {
6678 let target =
6679 focused_text_input_id(&runtime, pipeline.prev_ir.as_ref());
6680 let trace_seq = start_text_trace(
6681 text_trace_enabled && target.is_some(),
6682 &mut pending_text_traces,
6683 &mut next_text_trace_seq,
6684 source.unwrap_or_else(|| "ime".to_string()),
6685 target,
6686 presented_frames,
6687 );
6688 runtime.handle_input(e, ir, layout).ok();
6689 invalidations.mark_build();
6690 mark_text_trace_handled(&mut pending_text_traces, trace_seq);
6691 if process_pending_effects(
6692 &mut runtime,
6693 &effect_result_tx,
6694 &event_proxy,
6695 &async_registry,
6696 &mut active_services,
6697 &mut service_bindings,
6698 &mut next_service_instance_id,
6699 ) {
6700 mark_text_trace_effects(
6701 &mut pending_text_traces,
6702 trace_seq,
6703 );
6704 invalidations.mark_build();
6705 request_redraw_logged(
6706 &window,
6707 elwt,
6708 &mut last_redraw_at,
6709 min_frame,
6710 &mut redraw_pending,
6711 &mut frame_trace,
6712 "ime:effects",
6713 );
6714 }
6715 reset_text_input_caret(
6716 &mut runtime,
6717 pipeline.prev_ir.as_ref(),
6718 &mut last_blink_toggle,
6719 );
6720 request_redraw_logged(
6721 &window,
6722 elwt,
6723 &mut last_redraw_at,
6724 min_frame,
6725 &mut redraw_pending,
6726 &mut frame_trace,
6727 "ime",
6728 );
6729 }
6730 }
6731 }
6732 _ => {}
6733 }
6734 }
6735 _ => {}
6736 }
6737 };
6738
6739 #[cfg(target_arch = "wasm32")]
6740 {
6741 #[allow(deprecated)]
6742 event_loop.spawn(event_handler);
6743 Ok(())
6744 }
6745 #[cfg(not(target_arch = "wasm32"))]
6746 {
6747 #[allow(deprecated)]
6748 event_loop
6749 .run(event_handler)
6750 .map_err(|e| anyhow::anyhow!("Event loop error: {}", e))
6751 }
6752 }
6753}
6754
6755fn build_font_context() -> FontContext {
6756 let use_system_fonts = std::env::var("FISSION_USE_SYSTEM_FONTS")
6757 .map(|v| matches!(v.to_ascii_lowercase().as_str(), "1" | "true" | "yes"))
6758 .unwrap_or(false);
6759 let options = CollectionOptions {
6760 shared: false,
6761 system_fonts: use_system_fonts,
6762 };
6763 FontContext {
6764 collection: Collection::new(options),
6765 source_cache: SourceCache::default(),
6766 }
6767}
6768
6769fn map_mouse_button(button: MouseButton) -> Option<PointerButton> {
6771 match button {
6772 MouseButton::Left => Some(PointerButton::Primary),
6773 MouseButton::Right => Some(PointerButton::Secondary),
6774 MouseButton::Middle => Some(PointerButton::Middle),
6775 MouseButton::Other(id) => Some(PointerButton::Other(id as u8)),
6776 _ => None,
6777 }
6778}
6779
6780fn clamp_copy_extent_to_texture(
6781 requested_width: u32,
6782 requested_height: u32,
6783 actual_width: u32,
6784 actual_height: u32,
6785) -> (u32, u32) {
6786 (
6787 requested_width.min(actual_width).max(1),
6788 requested_height.min(actual_height).max(1),
6789 )
6790}
6791
6792fn gpu_screenshot(
6793 device: &wgpu::Device,
6794 queue: &wgpu::Queue,
6795 texture: &wgpu::Texture,
6796 texture_width: u32,
6797 texture_height: u32,
6798 output_width: u32,
6799 output_height: u32,
6800 path: Option<&str>,
6801) -> fission_test_driver::TestResponse {
6802 let actual_texture_width = texture.width();
6803 let actual_texture_height = texture.height();
6804 let (texture_width, texture_height) = clamp_copy_extent_to_texture(
6805 texture_width,
6806 texture_height,
6807 actual_texture_width,
6808 actual_texture_height,
6809 );
6810 if output_width == 0 || output_height == 0 {
6811 return fission_test_driver::TestResponse::Error {
6812 message: "zero-size viewport".into(),
6813 };
6814 }
6815
6816 let bytes_per_pixel = 4u32;
6817 let unpadded_bytes_per_row = texture_width * bytes_per_pixel;
6818 let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT;
6819 let padded_bytes_per_row = (unpadded_bytes_per_row + align - 1) / align * align;
6820 let buffer_size = (padded_bytes_per_row * texture_height) as u64;
6821
6822 let staging = device.create_buffer(&wgpu::BufferDescriptor {
6823 label: Some("screenshot staging"),
6824 size: buffer_size,
6825 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
6826 mapped_at_creation: false,
6827 });
6828
6829 let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
6830 label: Some("screenshot copy"),
6831 });
6832
6833 encoder.copy_texture_to_buffer(
6834 wgpu::TexelCopyTextureInfo {
6835 texture,
6836 mip_level: 0,
6837 origin: wgpu::Origin3d::ZERO,
6838 aspect: wgpu::TextureAspect::All,
6839 },
6840 wgpu::TexelCopyBufferInfo {
6841 buffer: &staging,
6842 layout: wgpu::TexelCopyBufferLayout {
6843 offset: 0,
6844 bytes_per_row: Some(padded_bytes_per_row),
6845 rows_per_image: Some(texture_height),
6846 },
6847 },
6848 wgpu::Extent3d {
6849 width: texture_width,
6850 height: texture_height,
6851 depth_or_array_layers: 1,
6852 },
6853 );
6854
6855 queue.submit(Some(encoder.finish()));
6856
6857 let (tx, rx) = std::sync::mpsc::channel();
6858 staging
6859 .slice(..)
6860 .map_async(wgpu::MapMode::Read, move |result| {
6861 let _ = tx.send(result);
6862 });
6863 let _ = device.poll(wgpu::PollType::Wait);
6864
6865 match rx.recv() {
6866 Ok(Ok(())) => {}
6867 Ok(Err(e)) => {
6868 return fission_test_driver::TestResponse::Error {
6869 message: format!("buffer map failed: {:?}", e),
6870 };
6871 }
6872 Err(e) => {
6873 return fission_test_driver::TestResponse::Error {
6874 message: format!("buffer map channel error: {}", e),
6875 };
6876 }
6877 }
6878
6879 let data = staging.slice(..).get_mapped_range();
6880
6881 let mut rgba = Vec::with_capacity((texture_width * texture_height * 4) as usize);
6883 for row in 0..texture_height {
6884 let start = (row * padded_bytes_per_row) as usize;
6885 let end = start + (texture_width * bytes_per_pixel) as usize;
6886 rgba.extend_from_slice(&data[start..end]);
6887 }
6888
6889 drop(data);
6890 staging.unmap();
6891
6892 let (rgba, width, height) = if texture_width == output_width && texture_height == output_height
6893 {
6894 (rgba, texture_width, texture_height)
6895 } else if let Some(resized) = downscale_rgba_box(
6896 &rgba,
6897 texture_width,
6898 texture_height,
6899 output_width,
6900 output_height,
6901 ) {
6902 (resized, output_width, output_height)
6903 } else {
6904 let Some(image) = image::RgbaImage::from_raw(texture_width, texture_height, rgba) else {
6905 return fission_test_driver::TestResponse::Error {
6906 message: "failed to decode screenshot RGBA buffer".into(),
6907 };
6908 };
6909 let resized = image::imageops::resize(
6910 &image,
6911 output_width,
6912 output_height,
6913 image::imageops::FilterType::Triangle,
6914 );
6915 (resized.into_raw(), output_width, output_height)
6916 };
6917
6918 let mut png = Vec::new();
6919 {
6920 use image::ImageEncoder;
6921 let encoder = image::codecs::png::PngEncoder::new(&mut png);
6922 if let Err(e) = encoder.write_image(&rgba, width, height, image::ExtendedColorType::Rgba8) {
6923 return fission_test_driver::TestResponse::Error {
6924 message: format!("PNG encode failed: {}", e),
6925 };
6926 }
6927 }
6928
6929 if let Some(path) = path {
6930 match std::fs::write(path, &png) {
6931 Ok(()) => fission_test_driver::TestResponse::Ok {},
6932 Err(e) => fission_test_driver::TestResponse::Error {
6933 message: format!("PNG save failed: {}", e),
6934 },
6935 }
6936 } else {
6937 fission_test_driver::TestResponse::Screenshot {
6938 png_base64: base64::engine::general_purpose::STANDARD.encode(png),
6939 width,
6940 height,
6941 }
6942 }
6943}
6944
6945fn downscale_rgba_box(
6946 rgba: &[u8],
6947 input_width: u32,
6948 input_height: u32,
6949 output_width: u32,
6950 output_height: u32,
6951) -> Option<Vec<u8>> {
6952 if output_width == 0
6953 || output_height == 0
6954 || input_width % output_width != 0
6955 || input_height % output_height != 0
6956 {
6957 return None;
6958 }
6959
6960 let scale_x = input_width / output_width;
6961 let scale_y = input_height / output_height;
6962 if scale_x <= 1 && scale_y <= 1 {
6963 return None;
6964 }
6965
6966 let samples_per_pixel = scale_x.checked_mul(scale_y)?;
6967 let mut out = vec![0u8; (output_width * output_height * 4) as usize];
6968
6969 for out_y in 0..output_height {
6970 let src_y0 = out_y * scale_y;
6971 for out_x in 0..output_width {
6972 let src_x0 = out_x * scale_x;
6973 let mut sum = [0u32; 4];
6974 for dy in 0..scale_y {
6975 let src_y = src_y0 + dy;
6976 let row_offset = ((src_y * input_width) * 4) as usize;
6977 for dx in 0..scale_x {
6978 let src_x = src_x0 + dx;
6979 let src_index = row_offset + (src_x * 4) as usize;
6980 sum[0] += rgba[src_index] as u32;
6981 sum[1] += rgba[src_index + 1] as u32;
6982 sum[2] += rgba[src_index + 2] as u32;
6983 sum[3] += rgba[src_index + 3] as u32;
6984 }
6985 }
6986
6987 let dst_index = (((out_y * output_width) + out_x) * 4) as usize;
6988 out[dst_index] = (sum[0] / samples_per_pixel) as u8;
6989 out[dst_index + 1] = (sum[1] / samples_per_pixel) as u8;
6990 out[dst_index + 2] = (sum[2] / samples_per_pixel) as u8;
6991 out[dst_index + 3] = (sum[3] / samples_per_pixel) as u8;
6992 }
6993 }
6994
6995 Some(out)
6996}
6997
6998fn layout_size_to_image_dimensions(size: LayoutSize) -> (u32, u32) {
6999 let width = size.width.max(1.0).round() as u32;
7000 let height = size.height.max(1.0).round() as u32;
7001 (width.max(1), height.max(1))
7002}
7003
7004fn normalize_scale_factor(scale_factor: f64) -> f64 {
7005 if scale_factor.is_finite() && scale_factor > 0.0 {
7006 scale_factor
7007 } else {
7008 1.0
7009 }
7010}
7011
7012#[cfg(target_os = "ios")]
7013fn ios_effective_scale_factor(reported_scale_factor: f64) -> f64 {
7014 std::env::var("FISSION_IOS_SCALE_FACTOR")
7015 .ok()
7016 .and_then(|value| value.parse::<f64>().ok())
7017 .filter(|scale| scale.is_finite() && *scale > 0.0)
7018 .unwrap_or_else(|| {
7019 if reported_scale_factor >= 2.0 {
7020 reported_scale_factor
7021 } else {
7022 3.0
7023 }
7024 })
7025}
7026
7027#[cfg(target_arch = "wasm32")]
7028fn web_browser_viewport_state() -> Option<WindowViewportState> {
7029 let window = web_sys::window()?;
7030 let width = window.inner_width().ok()?.as_f64()? as f32;
7031 let height = window.inner_height().ok()?.as_f64()? as f32;
7032 if !width.is_finite() || !height.is_finite() || width <= 0.0 || height <= 0.0 {
7033 return None;
7034 }
7035 let scale_factor = normalize_scale_factor(window.device_pixel_ratio());
7036 Some(WindowViewportState {
7037 physical_size: logical_viewport_to_physical_size(
7038 LayoutSize::new(width, height),
7039 scale_factor,
7040 ),
7041 scale_factor,
7042 })
7043}
7044
7045fn physical_size_to_layout_size(size: PhysicalSize<u32>, scale_factor: f64) -> LayoutSize {
7046 let scale_factor = normalize_scale_factor(scale_factor);
7047 LayoutSize {
7048 width: (size.width as f64 / scale_factor) as f32,
7049 height: (size.height as f64 / scale_factor) as f32,
7050 }
7051}
7052
7053fn logical_viewport_to_render_target_size(size: LayoutSize, scale_factor: f64) -> (u32, u32) {
7054 let scale_factor = normalize_scale_factor(scale_factor);
7055 let width = (size.width.max(1.0) as f64 * scale_factor).ceil() as u32;
7056 let height = (size.height.max(1.0) as f64 * scale_factor).ceil() as u32;
7057 (width.max(1), height.max(1))
7058}
7059
7060fn logical_viewport_to_physical_size(size: LayoutSize, scale_factor: f64) -> PhysicalSize<u32> {
7061 let (width, height) = logical_viewport_to_render_target_size(size, scale_factor);
7062 PhysicalSize::new(width, height)
7063}
7064
7065fn recreate_target_texture(
7066 surface: &mut RenderSurface,
7067 render_cx: &RenderContext,
7068 width: u32,
7069 height: u32,
7070) {
7071 let device = &render_cx.devices[surface.dev_id].device;
7072 let size = wgpu::Extent3d {
7073 width: width.max(1),
7074 height: height.max(1),
7075 depth_or_array_layers: 1,
7076 };
7077 let new_texture = device.create_texture(&wgpu::TextureDescriptor {
7078 label: Some("fission_target_with_copy"),
7079 size,
7080 mip_level_count: 1,
7081 sample_count: 1,
7082 dimension: wgpu::TextureDimension::D2,
7083 format: wgpu::TextureFormat::Rgba8Unorm, usage: wgpu::TextureUsages::STORAGE_BINDING
7085 | wgpu::TextureUsages::TEXTURE_BINDING
7086 | wgpu::TextureUsages::RENDER_ATTACHMENT
7087 | wgpu::TextureUsages::COPY_SRC
7088 | wgpu::TextureUsages::COPY_DST,
7089 view_formats: &[],
7090 });
7091 let new_view = new_texture.create_view(&wgpu::TextureViewDescriptor::default());
7092 surface.target_texture = new_texture;
7093 surface.target_view = new_view;
7094}
7095
7096fn sync_tracked_target_texture_size_to_surface(
7097 target_texture_size: &mut (u32, u32),
7098 surface_size: PhysicalSize<u32>,
7099) {
7100 *target_texture_size = (surface_size.width.max(1), surface_size.height.max(1));
7101}
7102
7103#[cfg(any(test, not(any(target_os = "android", target_os = "ios"))))]
7104fn native_window_size_for_logical_viewport(size: LayoutSize) -> winit::dpi::LogicalSize<f64> {
7105 winit::dpi::LogicalSize::new(size.width as f64, size.height as f64)
7106}
7107
7108#[cfg(test)]
7109mod tests {
7110 use super::{
7111 animation_redraw_interval, clamp_copy_extent_to_texture, collect_startup_deep_links_from,
7112 cursor_icon_for, downscale_rgba_box, layout_size_to_image_dimensions,
7113 logical_viewport_to_physical_size, logical_viewport_to_render_target_size,
7114 native_window_size_for_logical_viewport, normalize_scale_factor,
7115 normalize_winit_scroll_delta, physical_position_to_layout_point,
7116 physical_size_to_layout_size, rect_visible_in_scroll_ancestors,
7117 repeating_animation_redraw_interval, resize_is_unsettled, resolve_build_viewport,
7118 sync_tracked_target_texture_size_to_surface, texture_plans_fit_device_limits,
7119 visual_rect_for_node, window_insets_from_safe_area_frames, LiveResizeController,
7120 WindowViewportState,
7121 };
7122 use crate::pipeline::CompositorTexturePlan;
7123 use crate::InvalidationSet;
7124 use fission_core::{ActiveMotion, MotionEasing, MotionStateMap, MotionValue, ScrollStateMap};
7125 use fission_core::{DeepLinkConfig, MotionPropertyId, WidgetId};
7126 use fission_ir::semantics::MouseCursor;
7127 use fission_ir::{CoreIR, FlexDirection, LayoutOp, Op};
7128 use fission_layout::{LayoutNodeGeometry, LayoutRect, LayoutSize, LayoutSnapshot};
7129 use std::collections::HashMap;
7130 use std::time::Duration;
7131 use winit::dpi::{PhysicalPosition, PhysicalSize};
7132 use winit::event::MouseScrollDelta;
7133 use winit::window::CursorIcon;
7134
7135 #[test]
7136 fn semantic_cursor_icons_map_to_winit_icons() {
7137 assert_eq!(cursor_icon_for(MouseCursor::Default), CursorIcon::Default);
7138 assert_eq!(cursor_icon_for(MouseCursor::Pointer), CursorIcon::Pointer);
7139 assert_eq!(cursor_icon_for(MouseCursor::Text), CursorIcon::Text);
7140 assert_eq!(
7141 cursor_icon_for(MouseCursor::NotAllowed),
7142 CursorIcon::NotAllowed
7143 );
7144 assert_eq!(
7145 cursor_icon_for(MouseCursor::VerticalText),
7146 CursorIcon::VerticalText
7147 );
7148 }
7149
7150 #[test]
7151 fn winit_scroll_delta_normalizes_to_positive_down_and_right() {
7152 assert_eq!(
7153 normalize_winit_scroll_delta(&MouseScrollDelta::LineDelta(-1.0, -2.0), 1.0),
7154 (50.0, 100.0)
7155 );
7156 assert_eq!(
7157 normalize_winit_scroll_delta(
7158 &MouseScrollDelta::PixelDelta(PhysicalPosition::new(-20.0, -40.0)),
7159 2.0,
7160 ),
7161 (10.0, 20.0)
7162 );
7163 }
7164
7165 #[test]
7166 fn physical_input_position_maps_into_layout_space() {
7167 let point = physical_position_to_layout_point(
7168 PhysicalPosition::new(240.0, 360.0),
7169 2.0,
7170 PhysicalPosition::new(0, 0),
7171 );
7172 assert_eq!(point, fission_render::LayoutPoint::new(120.0, 180.0));
7173 }
7174
7175 #[test]
7176 fn physical_input_position_subtracts_content_origin_before_scaling() {
7177 let point = physical_position_to_layout_point(
7178 PhysicalPosition::new(240.0, 460.0),
7179 2.0,
7180 PhysicalPosition::new(0, 100),
7181 );
7182 assert_eq!(point, fission_render::LayoutPoint::new(120.0, 180.0));
7183 }
7184
7185 #[test]
7186 fn safe_area_frames_convert_to_logical_window_insets() {
7187 let insets = window_insets_from_safe_area_frames(
7188 PhysicalPosition::new(0, 177),
7189 PhysicalPosition::new(0, 0),
7190 PhysicalSize::new(1206, 2343),
7191 PhysicalSize::new(1206, 2622),
7192 3.0,
7193 );
7194
7195 assert_eq!(insets.left, 0.0);
7196 assert_eq!(insets.right, 0.0);
7197 assert_eq!(insets.top, 59.0);
7198 assert_eq!(insets.bottom, 34.0);
7199 }
7200
7201 #[test]
7202 fn visual_rect_subtracts_ancestor_scroll_offset() {
7203 let scroll = WidgetId::from_u128(1);
7204 let child = WidgetId::from_u128(2);
7205 let mut ir = CoreIR::new();
7206 ir.add_node(
7207 child,
7208 Op::Paint(fission_ir::PaintOp::DrawRect {
7209 fill: None,
7210 stroke: None,
7211 corner_radius: 0.0,
7212 shadow: None,
7213 }),
7214 Vec::new(),
7215 );
7216 ir.add_node(
7217 scroll,
7218 Op::Layout(LayoutOp::Scroll {
7219 direction: FlexDirection::Column,
7220 show_scrollbar: true,
7221 width: None,
7222 height: None,
7223 min_width: None,
7224 max_width: None,
7225 min_height: None,
7226 max_height: None,
7227 padding: [0.0; 4],
7228 flex_grow: 0.0,
7229 flex_shrink: 1.0,
7230 }),
7231 vec![child],
7232 );
7233 ir.set_root(scroll);
7234
7235 let mut snapshot = LayoutSnapshot::new(LayoutSize::new(100.0, 100.0));
7236 snapshot.nodes.insert(
7237 scroll,
7238 LayoutNodeGeometry {
7239 rect: LayoutRect::new(0.0, 0.0, 100.0, 100.0),
7240 content_size: LayoutSize::new(100.0, 400.0),
7241 },
7242 );
7243 snapshot.nodes.insert(
7244 child,
7245 LayoutNodeGeometry {
7246 rect: LayoutRect::new(0.0, 150.0, 80.0, 20.0),
7247 content_size: LayoutSize::new(80.0, 20.0),
7248 },
7249 );
7250 let mut scroll_map = ScrollStateMap::default();
7251 scroll_map.set_offset(scroll, 120.0);
7252
7253 let visual = visual_rect_for_node(&ir, &snapshot, &scroll_map, child).unwrap();
7254 assert_eq!(visual, LayoutRect::new(0.0, 30.0, 80.0, 20.0));
7255 assert!(rect_visible_in_scroll_ancestors(
7256 &ir,
7257 &snapshot,
7258 &scroll_map,
7259 child,
7260 visual
7261 ));
7262 }
7263
7264 #[test]
7265 fn repeating_animation_uses_reduced_frame_rate() {
7266 let min_frame = Duration::from_millis(16);
7267 let repeat_frame = Duration::from_millis(66);
7268 assert_eq!(
7269 animation_redraw_interval(false, Some(repeat_frame), false, min_frame),
7270 Some(repeat_frame)
7271 );
7272 }
7273
7274 #[test]
7275 fn finite_animation_keeps_full_frame_rate() {
7276 let min_frame = Duration::from_millis(16);
7277 assert_eq!(
7278 animation_redraw_interval(true, None, false, min_frame),
7279 Some(min_frame)
7280 );
7281 assert_eq!(
7282 animation_redraw_interval(false, None, true, min_frame),
7283 Some(min_frame)
7284 );
7285 }
7286
7287 #[test]
7288 fn idle_video_does_not_force_full_frame_rate() {
7289 let min_frame = Duration::from_millis(16);
7290 let repeat_frame = Duration::from_millis(66);
7291 assert_eq!(
7292 animation_redraw_interval(false, Some(repeat_frame), false, min_frame),
7293 Some(repeat_frame)
7294 );
7295 }
7296
7297 #[test]
7298 fn no_repeat_interval_means_no_idle_animation_redraw() {
7299 let min_frame = Duration::from_millis(16);
7300 assert_eq!(
7301 animation_redraw_interval(false, None, false, min_frame),
7302 None
7303 );
7304 }
7305
7306 #[test]
7307 fn repeat_animation_interval_uses_low_priority_hint() {
7308 let mut animation = MotionStateMap::default();
7309 animation.active.insert(
7310 (WidgetId::explicit("spinner"), MotionPropertyId::opacity()),
7311 ActiveMotion {
7312 target: WidgetId::explicit("spinner"),
7313 property: MotionPropertyId::opacity(),
7314 start_value: MotionValue::Scalar(0.3),
7315 end_value: MotionValue::Scalar(1.0),
7316 start_time: 0,
7317 duration: 600,
7318 repeat: true,
7319 frame_interval_ms: Some(166),
7320 easing: MotionEasing::Linear,
7321 },
7322 );
7323 assert_eq!(
7324 repeating_animation_redraw_interval(&animation, Duration::from_millis(66)),
7325 Some(Duration::from_millis(166))
7326 );
7327 }
7328
7329 #[test]
7330 fn repeat_animation_interval_chooses_fastest_active_repeat() {
7331 let mut animation = MotionStateMap {
7332 values: HashMap::new(),
7333 active: HashMap::new(),
7334 ..Default::default()
7335 };
7336 animation.active.insert(
7337 (WidgetId::explicit("slow"), MotionPropertyId::opacity()),
7338 ActiveMotion {
7339 target: WidgetId::explicit("slow"),
7340 property: MotionPropertyId::opacity(),
7341 start_value: MotionValue::Scalar(0.3),
7342 end_value: MotionValue::Scalar(1.0),
7343 start_time: 0,
7344 duration: 600,
7345 repeat: true,
7346 frame_interval_ms: Some(200),
7347 easing: MotionEasing::Linear,
7348 },
7349 );
7350 animation.active.insert(
7351 (WidgetId::explicit("fast"), MotionPropertyId::opacity()),
7352 ActiveMotion {
7353 target: WidgetId::explicit("fast"),
7354 property: MotionPropertyId::opacity(),
7355 start_value: MotionValue::Scalar(0.3),
7356 end_value: MotionValue::Scalar(1.0),
7357 start_time: 0,
7358 duration: 600,
7359 repeat: true,
7360 frame_interval_ms: Some(100),
7361 easing: MotionEasing::Linear,
7362 },
7363 );
7364 assert_eq!(
7365 repeating_animation_redraw_interval(&animation, Duration::from_millis(66)),
7366 Some(Duration::from_millis(100))
7367 );
7368 }
7369
7370 #[test]
7371 fn live_resize_reports_unsettled_until_deadline() {
7372 let settle = Duration::from_millis(90);
7373 let mut resize = LiveResizeController::new(settle);
7374 let now = std::time::Instant::now();
7375 resize.note_resize(now);
7376
7377 assert!(resize.is_live(now + Duration::from_millis(30)));
7378 assert!(resize_is_unsettled(
7379 false,
7380 false,
7381 resize.is_live(now + Duration::from_millis(30))
7382 ));
7383 assert!(!resize.is_live(now + Duration::from_millis(95)));
7384 }
7385
7386 #[test]
7387 fn viewport_resize_forces_build_viewport_refresh() {
7388 let target = LayoutSize::new(1440.0, 900.0);
7389 let mut invalidations = InvalidationSet::default();
7390
7391 let build_viewport = resolve_build_viewport(
7392 Some(LayoutSize::new(1024.0, 768.0)),
7393 target,
7394 true,
7395 &mut invalidations,
7396 );
7397
7398 assert!(invalidations.build);
7399 assert_eq!(build_viewport, target);
7400 }
7401
7402 #[test]
7403 fn stable_viewport_preserves_existing_build_viewport() {
7404 let target = LayoutSize::new(1024.0, 768.0);
7405 let mut invalidations = InvalidationSet::default();
7406
7407 let build_viewport = resolve_build_viewport(Some(target), target, true, &mut invalidations);
7408
7409 assert!(!invalidations.build);
7410 assert_eq!(build_viewport, target);
7411 }
7412
7413 #[test]
7414 fn oversized_texture_plan_forces_scene_fallback() {
7415 let plans = vec![CompositorTexturePlan {
7416 key: 1,
7417 bounds: LayoutRect::new(0.0, 0.0, 320.0, 9000.0),
7418 scene: Some(fission_render::RenderScene::new(LayoutRect::new(
7419 0.0, 0.0, 320.0, 9000.0,
7420 ))),
7421 scene_cache_key: Some(1),
7422 content_key: 1,
7423 local_dynamic: false,
7424 composite_dynamic: false,
7425 opacity: 1.0,
7426 transform: None,
7427 transform_clip: false,
7428 clip: None,
7429 children: Vec::new(),
7430 source_layer_path: None,
7431 }];
7432 assert!(!texture_plans_fit_device_limits(&plans, 1.0, 8192));
7433 }
7434
7435 #[test]
7436 fn nested_texture_plans_must_all_fit_device_limits() {
7437 let child = CompositorTexturePlan {
7438 key: 2,
7439 bounds: LayoutRect::new(0.0, 0.0, 400.0, 8400.0),
7440 scene: Some(fission_render::RenderScene::new(LayoutRect::new(
7441 0.0, 0.0, 400.0, 8400.0,
7442 ))),
7443 scene_cache_key: Some(2),
7444 content_key: 2,
7445 local_dynamic: false,
7446 composite_dynamic: false,
7447 opacity: 1.0,
7448 transform: None,
7449 transform_clip: false,
7450 clip: None,
7451 children: Vec::new(),
7452 source_layer_path: None,
7453 };
7454 let plans = vec![CompositorTexturePlan {
7455 key: 1,
7456 bounds: LayoutRect::new(0.0, 0.0, 800.0, 600.0),
7457 scene: None,
7458 scene_cache_key: None,
7459 content_key: 3,
7460 local_dynamic: false,
7461 composite_dynamic: false,
7462 opacity: 1.0,
7463 transform: None,
7464 transform_clip: false,
7465 clip: None,
7466 children: vec![child],
7467 source_layer_path: None,
7468 }];
7469 assert!(!texture_plans_fit_device_limits(&plans, 1.0, 8192));
7470 }
7471
7472 #[test]
7473 fn screenshot_dimensions_follow_logical_viewport() {
7474 let dims = layout_size_to_image_dimensions(fission_layout::LayoutSize::new(1600.0, 1200.0));
7475 assert_eq!(dims, (1600, 1200));
7476
7477 let rounded =
7478 layout_size_to_image_dimensions(fission_layout::LayoutSize::new(999.6, 700.4));
7479 assert_eq!(rounded, (1000, 700));
7480 }
7481
7482 #[test]
7483 fn simulated_resize_uses_physical_render_target_size() {
7484 let dims = logical_viewport_to_render_target_size(
7485 fission_layout::LayoutSize::new(1600.0, 1200.0),
7486 2.0,
7487 );
7488 assert_eq!(dims, (3200, 2400));
7489
7490 let fractional = logical_viewport_to_render_target_size(
7491 fission_layout::LayoutSize::new(430.0, 900.0),
7492 1.5,
7493 );
7494 assert_eq!(fractional, (645, 1350));
7495 }
7496
7497 #[test]
7498 fn physical_viewport_maps_to_logical_size_with_scale_factor() {
7499 let logical = physical_size_to_layout_size(PhysicalSize::new(1728, 1117), 1.5);
7500 assert_eq!(logical.width, 1152.0);
7501 assert!((logical.height - 744.6667).abs() < 0.001);
7502 }
7503
7504 #[test]
7505 fn scale_factor_change_preserves_logical_viewport_until_resize_arrives() {
7506 let viewport = WindowViewportState {
7507 physical_size: PhysicalSize::new(1600, 1200),
7508 scale_factor: 1.0,
7509 }
7510 .with_scale_factor(2.0);
7511
7512 assert_eq!(viewport.physical_size, PhysicalSize::new(3200, 2400));
7513 assert_eq!(
7514 viewport.logical_size(),
7515 fission_layout::LayoutSize::new(1600.0, 1200.0)
7516 );
7517 }
7518
7519 #[test]
7520 fn resized_event_overrides_scale_factor_prediction_authoritatively() {
7521 let viewport = WindowViewportState {
7522 physical_size: PhysicalSize::new(1600, 1200),
7523 scale_factor: 1.0,
7524 }
7525 .with_scale_factor(1.5)
7526 .with_physical_size(PhysicalSize::new(2412, 1809));
7527
7528 assert_eq!(viewport.physical_size, PhysicalSize::new(2412, 1809));
7529 assert_eq!(
7530 viewport.logical_size(),
7531 fission_layout::LayoutSize::new(1608.0, 1206.0)
7532 );
7533 }
7534
7535 #[test]
7536 fn fractional_logical_viewports_round_up_for_render_targets() {
7537 let physical =
7538 logical_viewport_to_physical_size(fission_layout::LayoutSize::new(430.2, 900.1), 1.5);
7539 assert_eq!(physical, PhysicalSize::new(646, 1351));
7540 }
7541
7542 #[test]
7543 fn scale_factor_prediction_never_undershoots_fractional_viewports() {
7544 let initial = WindowViewportState {
7545 physical_size: PhysicalSize::new(1728, 1117),
7546 scale_factor: 1.5,
7547 };
7548 let predicted = initial.with_scale_factor(2.0);
7549
7550 assert_eq!(predicted.physical_size, PhysicalSize::new(2304, 1490));
7551 assert!(predicted.logical_size().width >= initial.logical_size().width);
7552 assert!(predicted.logical_size().height >= initial.logical_size().height);
7553 }
7554
7555 #[test]
7556 fn logical_resize_updates_native_viewport_prediction() {
7557 let initial = WindowViewportState {
7558 physical_size: PhysicalSize::new(800, 632),
7559 scale_factor: 2.0,
7560 };
7561 let resized = initial.with_logical_size(fission_layout::LayoutSize::new(1600.0, 1200.0));
7562
7563 assert_eq!(resized.physical_size, PhysicalSize::new(3200, 2400));
7564 assert_eq!(
7565 resized.logical_size(),
7566 fission_layout::LayoutSize::new(1600.0, 1200.0)
7567 );
7568 }
7569
7570 #[test]
7571 fn logical_resize_requests_logical_window_dimensions() {
7572 let requested = native_window_size_for_logical_viewport(fission_layout::LayoutSize::new(
7573 1600.0, 2200.0,
7574 ));
7575
7576 assert_eq!(requested.width, 1600.0);
7577 assert_eq!(requested.height, 2200.0);
7578 }
7579
7580 #[test]
7581 fn invalid_scale_factors_fall_back_to_unit_scale() {
7582 assert_eq!(normalize_scale_factor(0.0), 1.0);
7583 assert_eq!(normalize_scale_factor(-2.0), 1.0);
7584 assert_eq!(normalize_scale_factor(f64::NAN), 1.0);
7585 assert_eq!(normalize_scale_factor(f64::INFINITY), 1.0);
7586 assert_eq!(normalize_scale_factor(1.5), 1.5);
7587 }
7588
7589 #[test]
7590 fn invalid_scale_factor_does_not_shrink_viewport_math() {
7591 let logical = physical_size_to_layout_size(PhysicalSize::new(1600, 1200), 0.0);
7592 assert_eq!(logical, fission_layout::LayoutSize::new(1600.0, 1200.0));
7593
7594 let render_target = logical_viewport_to_render_target_size(
7595 fission_layout::LayoutSize::new(1600.0, 1200.0),
7596 0.0,
7597 );
7598 assert_eq!(render_target, (1600, 1200));
7599 }
7600
7601 #[test]
7602 fn surface_resize_resets_custom_target_texture_tracking() {
7603 let mut tracked_target_texture_size = (1600, 1200);
7604
7605 sync_tracked_target_texture_size_to_surface(
7606 &mut tracked_target_texture_size,
7607 PhysicalSize::new(1055, 791),
7608 );
7609
7610 assert_eq!(tracked_target_texture_size, (1055, 791));
7611 assert_ne!(
7612 tracked_target_texture_size,
7613 logical_viewport_to_render_target_size(
7614 fission_layout::LayoutSize::new(1600.0, 1200.0),
7615 1.0,
7616 )
7617 );
7618 }
7619
7620 #[test]
7621 fn resize_settle_signal_tracks_real_resize_state() {
7622 assert!(resize_is_unsettled(true, false, false));
7623 assert!(resize_is_unsettled(false, true, false));
7624 assert!(resize_is_unsettled(false, false, true));
7625 assert!(!resize_is_unsettled(false, false, false));
7626 }
7627
7628 #[test]
7629 fn screenshot_copy_extent_never_exceeds_texture_bounds() {
7630 assert_eq!(
7631 clamp_copy_extent_to_texture(1600, 1200, 1055, 791),
7632 (1055, 791)
7633 );
7634 assert_eq!(clamp_copy_extent_to_texture(0, 0, 1055, 791), (1, 1));
7635 assert_eq!(
7636 clamp_copy_extent_to_texture(640, 480, 1055, 791),
7637 (640, 480)
7638 );
7639 }
7640
7641 #[test]
7642 fn integer_downscale_uses_fast_box_path() {
7643 let rgba = vec![
7644 10, 20, 30, 255, 30, 40, 50, 255, 50, 60, 70, 255, 70, 80, 90, 255,
7645 ];
7646 let downscaled = downscale_rgba_box(&rgba, 2, 2, 1, 1).expect("downscale");
7647 assert_eq!(downscaled, vec![40, 50, 60, 255]);
7648 }
7649
7650 #[test]
7651 fn startup_deep_link_collection_filters_to_declared_config() {
7652 let config = DeepLinkConfig::new()
7653 .scheme("fission")
7654 .domain("example.com")
7655 .path_prefix("/tasks");
7656
7657 let links = collect_startup_deep_links_from(
7658 &config,
7659 vec![
7660 "--ignored".to_string(),
7661 "fission://open/tasks/1".to_string(),
7662 "other://open/tasks/1".to_string(),
7663 ],
7664 vec!["https://example.com/tasks/2?source=email".to_string()],
7665 );
7666
7667 assert_eq!(links.len(), 2);
7668 assert_eq!(links[0].url, "https://example.com/tasks/2?source=email");
7669 assert!(links[0].cold_start);
7670 assert_eq!(links[1].url, "fission://open/tasks/1");
7671 assert!(links[1].cold_start);
7672 }
7673}