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