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