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