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