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