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