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