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