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 .animation
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::env::AnimationStateMap,
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 register_reducer(
3448 &mut self,
3449 action_id: ActionId,
3450 reducer: fission_core::action::Reducer<S>,
3451 ) -> Result<()> {
3452 self.runtime.register_reducer::<S>(action_id, reducer)
3453 }
3454
3455 pub fn absorb_registry(&mut self, registry: fission_core::ActionRegistry<S>) {
3456 self.runtime.absorb_persistent_registry(registry);
3457 }
3458
3459 pub fn run(self) -> Result<()> {
3460 self.run_inner(
3461 #[cfg(target_os = "android")]
3462 None,
3463 )
3464 }
3465
3466 #[cfg(target_os = "android")]
3467 pub fn run_with_android_app(self, android_app: AndroidApp) -> Result<()> {
3468 self.run_inner(Some(android_app))
3469 }
3470
3471 fn run_inner(
3472 mut self,
3473 #[cfg(target_os = "android")] android_app: Option<AndroidApp>,
3474 ) -> Result<()> {
3475 diag::emit(
3476 diag::DiagCategory::Frame,
3477 diag::DiagLevel::Info,
3478 diag::DiagEventKind::FrameStart { root: None },
3479 );
3480 diag::init_from_env();
3481
3482 let background_test_mode = std::env::var_os("FISSION_BACKGROUND_TEST").is_some();
3485 let mut event_loop_builder = EventLoopBuilder::<TestEvent>::with_user_event();
3486 #[cfg(target_os = "android")]
3487 if let Some(app) = android_app.as_ref() {
3488 android_capabilities::register_android_operation_capabilities(
3489 &mut self.async_registry,
3490 app,
3491 );
3492 }
3493 #[cfg(target_os = "android")]
3494 if let Some(app) = android_app {
3495 event_loop_builder.with_android_app(app);
3496 }
3497 #[cfg(target_os = "macos")]
3498 if background_test_mode {
3499 event_loop_builder.with_activation_policy(ActivationPolicy::Accessory);
3500 event_loop_builder.with_activate_ignoring_other_apps(false);
3501 event_loop_builder.with_default_menu(false);
3502 }
3503 let event_loop = event_loop_builder
3504 .build()
3505 .map_err(|e| anyhow::anyhow!("Event loop error: {}", e))?;
3506 let event_proxy = event_loop.create_proxy();
3507 #[cfg(feature = "tray")]
3508 let tray_event_rx = self
3509 .tray_config
3510 .as_ref()
3511 .map(|_| tray::install_event_forwarders(event_proxy.clone()));
3512 #[cfg(feature = "tray")]
3513 let tray_config = self.tray_config.clone();
3514 let window_title = self.title.clone();
3515 let web_mount_selector = self.web_mount_selector;
3516 let ime_handler = Arc::new(DesktopImeHandler::default());
3517 self.runtime = self.runtime.with_ime_handler(ime_handler.clone());
3518
3519 #[cfg(not(target_os = "android"))]
3520 let platform_window = build_window(
3521 &window_title,
3522 background_test_mode,
3523 &event_loop,
3524 web_mount_selector.as_deref(),
3525 )?;
3526 #[cfg(not(target_os = "android"))]
3527 ime_handler.set_window(Some(platform_window.clone()));
3528 #[cfg(target_os = "android")]
3529 let mut platform_window: Option<Arc<Window>> = None;
3530
3531 #[cfg(target_os = "android")]
3534 if std::env::var_os("WGPU_BACKEND").is_none() {
3535 eprintln!("fission-shell-winit: forcing WGPU_BACKEND=gl on Android");
3536 std::env::set_var("WGPU_BACKEND", "gl");
3537 }
3538 #[cfg(not(target_arch = "wasm32"))]
3539 let mut render_cx = RenderContext::new();
3540 #[cfg(not(target_arch = "wasm32"))]
3541 let mut render_state: Option<RenderState<'_>> = None;
3542 #[cfg(target_arch = "wasm32")]
3543 let mut web_renderer: Option<WebRenderer> = None;
3544 #[cfg(target_arch = "wasm32")]
3545 let pending_webgpu_init: PendingWebGpuInit = Rc::new(RefCell::new(None));
3546 #[cfg(target_arch = "wasm32")]
3547 let mut webgpu_init_in_flight = false;
3548 #[cfg(target_arch = "wasm32")]
3549 let mut web_renderer_reported = false;
3550 #[cfg(not(target_arch = "wasm32"))]
3551 let mut scene = Scene::new();
3552 #[cfg(not(target_arch = "wasm32"))]
3553 let mut retained_scene_cache = RetainedSceneCache::default();
3554
3555 #[cfg(not(target_os = "android"))]
3556 platform_window.request_redraw();
3557
3558 let mut startup_deep_links = self.startup_deep_links.clone();
3559 startup_deep_links.extend(collect_startup_deep_links(&self.deep_link_config));
3560 let startup_notification_responses = self.startup_notification_responses.clone();
3561
3562 let mut runtime = self.runtime;
3563 for link in startup_deep_links {
3564 runtime.dispatch(DeepLinkReceived { link }.into(), WidgetId::from_u128(0))?;
3565 }
3566 for response in startup_notification_responses {
3567 runtime.dispatch(
3568 NotificationResponseReceived { response }.into(),
3569 WidgetId::from_u128(0),
3570 )?;
3571 }
3572 let mut layout_engine = self.layout_engine;
3573 let root_widget = self.root_widget;
3574 let mut env = self.env;
3575 env.window.title = fission_core::WindowTitle::plain(window_title.clone());
3576 let mut applied_window_title = window_title.clone();
3577 let mut pipeline = self.pipeline;
3578 let measurer = self.measurer;
3579 let effect_result_tx = self.effect_result_tx;
3580 let effect_result_rx = self.effect_result_rx;
3581 let async_registry = self.async_registry;
3582 let startup_action = self.startup_action;
3583 let mut startup_dispatched = false;
3584 let mut next_service_instance_id = 1_u64;
3585 let mut active_services: HashMap<ServiceKey, ActiveServiceHandle> = HashMap::new();
3586 let mut service_bindings: HashMap<ServiceBindingKey, ServiceBindings> = HashMap::new();
3587
3588 #[cfg(target_os = "macos")]
3589 let video_backend: Arc<dyn VideoBackend> = Arc::new(MacVideoBackend::new(&platform_window));
3590 #[cfg(not(target_os = "macos"))]
3591 let video_backend: Arc<dyn VideoBackend> = Arc::new(MockVideoBackend::new());
3592 #[cfg(target_os = "macos")]
3593 let web_backend = MacWebBackend::new(&platform_window);
3594 #[cfg(not(target_os = "macos"))]
3595 let web_backend = MockWebBackend::new();
3596 let mut players: HashMap<WidgetId, ActivePlayer> = HashMap::new();
3597
3598 let mut last_cursor_position: Option<PhysicalPosition<f64>> = None;
3599 let mut active_primary_touch: Option<u64> = None;
3600 let mut touch_positions: HashMap<u64, PhysicalPosition<f64>> = HashMap::new();
3601 let max_fps = std::env::var("FISSION_MAX_FPS")
3602 .ok()
3603 .and_then(|v| v.parse::<u32>().ok())
3604 .filter(|v| *v > 0)
3605 .unwrap_or(60);
3606 let min_frame = Duration::from_secs_f32(1.0 / max_fps as f32);
3607 let repeat_animation_fps = std::env::var("FISSION_REPEAT_ANIMATION_FPS")
3608 .ok()
3609 .and_then(|v| v.parse::<u32>().ok())
3610 .filter(|v| *v > 0)
3611 .map(|v| v.min(max_fps))
3612 .unwrap_or(10);
3613 let repeat_animation_frame = Duration::from_secs_f32(1.0 / repeat_animation_fps as f32);
3614 let resize_fps = std::env::var("FISSION_RESIZE_FPS")
3615 .ok()
3616 .and_then(|v| v.parse::<u32>().ok())
3617 .filter(|v| *v > 0)
3618 .map(|v| v.min(max_fps))
3619 .unwrap_or(60);
3620 let resize_frame = Duration::from_secs_f32(1.0 / resize_fps as f32);
3621 let resize_settle_delay = Duration::from_millis(
3622 std::env::var("FISSION_RESIZE_SETTLE_MS")
3623 .ok()
3624 .and_then(|v| v.parse::<u64>().ok())
3625 .filter(|v| *v > 0)
3626 .unwrap_or(90),
3627 );
3628 let mut last_redraw_at = Instant::now()
3629 .checked_sub(min_frame)
3630 .unwrap_or_else(Instant::now);
3631 let mut redraw_pending = false;
3632 let mut last_frame_time = Instant::now();
3633 let blink_enabled = std::env::var("FISSION_TEXTINPUT_BLINK")
3634 .map(|v| !matches!(v.to_ascii_lowercase().as_str(), "0" | "false" | "no"))
3635 .unwrap_or(true);
3636 let blink_period = Duration::from_millis(
3637 std::env::var("FISSION_TEXTINPUT_BLINK_MS")
3638 .ok()
3639 .and_then(|v| v.parse::<u64>().ok())
3640 .filter(|v| *v > 0)
3641 .unwrap_or(530),
3642 );
3643 let mut last_blink_toggle = Instant::now();
3644 let mut blink_focus_id: Option<WidgetId> = None;
3645 let text_trace_enabled = std::env::var("FISSION_TEXT_TRACE")
3646 .map(|v| matches!(v.to_ascii_lowercase().as_str(), "1" | "true" | "yes"))
3647 .unwrap_or(false);
3648 let mut frame_trace = FrameTraceState::new(frame_trace_enabled());
3649 let mut presented_frames: u64 = 0;
3650 let mut next_text_trace_seq: u64 = 0;
3651 let mut pending_text_traces: VecDeque<PendingTextTrace> = VecDeque::new();
3652 #[cfg(target_arch = "wasm32")]
3653 let mut pending_web_input_at: Option<Instant> = None;
3654 let mut current_mods: u8 = 0;
3655
3656 #[cfg(not(target_arch = "wasm32"))]
3661 let test_control_port = self.test_control_port.or_else(|| {
3662 std::env::var("FISSION_TEST_CONTROL_PORT")
3663 .ok()
3664 .and_then(|v| v.parse::<u16>().ok())
3665 });
3666 #[cfg(all(target_os = "android", not(target_arch = "wasm32")))]
3667 let pending_test_events = test_control::create_pending_event_queue();
3668 #[cfg(not(target_arch = "wasm32"))]
3669 let test_control_enabled = test_control_port
3670 .map(|port| {
3671 #[cfg(target_os = "android")]
3672 let injector = test_control::EventInjector::Queue {
3673 queue: pending_test_events.clone(),
3674 wake_proxy: Some(event_proxy.clone()),
3675 };
3676 #[cfg(not(target_os = "android"))]
3677 let injector = test_control::EventInjector::Proxy(event_proxy.clone());
3678 test_control::spawn_server(port, injector);
3679 true
3680 })
3681 .unwrap_or(false);
3682 #[cfg(target_arch = "wasm32")]
3683 let test_control_enabled = false;
3684 #[cfg(not(target_os = "android"))]
3685 let _ = test_control_enabled;
3686 let mut pending_screenshot_path: Option<String> = None;
3688 let mut pending_screenshot_response_tx: Option<test_control::ResponseSender> = None;
3689 #[cfg(not(target_os = "android"))]
3690 let mut window_viewport = WindowViewportState::from_window(&platform_window);
3691 #[cfg(target_os = "android")]
3692 let mut window_viewport: Option<WindowViewportState> = None;
3693 #[cfg(not(target_os = "android"))]
3694 let mut pending_resize = Some(window_viewport);
3695 #[cfg(target_os = "android")]
3696 let mut pending_resize = None;
3697 let mut resize_needs_settled_frame = pending_resize.is_some();
3698 let mut pending_capture_settle = false;
3699 let mut last_built_viewport: Option<LayoutSize> = None;
3700 let mut live_resize = LiveResizeController::new(resize_settle_delay);
3701 #[cfg(feature = "tray")]
3702 let mut active_tray: Option<tray::ActiveTray<S>> = None;
3703 let mut invalidations = InvalidationSet {
3704 build: true,
3705 layout: true,
3706 paint: true,
3707 composite: true,
3708 };
3709 let mut vello_image_cache_generation = fission_render_vello::image_cache_generation();
3710 let mut software_image_cache_generation = software_renderer::image_cache_generation();
3711
3712 let event_handler =
3713 move |event: Event<TestEvent>, elwt: &EventLoopWindowTarget<TestEvent>| {
3714 elwt.set_control_flow(ControlFlow::Wait);
3715 let debug_android_events = cfg!(target_os = "android")
3716 && std::env::var_os("FISSION_DEBUG_ANDROID_EVENTS").is_some();
3717
3718 let mut handle_test_event = |test_event: TestEvent| {
3719 if debug_android_events {
3720 eprintln!("[android-events] user_event={test_event:?}");
3721 }
3722 match test_event {
3723 TestEvent::MouseMove { x, y } => {
3724 let Some(window) = platform_window.active_window() else {
3725 return;
3726 };
3727 let scale_factor = window.scale_factor();
3728 last_cursor_position = Some(PhysicalPosition::new(
3729 (x as f64) * scale_factor,
3730 (y as f64) * scale_factor,
3731 ));
3732 handle_cursor_moved(
3733 x,
3734 y,
3735 0,
3736 &mut runtime,
3737 &pipeline,
3738 &effect_result_tx,
3739 &event_proxy,
3740 &async_registry,
3741 &mut active_services,
3742 &mut service_bindings,
3743 &mut next_service_instance_id,
3744 window,
3745 elwt,
3746 &mut last_redraw_at,
3747 min_frame,
3748 &mut redraw_pending,
3749 &mut frame_trace,
3750 &mut invalidations,
3751 );
3752 }
3753 TestEvent::MouseDown { x, y, button } => {
3754 let Some(window) = platform_window.active_window() else {
3755 return;
3756 };
3757 let btn = map_test_button(button);
3758 handle_mouse_button(
3759 x,
3760 y,
3761 btn,
3762 true,
3763 0,
3764 &mut runtime,
3765 &pipeline,
3766 &effect_result_tx,
3767 &event_proxy,
3768 &async_registry,
3769 &mut active_services,
3770 &mut service_bindings,
3771 &mut next_service_instance_id,
3772 window,
3773 elwt,
3774 &mut last_redraw_at,
3775 min_frame,
3776 &mut redraw_pending,
3777 text_trace_enabled,
3778 &mut pending_text_traces,
3779 &mut next_text_trace_seq,
3780 presented_frames,
3781 &mut last_blink_toggle,
3782 &mut frame_trace,
3783 &mut invalidations,
3784 );
3785 }
3786 TestEvent::MouseUp { x, y, button } => {
3787 let Some(window) = platform_window.active_window() else {
3788 return;
3789 };
3790 let btn = map_test_button(button);
3791 handle_mouse_button(
3792 x,
3793 y,
3794 btn,
3795 false,
3796 0,
3797 &mut runtime,
3798 &pipeline,
3799 &effect_result_tx,
3800 &event_proxy,
3801 &async_registry,
3802 &mut active_services,
3803 &mut service_bindings,
3804 &mut next_service_instance_id,
3805 window,
3806 elwt,
3807 &mut last_redraw_at,
3808 min_frame,
3809 &mut redraw_pending,
3810 text_trace_enabled,
3811 &mut pending_text_traces,
3812 &mut next_text_trace_seq,
3813 presented_frames,
3814 &mut last_blink_toggle,
3815 &mut frame_trace,
3816 &mut invalidations,
3817 );
3818 }
3819 TestEvent::KeyDown {
3820 key_code,
3821 modifiers,
3822 } => {
3823 let Some(window) = platform_window.active_window() else {
3824 return;
3825 };
3826 let code = parse_key_code(&key_code);
3827 handle_key_down::<S>(
3828 code,
3829 modifiers,
3830 &mut runtime,
3831 &pipeline,
3832 &effect_result_tx,
3833 &event_proxy,
3834 &async_registry,
3835 &mut active_services,
3836 &mut service_bindings,
3837 &mut next_service_instance_id,
3838 window,
3839 elwt,
3840 &mut last_redraw_at,
3841 min_frame,
3842 &mut redraw_pending,
3843 text_trace_enabled,
3844 &mut pending_text_traces,
3845 &mut next_text_trace_seq,
3846 presented_frames,
3847 &mut last_blink_toggle,
3848 self.key_handler.as_ref(),
3849 &mut frame_trace,
3850 &mut invalidations,
3851 );
3852 }
3853 TestEvent::KeyUp { .. } => {
3854 let Some(window) = platform_window.active_window() else {
3855 return;
3856 };
3857 request_redraw_logged(
3858 window,
3859 elwt,
3860 &mut last_redraw_at,
3861 min_frame,
3862 &mut redraw_pending,
3863 &mut frame_trace,
3864 "test_key_up",
3865 );
3866 }
3867 TestEvent::TextInput { text } => {
3868 let Some(window) = platform_window.active_window() else {
3869 return;
3870 };
3871 if let (Some(ir), Some(layout)) =
3872 (&pipeline.prev_ir, &pipeline.last_snapshot)
3873 {
3874 let target =
3875 focused_text_input_id(&runtime, pipeline.prev_ir.as_ref());
3876 if target.is_some()
3877 || focused_custom_text_input(
3878 &runtime,
3879 pipeline.prev_ir.as_ref(),
3880 )
3881 {
3882 let trace_seq = start_text_trace(
3883 text_trace_enabled && target.is_some(),
3884 &mut pending_text_traces,
3885 &mut next_text_trace_seq,
3886 format!("test_text_input:{}", text.chars().count()),
3887 target,
3888 presented_frames,
3889 );
3890 runtime
3891 .handle_input(
3892 InputEvent::Ime(
3893 fission_core::event::ImeEvent::Commit {
3894 text: text.clone(),
3895 },
3896 ),
3897 ir,
3898 layout,
3899 )
3900 .ok();
3901 invalidations.mark_build();
3902 mark_text_trace_handled(&mut pending_text_traces, trace_seq);
3903 if process_pending_effects(
3904 &mut runtime,
3905 &effect_result_tx,
3906 &event_proxy,
3907 &async_registry,
3908 &mut active_services,
3909 &mut service_bindings,
3910 &mut next_service_instance_id,
3911 ) {
3912 mark_text_trace_effects(
3913 &mut pending_text_traces,
3914 trace_seq,
3915 );
3916 invalidations.mark_build();
3917 }
3918 request_redraw_logged(
3919 window,
3920 elwt,
3921 &mut last_redraw_at,
3922 min_frame,
3923 &mut redraw_pending,
3924 &mut frame_trace,
3925 "test_text_input",
3926 );
3927 } else {
3928 for ch in text.chars() {
3929 let key = if ch == ' ' {
3930 KeyCode::Space
3931 } else if ch == '\n' {
3932 KeyCode::Enter
3933 } else {
3934 KeyCode::Char(ch)
3935 };
3936 handle_key_down::<S>(
3937 key,
3938 0,
3939 &mut runtime,
3940 &pipeline,
3941 &effect_result_tx,
3942 &event_proxy,
3943 &async_registry,
3944 &mut active_services,
3945 &mut service_bindings,
3946 &mut next_service_instance_id,
3947 window,
3948 elwt,
3949 &mut last_redraw_at,
3950 min_frame,
3951 &mut redraw_pending,
3952 text_trace_enabled,
3953 &mut pending_text_traces,
3954 &mut next_text_trace_seq,
3955 presented_frames,
3956 &mut last_blink_toggle,
3957 self.key_handler.as_ref(),
3958 &mut frame_trace,
3959 &mut invalidations,
3960 );
3961 }
3962 }
3963 }
3964 }
3965 TestEvent::Scroll { x, y, dx, dy } => {
3966 let Some(window) = platform_window.active_window() else {
3967 return;
3968 };
3969 handle_scroll(
3970 x,
3971 y,
3972 dx,
3973 dy,
3974 0,
3975 &mut runtime,
3976 &pipeline,
3977 &effect_result_tx,
3978 &event_proxy,
3979 &async_registry,
3980 &mut active_services,
3981 &mut service_bindings,
3982 &mut next_service_instance_id,
3983 window,
3984 elwt,
3985 &mut last_redraw_at,
3986 min_frame,
3987 &mut redraw_pending,
3988 &mut frame_trace,
3989 &mut invalidations,
3990 );
3991 }
3992 TestEvent::Resize { width, height } => {
3993 let Some(window) = platform_window.active_window() else {
3994 return;
3995 };
3996 if width > 0 && height > 0 {
3997 let requested_logical_size =
3998 LayoutSize::new(width as f32, height as f32);
3999 let current_viewport = pending_resize
4000 .unwrap_or_else(|| WindowViewportState::from_window(window))
4001 .with_logical_size(requested_logical_size);
4002 #[cfg(not(any(target_os = "android", target_os = "ios")))]
4003 {
4004 let _ = window.request_inner_size(
4005 native_window_size_for_logical_viewport(
4006 requested_logical_size,
4007 ),
4008 );
4009 }
4010 #[cfg(not(target_os = "android"))]
4011 {
4012 window_viewport = current_viewport;
4013 }
4014 #[cfg(target_os = "android")]
4015 {
4016 window_viewport = Some(current_viewport);
4017 }
4018 apply_authoritative_resize(
4019 window,
4020 elwt,
4021 current_viewport,
4022 &mut pending_resize,
4023 &mut resize_needs_settled_frame,
4024 &mut pending_capture_settle,
4025 pending_screenshot_path.as_deref(),
4026 &mut live_resize,
4027 &mut invalidations,
4028 &mut last_redraw_at,
4029 resize_frame,
4030 &mut redraw_pending,
4031 &mut frame_trace,
4032 "test_resize",
4033 );
4034 }
4035 }
4036 TestEvent::TapText { text, response_tx } => {
4037 let Some(window) = platform_window.active_window() else {
4038 let _ =
4039 response_tx.send(fission_test_driver::TestResponse::Error {
4040 message: "window not ready".into(),
4041 });
4042 return;
4043 };
4044 let resp = handle_tap_text(&text, &mut runtime, &pipeline);
4045 if matches!(resp, fission_test_driver::TestResponse::Ok { .. }) {
4046 invalidations.mark_build();
4047 if process_pending_effects(
4048 &mut runtime,
4049 &effect_result_tx,
4050 &event_proxy,
4051 &async_registry,
4052 &mut active_services,
4053 &mut service_bindings,
4054 &mut next_service_instance_id,
4055 ) {
4056 invalidations.mark_build();
4057 }
4058 }
4059 let _ = response_tx.send(resp);
4060 request_redraw_logged(
4061 window,
4062 elwt,
4063 &mut last_redraw_at,
4064 min_frame,
4065 &mut redraw_pending,
4066 &mut frame_trace,
4067 "test_tap_text",
4068 );
4069 }
4070 TestEvent::Screenshot { path, response_tx } => {
4071 let Some(window) = platform_window.active_window() else {
4072 let _ =
4073 response_tx.send(fission_test_driver::TestResponse::Error {
4074 message: "window not ready".into(),
4075 });
4076 return;
4077 };
4078 pending_screenshot_path = Some(path);
4079 pending_screenshot_response_tx = Some(response_tx);
4080 pending_capture_settle = resize_is_unsettled(
4081 pending_resize.is_some(),
4082 resize_needs_settled_frame,
4083 live_resize.is_live(Instant::now()),
4084 );
4085 window.request_redraw();
4086 }
4087 TestEvent::CaptureScreenshot { response_tx } => {
4088 let Some(window) = platform_window.active_window() else {
4089 let _ =
4090 response_tx.send(fission_test_driver::TestResponse::Error {
4091 message: "window not ready".into(),
4092 });
4093 return;
4094 };
4095 pending_screenshot_path = Some("__capture__".into());
4096 pending_screenshot_response_tx = Some(response_tx);
4097 pending_capture_settle = resize_is_unsettled(
4098 pending_resize.is_some(),
4099 resize_needs_settled_frame,
4100 live_resize.is_live(Instant::now()),
4101 );
4102 window.request_redraw();
4103 }
4104 TestEvent::GetText { response_tx } => {
4105 let resp =
4106 build_get_text_response(&pipeline, &runtime.runtime_state.scroll);
4107 let _ = response_tx.send(resp);
4108 }
4109 TestEvent::GetTree { response_tx } => {
4110 let resp =
4111 build_get_tree_response(&pipeline, &runtime.runtime_state.scroll);
4112 let _ = response_tx.send(resp);
4113 }
4114 TestEvent::Pump { response_tx } => {
4115 let Some(window) = platform_window.active_window() else {
4116 let _ =
4117 response_tx.send(fission_test_driver::TestResponse::Error {
4118 message: "window not ready".into(),
4119 });
4120 return;
4121 };
4122 pending_screenshot_path = Some("__pump__".into());
4123 pending_screenshot_response_tx = Some(response_tx);
4124 pending_capture_settle = resize_is_unsettled(
4125 pending_resize.is_some(),
4126 resize_needs_settled_frame,
4127 live_resize.is_live(Instant::now()),
4128 );
4129 window.request_redraw();
4130 }
4131 TestEvent::Wake => {
4132 if let Some(window) = platform_window.active_window() {
4133 request_redraw_logged(
4134 window,
4135 elwt,
4136 &mut last_redraw_at,
4137 min_frame,
4138 &mut redraw_pending,
4139 &mut frame_trace,
4140 "wake",
4141 );
4142 }
4143 }
4144 TestEvent::Wait { ms: _, response_tx } => {
4145 let _ = response_tx.send(fission_test_driver::TestResponse::Ok {});
4146 }
4147 TestEvent::Quit => {
4148 elwt.exit();
4149 }
4150 }
4151 };
4152
4153 #[cfg(target_os = "android")]
4154 let mut drain_pending_test_events = || loop {
4155 let pending = {
4156 let mut pending = pending_test_events
4157 .lock()
4158 .expect("pending test events lock poisoned");
4159 pending.pop_front()
4160 };
4161 let Some(test_event) = pending else {
4162 break;
4163 };
4164 if debug_android_events {
4165 eprintln!("[android-debug] draining_test_queue");
4166 }
4167 handle_test_event(test_event);
4168 };
4169
4170 match event {
4171 #[cfg(feature = "tray")]
4172 Event::NewEvents(StartCause::Init) => {
4173 if active_tray.is_none() {
4174 if let Some(config) = tray_config.clone() {
4175 match tray::ActiveTray::build(config) {
4176 Ok(tray) => {
4177 active_tray = Some(tray);
4178 }
4179 Err(error) => {
4180 eprintln!("Fission tray setup error: {error:?}");
4181 }
4182 }
4183 }
4184 }
4185 }
4186 Event::Resumed => {
4187 if debug_android_events {
4188 eprintln!("[android-events] resumed");
4189 }
4190 #[cfg(target_os = "android")]
4191 if platform_window.is_none() {
4192 match build_window(
4193 &window_title,
4194 background_test_mode,
4195 elwt,
4196 web_mount_selector.as_deref(),
4197 ) {
4198 Ok(new_window) => {
4199 ime_handler.set_window(Some(new_window.clone()));
4200 sync_window_cursor(&new_window, &runtime);
4201 platform_window = Some(new_window);
4202 }
4203 Err(err) => {
4204 eprintln!("window build error: {err}");
4205 elwt.exit();
4206 return;
4207 }
4208 }
4209 }
4210 let Some(window) = platform_window.active_window() else {
4211 return;
4212 };
4213 let current_viewport = WindowViewportState::from_window(window);
4214 #[cfg(not(target_os = "android"))]
4215 {
4216 window_viewport = current_viewport;
4217 }
4218 #[cfg(target_os = "android")]
4219 {
4220 window_viewport = Some(current_viewport);
4221 }
4222 #[cfg(not(target_arch = "wasm32"))]
4223 if render_state.is_none()
4224 && current_viewport.physical_size.width > 0
4225 && current_viewport.physical_size.height > 0
4226 {
4227 if let Some(render_window) = platform_window.active_window_arc() {
4228 match create_render_state(
4229 &mut render_cx,
4230 render_window,
4231 current_viewport,
4232 ) {
4233 Ok(mut state) => {
4234 if let Err(err) = present_startup_clear_frame(
4235 &mut state,
4236 &render_cx,
4237 theme_background_wgpu_color(&env),
4238 ) {
4239 eprintln!("startup clear frame failed: {err}");
4240 }
4241 render_state = Some(state);
4242 }
4243 Err(err) => {
4244 eprintln!("render surface not ready on resume: {err}");
4245 }
4246 }
4247 }
4248 }
4249 pending_resize = Some(current_viewport);
4250 resize_needs_settled_frame = true;
4251 if pending_screenshot_path.is_some() {
4252 pending_capture_settle = true;
4253 }
4254 invalidations.mark_composite();
4255 request_redraw_logged(
4256 window,
4257 elwt,
4258 &mut last_redraw_at,
4259 min_frame,
4260 &mut redraw_pending,
4261 &mut frame_trace,
4262 "app_resumed",
4263 );
4264 }
4265 Event::Suspended => {
4266 #[cfg(not(target_arch = "wasm32"))]
4267 {
4268 render_state = None;
4269 }
4270 #[cfg(target_arch = "wasm32")]
4271 {
4272 web_renderer = None;
4273 webgpu_init_in_flight = false;
4274 *pending_webgpu_init.borrow_mut() = None;
4275 web_renderer_reported = false;
4276 }
4277 #[cfg(target_os = "android")]
4278 {
4279 ime_handler.set_window(None);
4280 platform_window = None;
4281 window_viewport = None;
4282 pending_resize = None;
4283 resize_needs_settled_frame = false;
4284 pending_capture_settle = false;
4285 last_built_viewport = None;
4286 last_cursor_position = None;
4287 active_primary_touch = None;
4288 touch_positions.clear();
4289 }
4290 }
4291 Event::UserEvent(test_event) => {
4295 #[cfg(target_os = "android")]
4296 if matches!(test_event, TestEvent::Wake) {
4297 if debug_android_events {
4298 eprintln!("[android-debug] wake_received");
4299 }
4300 drain_pending_test_events();
4301 return;
4302 }
4303 handle_test_event(test_event)
4304 }
4305
4306 Event::AboutToWait => {
4310 let Some(window) = platform_window.active_window() else {
4311 elwt.set_control_flow(ControlFlow::Wait);
4312 return;
4313 };
4314 #[cfg(target_os = "android")]
4315 drain_pending_test_events();
4316 #[cfg(feature = "tray")]
4317 if let (Some(rx), Some(active)) =
4318 (tray_event_rx.as_ref(), active_tray.as_ref())
4319 {
4320 while let Ok(event) = rx.try_recv() {
4321 match active.handle_event(event, window, &mut runtime) {
4322 Ok(outcome) => {
4323 if outcome.quit {
4324 elwt.exit();
4325 return;
4326 }
4327 if outcome.redraw {
4328 invalidations.mark_build();
4329 if process_pending_effects(
4330 &mut runtime,
4331 &effect_result_tx,
4332 &event_proxy,
4333 &async_registry,
4334 &mut active_services,
4335 &mut service_bindings,
4336 &mut next_service_instance_id,
4337 ) {
4338 invalidations.mark_build();
4339 }
4340 request_redraw_logged(
4341 window,
4342 elwt,
4343 &mut last_redraw_at,
4344 min_frame,
4345 &mut redraw_pending,
4346 &mut frame_trace,
4347 "tray_menu_action",
4348 );
4349 }
4350 }
4351 Err(error) => {
4352 eprintln!("Fission tray event error: {error:?}");
4353 }
4354 }
4355 }
4356 }
4357 let now = Instant::now();
4358
4359 let mut surfaces = pipeline.video_surfaces.clone();
4361 let mut active_nodes = std::collections::HashSet::new();
4362
4363 for surface in &mut surfaces {
4364 active_nodes.insert(surface.widget_id);
4365
4366 if !players.contains_key(&surface.widget_id) {
4368 if let Some(state) =
4369 runtime.runtime_state.video.states.get(&surface.widget_id)
4370 {
4371 let source = &state.asset_source;
4372 if !source.is_empty() {
4373 let player = video_backend.create_player(source);
4374 surface.surface_id = player.surface_id();
4375 if let Some(state) = runtime
4376 .runtime_state
4377 .video
4378 .states
4379 .get_mut(&surface.widget_id)
4380 {
4381 state.surface_id = Some(surface.surface_id);
4382 }
4383 players.insert(
4384 surface.widget_id,
4385 ActivePlayer {
4386 player,
4387 last_status: None,
4388 last_rate: None,
4389 last_volume: None,
4390 last_muted: None,
4391 },
4392 );
4393 }
4394 }
4395 } else if let Some(active_player) = players.get(&surface.widget_id) {
4396 surface.surface_id = active_player.player.surface_id();
4397 }
4398 }
4399
4400 players.retain(|id, _| active_nodes.contains(id));
4402
4403 video_backend.present_surfaces(&surfaces);
4405 let web_surfaces = pipeline.web_surfaces.clone();
4406 web_backend.present_surfaces(&web_surfaces);
4407
4408 for (widget_id, active_player) in players.iter_mut() {
4410 if let Some(video_state) =
4411 runtime.runtime_state.video.states.get_mut(widget_id)
4412 {
4413 let player = &mut active_player.player;
4414
4415 if active_player.last_status != Some(video_state.status) {
4417 match video_state.status {
4418 VideoStatus::Playing => player.play(),
4419 VideoStatus::Paused => player.pause(),
4420 VideoStatus::Stopped => player.stop(),
4421 _ => {}
4422 }
4423 active_player.last_status = Some(video_state.status);
4424 }
4425
4426 for event in player.poll_events() {
4428 match event {
4429 VideoEvent::Ready { duration } => {
4430 video_state.duration_ms = Some(duration);
4431 if video_state.status == VideoStatus::Playing {
4432 player.play();
4433 }
4434 }
4435 VideoEvent::Ended => {
4436 video_state.status = VideoStatus::Ended;
4437 active_player.last_status = Some(VideoStatus::Ended);
4438 request_redraw_logged(
4439 &window,
4440 elwt,
4441 &mut last_redraw_at,
4442 min_frame,
4443 &mut redraw_pending,
4444 &mut frame_trace,
4445 "video_ended",
4446 );
4447 }
4448 VideoEvent::Error(e) => {
4449 eprintln!(
4450 "Video playback error for {:?}: {:?}",
4451 widget_id, e
4452 );
4453 video_state.status = VideoStatus::Error;
4454 active_player.last_status = Some(VideoStatus::Error);
4455 request_redraw_logged(
4456 &window,
4457 elwt,
4458 &mut last_redraw_at,
4459 min_frame,
4460 &mut redraw_pending,
4461 &mut frame_trace,
4462 "video_error",
4463 );
4464 }
4465 }
4466 }
4467 video_state.position_ms = player.position();
4469
4470 if active_player.last_rate != Some(video_state.rate) {
4471 player.set_rate(video_state.rate);
4472 active_player.last_rate = Some(video_state.rate);
4473 }
4474 if active_player.last_volume != Some(video_state.volume) {
4475 player.set_volume(video_state.volume);
4476 active_player.last_volume = Some(video_state.volume);
4477 }
4478 if active_player.last_muted != Some(video_state.muted) {
4479 player.set_muted(video_state.muted);
4480 active_player.last_muted = Some(video_state.muted);
4481 }
4482
4483 if let Some(seek_pos) = video_state.pending_seek.take() {
4484 player.seek_to(seek_pos);
4485 }
4486 }
4487 }
4488
4489 let has_finite_animation = runtime
4490 .runtime_state
4491 .animation
4492 .active
4493 .values()
4494 .any(|anim| !anim.repeat);
4495 let resize_unsettled = resize_is_unsettled(
4496 pending_resize.is_some(),
4497 resize_needs_settled_frame,
4498 live_resize.is_live(now),
4499 );
4500 let repeat_animation_interval =
4501 if resize_unsettled || pending_capture_settle {
4502 None
4503 } else {
4504 repeating_animation_redraw_interval(
4505 &runtime.runtime_state.animation,
4506 repeat_animation_frame,
4507 )
4508 };
4509 let has_playing_video = players.iter().any(|(widget_id, _)| {
4510 runtime
4511 .runtime_state
4512 .video
4513 .states
4514 .get(widget_id)
4515 .map(|state| state.status == VideoStatus::Playing)
4516 .unwrap_or(false)
4517 });
4518 let animation_frame = animation_redraw_interval(
4519 has_finite_animation,
4520 repeat_animation_interval,
4521 has_playing_video,
4522 min_frame,
4523 );
4524
4525 ime_handler.set_text_input_config(focused_text_input_config(
4526 &runtime,
4527 pipeline.prev_ir.as_ref(),
4528 ));
4529 let focused_text_input =
4530 focused_text_input_id(&runtime, pipeline.prev_ir.as_ref());
4531 if focused_text_input != blink_focus_id {
4532 if let Some(prev) = blink_focus_id {
4533 runtime.runtime_state.caret_visible.remove(&prev);
4534 }
4535 blink_focus_id = focused_text_input;
4536 if let Some(id) = blink_focus_id {
4537 runtime.runtime_state.caret_visible.insert(id, true);
4538 last_blink_toggle = now;
4539 invalidations.mark_build();
4540 request_redraw_logged(
4541 &window,
4542 elwt,
4543 &mut last_redraw_at,
4544 min_frame,
4545 &mut redraw_pending,
4546 &mut frame_trace,
4547 "caret_focus_changed",
4548 );
4549 }
4550 }
4551
4552 if blink_enabled {
4554 if let Some(id) = blink_focus_id {
4555 if now.duration_since(last_blink_toggle) >= blink_period {
4556 let visible = runtime
4557 .runtime_state
4558 .caret_visible
4559 .get(&id)
4560 .copied()
4561 .unwrap_or(true);
4562 runtime.runtime_state.caret_visible.insert(id, !visible);
4563 last_blink_toggle = now;
4564 invalidations.mark_build();
4565 request_redraw_logged(
4566 &window,
4567 elwt,
4568 &mut last_redraw_at,
4569 min_frame,
4570 &mut redraw_pending,
4571 &mut frame_trace,
4572 "caret_blink",
4573 );
4574 }
4575 }
4576 }
4577
4578 let blink_wake_at = if blink_enabled && blink_focus_id.is_some() {
4579 Some(last_blink_toggle + blink_period)
4580 } else {
4581 None
4582 };
4583
4584 let effect_results_dispatched = drain_effect_results(
4587 &mut runtime,
4588 &effect_result_rx,
4589 &mut active_services,
4590 &mut service_bindings,
4591 );
4592 if effect_results_dispatched {
4593 invalidations.mark_build();
4594 if process_pending_effects(
4597 &mut runtime,
4598 &effect_result_tx,
4599 &event_proxy,
4600 &async_registry,
4601 &mut active_services,
4602 &mut service_bindings,
4603 &mut next_service_instance_id,
4604 ) {
4605 invalidations.mark_build();
4606 request_redraw_logged(
4607 &window,
4608 elwt,
4609 &mut last_redraw_at,
4610 min_frame,
4611 &mut redraw_pending,
4612 &mut frame_trace,
4613 "effect_continuation",
4614 );
4615 }
4616 request_redraw_logged(
4617 &window,
4618 elwt,
4619 &mut last_redraw_at,
4620 min_frame,
4621 &mut redraw_pending,
4622 &mut frame_trace,
4623 "effect_result",
4624 );
4625 }
4626
4627 let frame_hook_wants_redraw = if let Some(ref hook) = self.frame_hook {
4629 let hook = hook.clone();
4630 if let Some(state) = runtime.get_global_state_mut::<S>() {
4631 hook(state)
4632 } else {
4633 false
4634 }
4635 } else {
4636 false
4637 };
4638 if frame_hook_wants_redraw {
4639 invalidations.mark_build();
4640 request_redraw_logged(
4641 &window,
4642 elwt,
4643 &mut last_redraw_at,
4644 min_frame,
4645 &mut redraw_pending,
4646 &mut frame_trace,
4647 "frame_hook",
4648 );
4649 }
4650
4651 let next_vello_image_generation =
4652 fission_render_vello::image_cache_generation();
4653 let next_software_image_generation =
4654 software_renderer::image_cache_generation();
4655 let image_cache_changed = next_vello_image_generation
4656 != vello_image_cache_generation
4657 || next_software_image_generation != software_image_cache_generation;
4658 if image_cache_changed {
4659 vello_image_cache_generation = next_vello_image_generation;
4660 software_image_cache_generation = next_software_image_generation;
4661 #[cfg(not(target_arch = "wasm32"))]
4662 retained_scene_cache.clear();
4663 #[cfg(target_arch = "wasm32")]
4664 if let Some(WebRenderer::WebGpu(presenter)) = web_renderer.as_mut() {
4665 presenter.retained_scene_cache.clear();
4666 }
4667 invalidations.mark_paint();
4668 request_redraw_logged(
4669 &window,
4670 elwt,
4671 &mut last_redraw_at,
4672 min_frame,
4673 &mut redraw_pending,
4674 &mut frame_trace,
4675 "image_cache",
4676 );
4677 }
4678 let image_cache_pending = fission_render_vello::image_cache_has_pending()
4679 || software_renderer::image_cache_has_pending();
4680
4681 let frame_hook_wake_at = if self.frame_hook.is_some() {
4686 Some(now + Duration::from_secs(2))
4687 } else {
4688 None
4689 };
4690
4691 let has_pending_work = effect_results_dispatched
4692 || frame_hook_wants_redraw
4693 || image_cache_changed
4694 || invalidations.any()
4695 || resize_unsettled
4696 || pending_capture_settle;
4697 let active_keys = active_animation_keys(&runtime);
4698
4699 if has_pending_work {
4700 let pending_frame = pending_work_redraw_interval(
4701 invalidations,
4702 resize_unsettled || pending_capture_settle,
4703 min_frame,
4704 resize_frame,
4705 );
4706 let redraw_reason = if resize_unsettled {
4707 "pending_resize"
4708 } else if pending_capture_settle {
4709 "pending_capture_settle"
4710 } else if invalidations.build {
4711 "pending_work:build"
4712 } else if invalidations.layout {
4713 "pending_work:layout"
4714 } else if invalidations.paint {
4715 "pending_work:paint"
4716 } else if invalidations.composite {
4717 "pending_work:composite"
4718 } else if effect_results_dispatched {
4719 "pending_work:effects"
4720 } else if frame_hook_wants_redraw {
4721 "pending_work:frame_hook"
4722 } else {
4723 "pending_work"
4724 };
4725 request_redraw_logged(
4726 &window,
4727 elwt,
4728 &mut last_redraw_at,
4729 pending_frame,
4730 &mut redraw_pending,
4731 &mut frame_trace,
4732 redraw_reason,
4733 );
4734 let reasons = frame_trace.take_redraw_reasons();
4735 frame_trace.emit(
4736 "about_to_wait",
4737 presented_frames + 1,
4738 &active_keys,
4739 invalidations,
4740 &reasons,
4741 &format!(
4742 "schedule=pending interval_ms={} pending_resize={} redraw_pending={} highest={}",
4743 pending_frame.as_millis(),
4744 resize_unsettled || pending_capture_settle,
4745 redraw_pending,
4746 invalidations.highest_class(),
4747 ),
4748 );
4749 let mut wake_at = last_redraw_at + pending_frame;
4750 if let Some(blink_at) = blink_wake_at {
4751 if blink_at < wake_at {
4752 wake_at = blink_at;
4753 }
4754 }
4755 if let Some(hook_at) = frame_hook_wake_at {
4756 if hook_at < wake_at {
4757 wake_at = hook_at;
4758 }
4759 }
4760 elwt.set_control_flow(ControlFlow::WaitUntil(wake_at));
4761 } else if let Some(animation_frame) = animation_frame {
4762 request_redraw_logged(
4763 &window,
4764 elwt,
4765 &mut last_redraw_at,
4766 animation_frame,
4767 &mut redraw_pending,
4768 &mut frame_trace,
4769 if has_finite_animation {
4770 "animation:finite"
4771 } else if has_playing_video {
4772 "animation:video"
4773 } else {
4774 "animation:repeat"
4775 },
4776 );
4777 let reasons = frame_trace.take_redraw_reasons();
4778 frame_trace.emit(
4779 "about_to_wait",
4780 presented_frames + 1,
4781 &active_keys,
4782 invalidations,
4783 &reasons,
4784 &format!(
4785 "schedule=animation interval_ms={} pending_resize={} redraw_pending={} highest={}",
4786 animation_frame.as_millis(),
4787 resize_unsettled || pending_capture_settle,
4788 redraw_pending,
4789 invalidations.highest_class(),
4790 ),
4791 );
4792 let mut wake_at = last_redraw_at + animation_frame;
4793 if let Some(blink_at) = blink_wake_at {
4794 if blink_at < wake_at {
4795 wake_at = blink_at;
4796 }
4797 }
4798 if let Some(hook_at) = frame_hook_wake_at {
4799 if hook_at < wake_at {
4800 wake_at = hook_at;
4801 }
4802 }
4803 elwt.set_control_flow(ControlFlow::WaitUntil(wake_at));
4804 } else if image_cache_pending {
4805 let wake_at = now + Duration::from_millis(50);
4806 elwt.set_control_flow(ControlFlow::WaitUntil(wake_at));
4807 } else if let Some(blink_at) = blink_wake_at {
4808 let reasons = frame_trace.take_redraw_reasons();
4809 frame_trace.emit(
4810 "about_to_wait",
4811 presented_frames + 1,
4812 &active_keys,
4813 invalidations,
4814 &reasons,
4815 "schedule=blink_wait pending_resize=false redraw_pending=false highest=none",
4816 );
4817 let mut wake_at = blink_at;
4818 if let Some(hook_at) = frame_hook_wake_at {
4819 if hook_at < wake_at {
4820 wake_at = hook_at;
4821 }
4822 }
4823 elwt.set_control_flow(ControlFlow::WaitUntil(wake_at));
4824 } else if let Some(hook_at) = frame_hook_wake_at {
4825 let reasons = frame_trace.take_redraw_reasons();
4826 frame_trace.emit(
4827 "about_to_wait",
4828 presented_frames + 1,
4829 &active_keys,
4830 invalidations,
4831 &reasons,
4832 "schedule=hook_wait pending_resize=false redraw_pending=false highest=none",
4833 );
4834 elwt.set_control_flow(ControlFlow::WaitUntil(hook_at));
4835 } else {
4836 let reasons = frame_trace.take_redraw_reasons();
4837 frame_trace.emit(
4838 "about_to_wait",
4839 presented_frames + 1,
4840 &active_keys,
4841 invalidations,
4842 &reasons,
4843 "schedule=idle pending_resize=false redraw_pending=false highest=none",
4844 );
4845 #[cfg(target_os = "android")]
4846 if test_control_enabled {
4847 elwt.set_control_flow(ControlFlow::Poll);
4848 } else {
4849 elwt.set_control_flow(ControlFlow::WaitUntil(
4850 now + Duration::from_millis(16),
4851 ));
4852 }
4853 #[cfg(not(target_os = "android"))]
4854 elwt.set_control_flow(ControlFlow::Wait);
4855 }
4856 }
4857
4858 Event::WindowEvent { window_id, event }
4862 if platform_window.active_window_id() == Some(window_id) =>
4863 {
4864 let Some(window) = platform_window.active_window() else {
4865 return;
4866 };
4867 match event {
4868 WindowEvent::Resized(size) => {
4869 if size.width > 0 && size.height > 0 {
4870 #[cfg(target_os = "ios")]
4871 let next_viewport = WindowViewportState::from_window(window);
4872 #[cfg(not(target_os = "ios"))]
4873 let next_viewport = pending_resize
4874 .unwrap_or_else(|| WindowViewportState::from_window(window))
4875 .with_physical_size(size);
4876 #[cfg(not(target_os = "android"))]
4877 {
4878 window_viewport = next_viewport;
4879 }
4880 #[cfg(target_os = "android")]
4881 {
4882 window_viewport = Some(next_viewport);
4883 }
4884 apply_authoritative_resize(
4885 &window,
4886 elwt,
4887 next_viewport,
4888 &mut pending_resize,
4889 &mut resize_needs_settled_frame,
4890 &mut pending_capture_settle,
4891 pending_screenshot_path.as_deref(),
4892 &mut live_resize,
4893 &mut invalidations,
4894 &mut last_redraw_at,
4895 resize_frame,
4896 &mut redraw_pending,
4897 &mut frame_trace,
4898 "window_resized",
4899 );
4900 }
4901 }
4902 WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
4903 #[cfg(target_os = "ios")]
4904 let _ = scale_factor;
4905 #[cfg(target_os = "ios")]
4906 let next_viewport = WindowViewportState::from_window(window);
4907 #[cfg(not(target_os = "ios"))]
4908 let next_viewport = pending_resize
4909 .unwrap_or_else(|| WindowViewportState::from_window(window))
4910 .with_scale_factor(scale_factor);
4911 #[cfg(not(target_os = "android"))]
4912 {
4913 window_viewport = next_viewport;
4914 }
4915 #[cfg(target_os = "android")]
4916 {
4917 window_viewport = Some(next_viewport);
4918 }
4919 apply_authoritative_resize(
4920 &window,
4921 elwt,
4922 next_viewport,
4923 &mut pending_resize,
4924 &mut resize_needs_settled_frame,
4925 &mut pending_capture_settle,
4926 pending_screenshot_path.as_deref(),
4927 &mut live_resize,
4928 &mut invalidations,
4929 &mut last_redraw_at,
4930 resize_frame,
4931 &mut redraw_pending,
4932 &mut frame_trace,
4933 "scale_factor_changed",
4934 );
4935 }
4936 WindowEvent::RedrawRequested => {
4937 if debug_android_events {
4938 eprintln!("[android-events] redraw_requested");
4939 }
4940 redraw_pending = false;
4941 diag::begin_frame(None);
4942 let now = Instant::now();
4943 let dt = now.duration_since(last_frame_time);
4944 last_frame_time = now;
4945 let dt_ms = dt.as_millis() as u64;
4946 let pre_tick_active = active_animation_keys(&runtime);
4947 match runtime.tick(dt_ms) {
4948 Ok(tick_result) => {
4949 let tick_invalidations = pipeline
4950 .classify_animation_updates(
4951 &tick_result.changed_animations,
4952 );
4953 invalidations.merge(tick_invalidations);
4954 let reasons = if tick_result.changed_animations.is_empty() {
4955 Vec::new()
4956 } else {
4957 tick_result
4958 .changed_animations
4959 .iter()
4960 .map(|(target, property)| {
4961 format!(
4962 "tick:{}:{:?}:{}",
4963 target.as_u128(),
4964 property,
4965 tick_invalidations.highest_class()
4966 )
4967 })
4968 .collect::<Vec<_>>()
4969 };
4970 frame_trace.emit(
4971 "redraw_requested",
4972 presented_frames + 1,
4973 &pre_tick_active,
4974 tick_invalidations,
4975 &reasons,
4976 &format!("dt_ms={}", dt_ms),
4977 );
4978 }
4979 Err(e) => {
4980 eprintln!("Runtime tick error: {:?}", e);
4981 }
4982 }
4983 if process_pending_effects(
4984 &mut runtime,
4985 &effect_result_tx,
4986 &event_proxy,
4987 &async_registry,
4988 &mut active_services,
4989 &mut service_bindings,
4990 &mut next_service_instance_id,
4991 ) {
4992 invalidations.mark_build();
4993 request_redraw_logged(
4994 &window,
4995 elwt,
4996 &mut last_redraw_at,
4997 min_frame,
4998 &mut redraw_pending,
4999 &mut frame_trace,
5000 "redraw:effects",
5001 );
5002 }
5003 let viewport_state = pending_resize.unwrap_or_else(|| {
5004 #[cfg(not(target_os = "android"))]
5005 {
5006 window_viewport
5007 }
5008 #[cfg(target_os = "android")]
5009 {
5010 window_viewport.unwrap_or_else(|| {
5011 WindowViewportState::from_window(window)
5012 })
5013 }
5014 });
5015 #[cfg(not(target_os = "android"))]
5016 {
5017 window_viewport = viewport_state;
5018 }
5019 #[cfg(target_os = "android")]
5020 {
5021 window_viewport = Some(viewport_state);
5022 }
5023 let swapchain_size = viewport_state.physical_size;
5024 if swapchain_size.width == 0 || swapchain_size.height == 0 {
5025 diag::end_frame(diag::FrameStats::default());
5026 return;
5027 }
5028
5029 let scale_factor = viewport_state.scale_factor;
5030 let pending_layout_viewport = viewport_state.logical_size();
5031 let render_target_size =
5032 (swapchain_size.width, swapchain_size.height);
5033
5034 #[cfg(target_arch = "wasm32")]
5035 if web_renderer.is_none() {
5036 let request = web_renderer_request();
5037 if matches!(request, RendererRequest::Canvas2dSoftware) {
5038 match WebCanvasPresenter::new(window) {
5039 Ok(mut presenter) => {
5040 presenter.report = RendererReport::new(
5041 "canvas2d-software",
5042 request,
5043 None,
5044 None,
5045 Some("forced_by_renderer_request".to_string()),
5046 render_target_size.0,
5047 render_target_size.1,
5048 scale_factor,
5049 );
5050 web_renderer =
5051 Some(WebRenderer::Canvas2d(presenter));
5052 }
5053 Err(err) => {
5054 eprintln!("web canvas not ready yet: {err}");
5055 request_redraw_logged(
5056 &window,
5057 elwt,
5058 &mut last_redraw_at,
5059 min_frame,
5060 &mut redraw_pending,
5061 &mut frame_trace,
5062 "web_canvas_pending",
5063 );
5064 diag::end_frame(diag::FrameStats::default());
5065 return;
5066 }
5067 }
5068 } else if let Some(result) =
5069 pending_webgpu_init.borrow_mut().take()
5070 {
5071 match result {
5072 Ok(presenter) => {
5073 web_renderer = Some(WebRenderer::WebGpu(presenter));
5074 }
5075 Err(error) if request.is_explicit_gpu() => {
5076 eprintln!(
5077 "webgpu-vello renderer requested but initialization failed: {error}"
5078 );
5079 diag::end_frame(diag::FrameStats::default());
5080 panic!(
5081 "webgpu-vello renderer requested but initialization failed: {error}"
5082 );
5083 }
5084 Err(error) => match WebCanvasPresenter::new(window) {
5085 Ok(mut presenter) => {
5086 presenter.report = RendererReport::new(
5087 "canvas2d-software",
5088 request,
5089 None,
5090 None,
5091 Some(format!(
5092 "webgpu_vello_init_failed:{error}"
5093 )),
5094 render_target_size.0,
5095 render_target_size.1,
5096 scale_factor,
5097 );
5098 web_renderer =
5099 Some(WebRenderer::Canvas2d(presenter));
5100 }
5101 Err(err) => {
5102 eprintln!(
5103 "web renderer fallback failed; webgpu error: {error}; canvas error: {err}"
5104 );
5105 request_redraw_logged(
5106 &window,
5107 elwt,
5108 &mut last_redraw_at,
5109 min_frame,
5110 &mut redraw_pending,
5111 &mut frame_trace,
5112 "web_canvas_pending",
5113 );
5114 diag::end_frame(diag::FrameStats::default());
5115 return;
5116 }
5117 },
5118 }
5119 } else {
5120 if !webgpu_init_in_flight {
5121 match window.canvas() {
5122 Some(canvas) => {
5123 let pending = pending_webgpu_init.clone();
5124 let proxy = event_proxy.clone();
5125 let init_viewport = viewport_state;
5126 webgpu_init_in_flight = true;
5127 wasm_bindgen_futures::spawn_local(async move {
5128 let result = create_webgpu_presenter(
5129 canvas,
5130 init_viewport,
5131 request,
5132 )
5133 .await
5134 .map_err(|error| error.to_string());
5135 *pending.borrow_mut() = Some(result);
5136 let _ = proxy.send_event(TestEvent::Wake);
5137 });
5138 }
5139 None => {
5140 eprintln!("web canvas not ready yet");
5141 }
5142 }
5143 }
5144 request_redraw_logged(
5145 &window,
5146 elwt,
5147 &mut last_redraw_at,
5148 min_frame,
5149 &mut redraw_pending,
5150 &mut frame_trace,
5151 "webgpu_renderer_pending",
5152 );
5153 diag::end_frame(diag::FrameStats::default());
5154 return;
5155 }
5156
5157 if !web_renderer_reported {
5158 if let Some(renderer) = web_renderer.as_ref() {
5159 publish_web_renderer_report(renderer.report());
5160 web_renderer_reported = true;
5161 }
5162 }
5163 }
5164
5165 #[cfg(not(target_arch = "wasm32"))]
5166 {
5167 if render_state.is_none() {
5168 let Some(render_window) =
5169 platform_window.active_window_arc()
5170 else {
5171 diag::end_frame(diag::FrameStats::default());
5172 return;
5173 };
5174 match create_render_state(
5175 &mut render_cx,
5176 render_window,
5177 viewport_state,
5178 ) {
5179 Ok(state) => {
5180 render_state = Some(state);
5181 }
5182 Err(err) => {
5183 eprintln!("render surface not ready yet: {err}");
5184 request_redraw_logged(
5185 &window,
5186 elwt,
5187 &mut last_redraw_at,
5188 min_frame,
5189 &mut redraw_pending,
5190 &mut frame_trace,
5191 "render_surface_pending",
5192 );
5193 diag::end_frame(diag::FrameStats::default());
5194 return;
5195 }
5196 }
5197 }
5198 let render_state = render_state.as_mut().expect("render state");
5199
5200 let mut surface_target_replaced = false;
5201 if swapchain_size.width != render_state.surface.config.width
5202 || swapchain_size.height
5203 != render_state.surface.config.height
5204 {
5205 render_cx.resize_surface(
5206 &mut render_state.surface,
5207 swapchain_size.width,
5208 swapchain_size.height,
5209 );
5210 let device_handle =
5211 &render_cx.devices[render_state.surface.dev_id];
5212 render_state.surface.config.alpha_mode =
5213 wgpu::CompositeAlphaMode::PostMultiplied;
5214 render_state.surface.surface.configure(
5215 &device_handle.device,
5216 &render_state.surface.config,
5217 );
5218 sync_tracked_target_texture_size_to_surface(
5219 &mut render_state.target_texture_size,
5220 swapchain_size,
5221 );
5222 surface_target_replaced = true;
5223 }
5224 if surface_target_replaced
5225 || render_target_size != render_state.target_texture_size
5226 {
5227 recreate_target_texture(
5228 &mut render_state.surface,
5229 &render_cx,
5230 render_target_size.0,
5231 render_target_size.1,
5232 );
5233 #[cfg(feature = "three-d")]
5234 {
5235 let device_handle =
5236 &render_cx.devices[render_state.surface.dev_id];
5237 render_state.scene3d_renderer.resize(
5239 &device_handle.device,
5240 render_target_size.0,
5241 render_target_size.1,
5242 );
5243 }
5244 render_state.target_texture_size = render_target_size;
5245 }
5246 }
5247
5248 let resize_settled =
5249 resize_needs_settled_frame && !live_resize.is_live(now);
5250 let target_viewport = pending_layout_viewport;
5251 let build_viewport = resolve_build_viewport(
5252 last_built_viewport,
5253 target_viewport,
5254 pipeline.prev_ir.is_some(),
5255 &mut invalidations,
5256 );
5257 env.viewport_size = build_viewport;
5258 env.window_insets =
5259 window_safe_area_insets(window, viewport_state.scale_factor);
5260
5261 if let Some(sync) = &self.sync_env {
5262 let state = runtime.get_global_state::<S>().unwrap();
5263 sync(state, &mut env);
5264 }
5265 let desired_window_title = env.window.title.plain_text();
5266 if desired_window_title != applied_window_title {
5267 if let Some(window) = platform_window.active_window() {
5268 window.set_title(desired_window_title);
5269 }
5270 applied_window_title = desired_window_title.to_string();
5271 }
5272
5273 if invalidations.build || pipeline.prev_ir.is_none() {
5274 let (
5275 node_tree,
5276 registry,
5277 resources,
5278 anims,
5279 videos,
5280 web_views,
5281 portals,
5282 ) = {
5283 let state = runtime.get_global_state::<S>().unwrap();
5284 let view = View::new(
5285 state,
5286 &runtime.runtime_state,
5287 &env,
5288 pipeline.last_snapshot.as_ref(),
5289 );
5290 let mut ctx = BuildCtx::new();
5291 let node =
5292 fission_core::build::enter(&mut ctx, &view, || {
5293 root_widget.clone().into()
5294 });
5295 let resources = ctx.take_resources();
5296 let anims = ctx.take_animation_requests();
5297 let videos = ctx.take_video_registrations();
5298 let web_views = ctx.take_web_registrations();
5299 let portals_with_ids = ctx.take_portals();
5300
5301 let portals = portals_with_ids
5302 .into_iter()
5303 .map(|(id, node)| {
5304 wrap_portal_for_viewport(id, node, &env)
5305 })
5306 .collect::<Vec<_>>();
5307
5308 diag::emit(
5309 diag::DiagCategory::Layout,
5310 diag::DiagLevel::Debug,
5311 diag::DiagEventKind::PortalsComposed {
5312 portal_count: portals.len() as u32,
5313 },
5314 );
5315 (
5316 node,
5317 ctx.registry,
5318 resources,
5319 anims,
5320 videos,
5321 web_views,
5322 portals,
5323 )
5324 };
5325
5326 #[cfg(feature = "tray")]
5327 let tray_registry = if let Some(tray) = active_tray.as_mut() {
5328 match tray.refresh_menu(&runtime, &env, &pipeline) {
5329 Ok(registry) => Some(registry),
5330 Err(err) => {
5331 eprintln!(
5332 "Runtime tray menu rebuild error: {:?}",
5333 err
5334 );
5335 None
5336 }
5337 }
5338 } else {
5339 None
5340 };
5341
5342 runtime.clear_reducers();
5343 runtime.absorb_registry(registry);
5344 #[cfg(feature = "tray")]
5345 if let Some(registry) = tray_registry {
5346 runtime.absorb_registry(registry);
5347 }
5348 if let Err(err) = runtime.reconcile_resources(resources) {
5349 eprintln!(
5350 "Runtime resource reconciliation error: {:?}",
5351 err
5352 );
5353 }
5354 if !startup_dispatched {
5355 if let Some(action) = startup_action.clone() {
5356 if let Err(err) =
5357 runtime.dispatch(action, WidgetId::from_u128(0))
5358 {
5359 eprintln!("Startup action error: {:?}", err);
5360 }
5361 }
5362 startup_dispatched = true;
5363 }
5364 runtime.sync_animation_requests(&anims);
5365 for (target, req) in anims {
5366 runtime.enqueue_animation(target, req);
5367 }
5368 runtime.sync_video_nodes(&videos);
5369 runtime.sync_web_nodes(&web_views);
5370
5371 let final_root: fission_core::Widget =
5372 fission_core::ui::Overlay {
5373 id: None,
5374 content: node_tree,
5375 overlay: fission_core::ui::ZStack {
5376 children: portals,
5377 ..Default::default()
5378 }
5379 .into(),
5380 }
5381 .into();
5382
5383 let mut lower_cx = InternalLoweringCx::new(
5384 &env,
5385 &runtime.runtime_state,
5386 runtime.measurer.as_ref(),
5387 pipeline.last_snapshot.as_ref(),
5388 );
5389 let root_id = fission_core::internal::lower_widget(
5390 &final_root,
5391 &mut lower_cx,
5392 );
5393 lower_cx.ir.root = Some(root_id);
5394
5395 let pipeline_invalidations =
5396 pipeline.replace_ir(lower_cx.ir, &env);
5397 invalidations.merge(pipeline_invalidations);
5398 last_built_viewport = Some(build_viewport);
5399 }
5400
5401 let layout_updates = match pipeline.ensure_layout(
5402 LayoutRect::new(
5403 0.0,
5404 0.0,
5405 target_viewport.width,
5406 target_viewport.height,
5407 ),
5408 &mut layout_engine,
5409 &runtime.runtime_state.scroll,
5410 ) {
5411 Ok(updates) => updates,
5412 Err(e) => {
5413 eprintln!("Layout error: {:?}", e);
5414 diag::end_frame(diag::FrameStats::default());
5415 return;
5416 }
5417 };
5418
5419 if layout_updates > 0 {
5420 if let (Some(ir), Some(layout)) =
5421 (pipeline.prev_ir.as_ref(), pipeline.last_snapshot.as_ref())
5422 {
5423 runtime.post_layout_hook(ir, layout);
5424 }
5425 }
5426
5427 match pipeline.prepare_current(
5428 target_viewport,
5429 target_viewport,
5430 false,
5431 &runtime.runtime_state.scroll,
5432 &runtime.runtime_state.animation,
5433 &runtime.runtime_state.video,
5434 &runtime.runtime_state.web,
5435 ) {
5436 Ok(_stats) => {
5437 #[cfg(target_arch = "wasm32")]
5438 {
5439 let Some(renderer) = web_renderer.as_mut() else {
5440 eprintln!("web renderer is unavailable");
5441 diag::end_frame(diag::FrameStats::default());
5442 return;
5443 };
5444 let active_renderer =
5445 renderer.active_name().to_string();
5446 match renderer {
5447 WebRenderer::Canvas2d(presenter) => {
5448 let retained_scene = pipeline
5449 .retained_scene()
5450 .expect(
5451 "retained render scene missing before render",
5452 );
5453 let rgba = SoftwareRenderer::render(
5454 retained_scene,
5455 render_target_size.0,
5456 render_target_size.1,
5457 fission_render::Color {
5458 r: env.theme.tokens.colors.background.r,
5459 g: env.theme.tokens.colors.background.g,
5460 b: env.theme.tokens.colors.background.b,
5461 a: env.theme.tokens.colors.background.a,
5462 },
5463 scale_factor as f32,
5464 )
5465 .expect(
5466 "failed to rasterize software web frame",
5467 );
5468
5469 if let Err(err) = presenter.present(
5470 &rgba,
5471 render_target_size.0,
5472 render_target_size.1,
5473 scale_factor,
5474 ) {
5475 eprintln!(
5476 "failed to present web canvas frame: {err}"
5477 );
5478 diag::end_frame(diag::FrameStats::default());
5479 return;
5480 }
5481 }
5482 WebRenderer::WebGpu(presenter) => {
5483 if swapchain_size.width
5484 != presenter
5485 .render_state
5486 .surface
5487 .config
5488 .width
5489 || swapchain_size.height
5490 != presenter
5491 .render_state
5492 .surface
5493 .config
5494 .height
5495 {
5496 presenter.render_cx.resize_surface(
5497 &mut presenter.render_state.surface,
5498 swapchain_size.width,
5499 swapchain_size.height,
5500 );
5501 let device_handle = &presenter
5502 .render_cx
5503 .devices
5504 [presenter.render_state.surface.dev_id];
5505 presenter
5506 .render_state
5507 .surface
5508 .config
5509 .alpha_mode =
5510 wgpu::CompositeAlphaMode::PostMultiplied;
5511 presenter
5512 .render_state
5513 .surface
5514 .surface
5515 .configure(
5516 &device_handle.device,
5517 &presenter
5518 .render_state
5519 .surface
5520 .config,
5521 );
5522 sync_tracked_target_texture_size_to_surface(
5523 &mut presenter
5524 .render_state
5525 .target_texture_size,
5526 swapchain_size,
5527 );
5528 }
5529 if render_target_size
5530 != presenter
5531 .render_state
5532 .target_texture_size
5533 {
5534 recreate_target_texture(
5535 &mut presenter.render_state.surface,
5536 &presenter.render_cx,
5537 render_target_size.0,
5538 render_target_size.1,
5539 );
5540 presenter
5541 .render_state
5542 .target_texture_size =
5543 render_target_size;
5544 }
5545
5546 let surface_texture = match presenter
5547 .render_state
5548 .surface
5549 .surface
5550 .get_current_texture()
5551 {
5552 Ok(texture) => texture,
5553 Err(err) => {
5554 eprintln!(
5555 "failed to get webgpu surface texture: {err}"
5556 );
5557 diag::end_frame(
5558 diag::FrameStats::default(),
5559 );
5560 return;
5561 }
5562 };
5563 let device_handle =
5564 &presenter.render_cx.devices
5565 [presenter.render_state.surface.dev_id];
5566
5567 let clear_color = vello::wgpu::Color {
5568 r: env.theme.tokens.colors.background.r
5569 as f64
5570 / 255.0,
5571 g: env.theme.tokens.colors.background.g
5572 as f64
5573 / 255.0,
5574 b: env.theme.tokens.colors.background.b
5575 as f64
5576 / 255.0,
5577 a: env.theme.tokens.colors.background.a
5578 as f64
5579 / 255.0,
5580 };
5581 match &mut presenter.render_state.main_renderer
5582 {
5583 MainRenderer::Vello {
5584 renderer,
5585 texture_compositor,
5586 } => {
5587 let texture_plans =
5588 pipeline.texture_compositor_plans();
5589 let texture_plans_fit_limits =
5590 texture_plans_fit_device_limits(
5591 texture_plans,
5592 scale_factor,
5593 device_handle
5594 .device
5595 .limits()
5596 .max_texture_dimension_2d,
5597 );
5598 let has_active_scroll_offsets = runtime
5599 .runtime_state
5600 .scroll
5601 .offsets
5602 .values()
5603 .any(|offset| offset.abs() > 0.5);
5604 let enable_texture_compositor =
5605 web_bool_global(
5606 "FISSION_ENABLE_TEXTURE_COMPOSITOR",
5607 );
5608 if !enable_texture_compositor
5609 || texture_plans.is_empty()
5610 || !texture_plans_fit_limits
5611 || has_active_scroll_offsets
5612 {
5613 let render_params =
5614 vello::RenderParams {
5615 base_color:
5616 vello::peniko::Color::from_rgba8(
5617 env.theme
5618 .tokens
5619 .colors
5620 .background
5621 .r,
5622 env.theme
5623 .tokens
5624 .colors
5625 .background
5626 .g,
5627 env.theme
5628 .tokens
5629 .colors
5630 .background
5631 .b,
5632 env.theme
5633 .tokens
5634 .colors
5635 .background
5636 .a,
5637 ),
5638 width: render_target_size.0,
5639 height: render_target_size.1,
5640 antialiasing_method:
5641 vello::AaConfig::Area,
5642 };
5643
5644 presenter.scene.reset();
5645 let retained_scene = pipeline
5646 .retained_scene()
5647 .expect(
5648 "retained render scene missing before render",
5649 );
5650 let mut renderer_wrapper =
5651 VelloRenderer::new(
5652 &mut presenter.scene,
5653 measurer.clone(),
5654 &mut presenter
5655 .retained_scene_cache,
5656 scale_factor,
5657 );
5658 renderer_wrapper
5659 .render_scene(retained_scene)
5660 .expect(
5661 "failed to encode retained scene",
5662 );
5663 renderer
5664 .render_to_texture(
5665 &device_handle.device,
5666 &device_handle.queue,
5667 &presenter.scene,
5668 &presenter
5669 .render_state
5670 .surface
5671 .target_view,
5672 &render_params,
5673 )
5674 .expect(
5675 "failed to render webgpu frame",
5676 );
5677 } else {
5678 let force_full_compositor_redraw =
5679 invalidations.build
5680 || invalidations.layout
5681 || invalidations.paint;
5682 let _compositor_stats =
5683 texture_compositor
5684 .render_layers(
5685 &device_handle.device,
5686 &device_handle.queue,
5687 renderer,
5688 &mut presenter
5689 .retained_scene_cache,
5690 measurer.clone(),
5691 scale_factor,
5692 render_target_size.0,
5693 render_target_size.1,
5694 pipeline
5695 .texture_compositor_root_transform(),
5696 texture_plans,
5697 force_full_compositor_redraw,
5698 clear_color,
5699 &presenter
5700 .render_state
5701 .surface
5702 .target_view,
5703 )
5704 .expect(
5705 "failed to composite webgpu texture layers",
5706 );
5707 }
5708 }
5709 MainRenderer::Software => {}
5710 }
5711
5712 let surface_view =
5713 surface_texture.texture.create_view(
5714 &wgpu::TextureViewDescriptor::default(),
5715 );
5716 let mut encoder = device_handle
5717 .device
5718 .create_command_encoder(
5719 &wgpu::CommandEncoderDescriptor {
5720 label: Some("WebGPU Surface Blit"),
5721 },
5722 );
5723 presenter.render_state.surface.blitter.copy(
5724 &device_handle.device,
5725 &mut encoder,
5726 &presenter.render_state.surface.target_view,
5727 &surface_view,
5728 );
5729 device_handle
5730 .queue
5731 .submit(Some(encoder.finish()));
5732 surface_texture.present();
5733 }
5734 }
5735
5736 let capture_ready =
5737 !pending_capture_settle || resize_settled;
5738 if capture_ready {
5739 pending_capture_settle = false;
5740 let _ = pending_screenshot_path.take();
5741 let _ = pending_screenshot_response_tx.take();
5742 }
5743
5744 pending_resize = None;
5745 if resize_settled {
5746 resize_needs_settled_frame = false;
5747 }
5748 invalidations = InvalidationSet::default();
5749
5750 presented_frames = presented_frames.saturating_add(1);
5751 flush_text_traces(
5752 text_trace_enabled,
5753 &mut pending_text_traces,
5754 presented_frames,
5755 );
5756
5757 let total_ms = now.elapsed().as_secs_f64() * 1000.0;
5758 publish_web_frame_perf(&active_renderer, total_ms);
5759 if let Some(input_at) = pending_web_input_at.take() {
5760 publish_web_input_latency(
5761 &active_renderer,
5762 input_at.elapsed().as_secs_f64() * 1000.0,
5763 );
5764 }
5765
5766 diag::end_frame(diag::FrameStats::default());
5767 }
5768 #[cfg(not(target_arch = "wasm32"))]
5769 {
5770 let render_state =
5771 render_state.as_mut().expect("render state");
5772 let surface_texture = render_state
5773 .surface
5774 .surface
5775 .get_current_texture()
5776 .expect("failed to get texture");
5777 let device_handle =
5778 &render_cx.devices[render_state.surface.dev_id];
5779
5780 let clear_color = vello::wgpu::Color {
5781 r: env.theme.tokens.colors.background.r as f64
5782 / 255.0,
5783 g: env.theme.tokens.colors.background.g as f64
5784 / 255.0,
5785 b: env.theme.tokens.colors.background.b as f64
5786 / 255.0,
5787 a: env.theme.tokens.colors.background.a as f64
5788 / 255.0,
5789 };
5790 match &mut render_state.main_renderer {
5791 MainRenderer::Vello {
5792 renderer,
5793 texture_compositor,
5794 } => {
5795 let texture_plans =
5796 pipeline.texture_compositor_plans();
5797 let texture_plans_fit_limits =
5798 texture_plans_fit_device_limits(
5799 texture_plans,
5800 scale_factor,
5801 device_handle
5802 .device
5803 .limits()
5804 .max_texture_dimension_2d,
5805 );
5806 let has_active_scroll_offsets = runtime
5807 .runtime_state
5808 .scroll
5809 .offsets
5810 .values()
5811 .any(|offset| offset.abs() > 0.5);
5812 let enable_texture_compositor = std::env::var(
5813 "FISSION_ENABLE_TEXTURE_COMPOSITOR",
5814 )
5815 .ok()
5816 .as_deref()
5817 == Some("1");
5818 if !enable_texture_compositor
5819 || texture_plans.is_empty()
5820 || !texture_plans_fit_limits
5821 || has_active_scroll_offsets
5822 {
5823 let render_params = vello::RenderParams {
5824 base_color:
5825 vello::peniko::Color::from_rgba8(
5826 env.theme
5827 .tokens
5828 .colors
5829 .background
5830 .r,
5831 env.theme
5832 .tokens
5833 .colors
5834 .background
5835 .g,
5836 env.theme
5837 .tokens
5838 .colors
5839 .background
5840 .b,
5841 env.theme
5842 .tokens
5843 .colors
5844 .background
5845 .a,
5846 ),
5847 width: render_target_size.0,
5848 height: render_target_size.1,
5849 antialiasing_method:
5850 vello::AaConfig::Area,
5851 };
5852
5853 scene.reset();
5854 let retained_scene = pipeline
5855 .retained_scene()
5856 .expect(
5857 "retained render scene missing before render",
5858 );
5859 let mut renderer_wrapper =
5860 VelloRenderer::new(
5861 &mut scene,
5862 measurer.clone(),
5863 &mut retained_scene_cache,
5864 scale_factor,
5865 );
5866 renderer_wrapper
5867 .render_scene(retained_scene)
5868 .expect(
5869 "failed to encode retained scene",
5870 );
5871 renderer
5872 .render_to_texture(
5873 &device_handle.device,
5874 &device_handle.queue,
5875 &scene,
5876 &render_state.surface.target_view,
5877 &render_params,
5878 )
5879 .expect("failed to render");
5880 } else {
5881 let force_full_compositor_redraw =
5882 invalidations.build
5883 || invalidations.layout
5884 || invalidations.paint;
5885 let _compositor_stats = texture_compositor
5886 .render_layers(
5887 &device_handle.device,
5888 &device_handle.queue,
5889 renderer,
5890 &mut retained_scene_cache,
5891 measurer.clone(),
5892 scale_factor,
5893 render_target_size.0,
5894 render_target_size.1,
5895 pipeline
5896 .texture_compositor_root_transform(
5897 ),
5898 texture_plans,
5899 force_full_compositor_redraw,
5900 clear_color,
5901 &render_state.surface.target_view,
5902 )
5903 .expect(
5904 "failed to composite texture layers",
5905 );
5906 }
5907 }
5908 MainRenderer::Software => {
5909 let retained_scene = pipeline
5910 .retained_scene()
5911 .expect(
5912 "retained render scene missing before render",
5913 );
5914 let rgba = SoftwareRenderer::render(
5915 retained_scene,
5916 render_target_size.0,
5917 render_target_size.1,
5918 fission_render::Color {
5919 r: env.theme.tokens.colors.background.r,
5920 g: env.theme.tokens.colors.background.g,
5921 b: env.theme.tokens.colors.background.b,
5922 a: env.theme.tokens.colors.background.a,
5923 },
5924 scale_factor as f32,
5925 )
5926 .expect("failed to rasterize software frame");
5927 device_handle.queue.write_texture(
5928 wgpu::TexelCopyTextureInfo {
5929 texture: &render_state
5930 .surface
5931 .target_texture,
5932 mip_level: 0,
5933 origin: wgpu::Origin3d::ZERO,
5934 aspect: wgpu::TextureAspect::All,
5935 },
5936 &rgba,
5937 wgpu::TexelCopyBufferLayout {
5938 offset: 0,
5939 bytes_per_row: Some(
5940 render_target_size.0 * 4,
5941 ),
5942 rows_per_image: Some(
5943 render_target_size.1,
5944 ),
5945 },
5946 wgpu::Extent3d {
5947 width: render_target_size.0,
5948 height: render_target_size.1,
5949 depth_or_array_layers: 1,
5950 },
5951 );
5952 }
5953 }
5954
5955 #[cfg(feature = "three-d")]
5956 {
5957 for (_, rect, payload) in
5958 &pipeline.scene_3d_surfaces
5959 {
5960 if let Ok(primitives) = bincode::deserialize::<
5961 Vec<fission_3d::Primitive3D>,
5962 >(
5963 payload
5964 ) {
5965 let scene3d = fission_3d::Scene3D {
5966 width: Some(rect.size.width),
5967 height: Some(rect.size.height),
5968 primitives,
5969 };
5970 let scale = scale_factor as f32;
5971 render_state
5972 .scene3d_renderer
5973 .render_in_rect(
5974 &device_handle.device,
5975 &device_handle.queue,
5976 &render_state.surface.target_view,
5977 &scene3d,
5978 fission_3d::render::Scene3DViewport {
5979 x: rect.origin.x * scale,
5980 y: rect.origin.y * scale,
5981 width: rect.size.width * scale,
5982 height: rect.size.height * scale,
5983 },
5984 );
5985 }
5986 }
5987 }
5988
5989 let surface_view = surface_texture.texture.create_view(
5990 &wgpu::TextureViewDescriptor::default(),
5991 );
5992
5993 let mut encoder =
5994 device_handle.device.create_command_encoder(
5995 &wgpu::CommandEncoderDescriptor {
5996 label: Some("Surface Blit"),
5997 },
5998 );
5999
6000 render_state.surface.blitter.copy(
6001 &device_handle.device,
6002 &mut encoder,
6003 &render_state.surface.target_view,
6004 &surface_view,
6005 );
6006
6007 device_handle.queue.submit(Some(encoder.finish()));
6008
6009 let capture_ready =
6010 !pending_capture_settle || resize_settled;
6011 if capture_ready {
6012 pending_capture_settle = false;
6013 }
6014 if capture_ready {
6015 if let Some(path) = pending_screenshot_path.take() {
6016 let screenshot_dimensions =
6017 layout_size_to_image_dimensions(
6018 target_viewport,
6019 );
6020 if let Some(tx) =
6021 pending_screenshot_response_tx.take()
6022 {
6023 if path == "__pump__" {
6024 let _ = tx.send(
6025 fission_test_driver::TestResponse::Ok {},
6026 );
6027 } else if path == "__capture__" {
6028 let resp = gpu_screenshot(
6029 &device_handle.device,
6030 &device_handle.queue,
6031 &render_state
6032 .surface
6033 .target_texture,
6034 render_target_size.0,
6035 render_target_size.1,
6036 screenshot_dimensions.0,
6037 screenshot_dimensions.1,
6038 None,
6039 );
6040 let _ = tx.send(resp);
6041 } else {
6042 let resp = gpu_screenshot(
6043 &device_handle.device,
6044 &device_handle.queue,
6045 &render_state
6046 .surface
6047 .target_texture,
6048 render_target_size.0,
6049 render_target_size.1,
6050 screenshot_dimensions.0,
6051 screenshot_dimensions.1,
6052 Some(&path),
6053 );
6054 let _ = tx.send(resp);
6055 }
6056 }
6057 }
6058 }
6059
6060 surface_texture.present();
6061 pending_resize = None;
6062 if resize_settled {
6063 resize_needs_settled_frame = false;
6064 }
6065 invalidations = InvalidationSet::default();
6066
6067 presented_frames = presented_frames.saturating_add(1);
6068 flush_text_traces(
6069 text_trace_enabled,
6070 &mut pending_text_traces,
6071 presented_frames,
6072 );
6073
6074 diag::emit(
6075 diag::DiagCategory::Frame,
6076 diag::DiagLevel::Debug,
6077 diag::DiagEventKind::FramePerformance {
6078 renderer: render_state
6079 .renderer_report
6080 .active
6081 .clone(),
6082 total_ms: now.elapsed().as_secs_f64() * 1000.0,
6083 },
6084 );
6085 diag::end_frame(diag::FrameStats::default());
6086 }
6087 }
6088 Err(e) => {
6089 eprintln!("Pipeline error: {:?}", e);
6090 diag::end_frame(diag::FrameStats::default());
6091 }
6092 }
6093 }
6094 WindowEvent::CloseRequested => {
6095 #[cfg(feature = "tray")]
6096 if active_tray
6097 .as_ref()
6098 .map(|tray| {
6099 tray.close_behavior()
6100 == tray::WindowCloseBehavior::HideToTray
6101 })
6102 .unwrap_or(false)
6103 {
6104 tray::hide_window_to_tray(window);
6105 return;
6106 }
6107 elwt.exit();
6108 }
6109 WindowEvent::CursorMoved { position, .. } => {
6112 last_cursor_position = Some(position);
6113 let point =
6114 window_physical_position_to_layout_point(window, position);
6115 handle_cursor_moved(
6116 point.x,
6117 point.y,
6118 current_mods,
6119 &mut runtime,
6120 &pipeline,
6121 &effect_result_tx,
6122 &event_proxy,
6123 &async_registry,
6124 &mut active_services,
6125 &mut service_bindings,
6126 &mut next_service_instance_id,
6127 &window,
6128 elwt,
6129 &mut last_redraw_at,
6130 min_frame,
6131 &mut redraw_pending,
6132 &mut frame_trace,
6133 &mut invalidations,
6134 );
6135 }
6136 WindowEvent::CursorLeft { .. } => {
6137 handle_cursor_left(
6138 last_cursor_position,
6139 &mut runtime,
6140 &pipeline,
6141 &effect_result_tx,
6142 &event_proxy,
6143 &async_registry,
6144 &mut active_services,
6145 &mut service_bindings,
6146 &mut next_service_instance_id,
6147 &window,
6148 elwt,
6149 &mut last_redraw_at,
6150 min_frame,
6151 &mut redraw_pending,
6152 &mut frame_trace,
6153 &mut invalidations,
6154 );
6155 last_cursor_position = None;
6156 }
6157 WindowEvent::MouseInput { state, button, .. } => {
6158 #[cfg(target_arch = "wasm32")]
6159 pending_web_input_at.get_or_insert_with(Instant::now);
6160 if let Some(position) = last_cursor_position {
6161 let point =
6162 window_physical_position_to_layout_point(window, position);
6163 if let Some(btn) = map_mouse_button(button) {
6164 let is_pressed = state.is_pressed();
6165 handle_mouse_button(
6166 point.x,
6167 point.y,
6168 btn,
6169 is_pressed,
6170 current_mods,
6171 &mut runtime,
6172 &pipeline,
6173 &effect_result_tx,
6174 &event_proxy,
6175 &async_registry,
6176 &mut active_services,
6177 &mut service_bindings,
6178 &mut next_service_instance_id,
6179 &window,
6180 elwt,
6181 &mut last_redraw_at,
6182 min_frame,
6183 &mut redraw_pending,
6184 text_trace_enabled,
6185 &mut pending_text_traces,
6186 &mut next_text_trace_seq,
6187 presented_frames,
6188 &mut last_blink_toggle,
6189 &mut frame_trace,
6190 &mut invalidations,
6191 );
6192 }
6193 }
6194 }
6195 WindowEvent::MouseWheel { delta, .. } => {
6196 #[cfg(target_arch = "wasm32")]
6197 pending_web_input_at.get_or_insert_with(Instant::now);
6198 if let Some(position) = last_cursor_position {
6199 let scale_factor = window.scale_factor();
6200 let point =
6201 window_physical_position_to_layout_point(window, position);
6202
6203 let (dx, dy) =
6204 normalize_winit_scroll_delta(&delta, scale_factor);
6205
6206 if std::env::var("FISSION_SCROLL_TRACE").ok().as_deref()
6207 == Some("1")
6208 {
6209 eprintln!(
6210 "[scroll-trace] mousewheel raw={:?} point=({:.1},{:.1}) delta=({:.1},{:.1})",
6211 delta, point.x, point.y, dx, dy
6212 );
6213 }
6214 handle_scroll(
6215 point.x,
6216 point.y,
6217 dx,
6218 dy,
6219 current_mods,
6220 &mut runtime,
6221 &pipeline,
6222 &effect_result_tx,
6223 &event_proxy,
6224 &async_registry,
6225 &mut active_services,
6226 &mut service_bindings,
6227 &mut next_service_instance_id,
6228 &window,
6229 elwt,
6230 &mut last_redraw_at,
6231 min_frame,
6232 &mut redraw_pending,
6233 &mut frame_trace,
6234 &mut invalidations,
6235 );
6236 }
6237 }
6238 WindowEvent::Touch(touch) => {
6239 #[cfg(target_arch = "wasm32")]
6240 pending_web_input_at.get_or_insert_with(Instant::now);
6241 let current_position = touch.location;
6242 let position = match touch.phase {
6246 TouchPhase::Ended | TouchPhase::Cancelled => touch_positions
6247 .get(&touch.id)
6248 .copied()
6249 .unwrap_or(current_position),
6250 TouchPhase::Started | TouchPhase::Moved => current_position,
6251 };
6252 last_cursor_position = Some(position);
6253
6254 let point =
6255 window_physical_position_to_layout_point(window, position);
6256
6257 match touch.phase {
6258 TouchPhase::Started => {
6259 touch_positions.insert(touch.id, position);
6260 if active_primary_touch.is_none() {
6261 active_primary_touch = Some(touch.id);
6262 }
6263 if active_primary_touch == Some(touch.id) {
6264 handle_cursor_moved(
6265 point.x,
6266 point.y,
6267 current_mods,
6268 &mut runtime,
6269 &pipeline,
6270 &effect_result_tx,
6271 &event_proxy,
6272 &async_registry,
6273 &mut active_services,
6274 &mut service_bindings,
6275 &mut next_service_instance_id,
6276 &window,
6277 elwt,
6278 &mut last_redraw_at,
6279 min_frame,
6280 &mut redraw_pending,
6281 &mut frame_trace,
6282 &mut invalidations,
6283 );
6284 handle_mouse_button(
6285 point.x,
6286 point.y,
6287 PointerButton::Primary,
6288 true,
6289 current_mods,
6290 &mut runtime,
6291 &pipeline,
6292 &effect_result_tx,
6293 &event_proxy,
6294 &async_registry,
6295 &mut active_services,
6296 &mut service_bindings,
6297 &mut next_service_instance_id,
6298 &window,
6299 elwt,
6300 &mut last_redraw_at,
6301 min_frame,
6302 &mut redraw_pending,
6303 text_trace_enabled,
6304 &mut pending_text_traces,
6305 &mut next_text_trace_seq,
6306 presented_frames,
6307 &mut last_blink_toggle,
6308 &mut frame_trace,
6309 &mut invalidations,
6310 );
6311 }
6312 }
6313 TouchPhase::Moved => {
6314 touch_positions.insert(touch.id, position);
6315 if active_primary_touch == Some(touch.id) {
6316 handle_cursor_moved(
6317 point.x,
6318 point.y,
6319 current_mods,
6320 &mut runtime,
6321 &pipeline,
6322 &effect_result_tx,
6323 &event_proxy,
6324 &async_registry,
6325 &mut active_services,
6326 &mut service_bindings,
6327 &mut next_service_instance_id,
6328 &window,
6329 elwt,
6330 &mut last_redraw_at,
6331 min_frame,
6332 &mut redraw_pending,
6333 &mut frame_trace,
6334 &mut invalidations,
6335 );
6336 }
6337 }
6338 TouchPhase::Ended | TouchPhase::Cancelled => {
6339 if active_primary_touch == Some(touch.id) {
6340 handle_cursor_moved(
6341 point.x,
6342 point.y,
6343 current_mods,
6344 &mut runtime,
6345 &pipeline,
6346 &effect_result_tx,
6347 &event_proxy,
6348 &async_registry,
6349 &mut active_services,
6350 &mut service_bindings,
6351 &mut next_service_instance_id,
6352 &window,
6353 elwt,
6354 &mut last_redraw_at,
6355 min_frame,
6356 &mut redraw_pending,
6357 &mut frame_trace,
6358 &mut invalidations,
6359 );
6360 handle_mouse_button(
6361 point.x,
6362 point.y,
6363 PointerButton::Primary,
6364 false,
6365 current_mods,
6366 &mut runtime,
6367 &pipeline,
6368 &effect_result_tx,
6369 &event_proxy,
6370 &async_registry,
6371 &mut active_services,
6372 &mut service_bindings,
6373 &mut next_service_instance_id,
6374 &window,
6375 elwt,
6376 &mut last_redraw_at,
6377 min_frame,
6378 &mut redraw_pending,
6379 text_trace_enabled,
6380 &mut pending_text_traces,
6381 &mut next_text_trace_seq,
6382 presented_frames,
6383 &mut last_blink_toggle,
6384 &mut frame_trace,
6385 &mut invalidations,
6386 );
6387 active_primary_touch = None;
6388 }
6389 touch_positions.remove(&touch.id);
6390 }
6391 }
6392 }
6393 WindowEvent::ModifiersChanged(modifiers) => {
6394 current_mods = 0;
6395 if modifiers.state().shift_key() {
6396 current_mods |= 1;
6397 }
6398 if modifiers.state().alt_key() {
6399 current_mods |= 2;
6400 }
6401 if modifiers.state().control_key() {
6402 current_mods |= 4;
6403 }
6404 if modifiers.state().super_key() {
6405 current_mods |= 8;
6406 }
6407 }
6408 WindowEvent::KeyboardInput { event, .. } => {
6409 #[cfg(target_arch = "wasm32")]
6410 pending_web_input_at.get_or_insert_with(Instant::now);
6411 if event.state.is_pressed() {
6412 use winit::keyboard::{Key, NamedKey};
6413 let key_code = match event.logical_key {
6414 Key::Named(NamedKey::Space) => Some(KeyCode::Space),
6415 Key::Named(NamedKey::Enter) => Some(KeyCode::Enter),
6416 Key::Named(NamedKey::Escape) => Some(KeyCode::Escape),
6417 Key::Named(NamedKey::Backspace) => Some(KeyCode::Backspace),
6418 Key::Named(NamedKey::Delete) => Some(KeyCode::Delete),
6419 Key::Named(NamedKey::Tab) => Some(KeyCode::Tab),
6420 Key::Named(NamedKey::ArrowLeft) => Some(KeyCode::Left),
6421 Key::Named(NamedKey::ArrowRight) => Some(KeyCode::Right),
6422 Key::Named(NamedKey::ArrowUp) => Some(KeyCode::Up),
6423 Key::Named(NamedKey::ArrowDown) => Some(KeyCode::Down),
6424 Key::Named(NamedKey::Home) => Some(KeyCode::Home),
6425 Key::Named(NamedKey::End) => Some(KeyCode::End),
6426 Key::Named(NamedKey::PageUp) => Some(KeyCode::PageUp),
6427 Key::Named(NamedKey::PageDown) => Some(KeyCode::PageDown),
6428 _ => {
6429 if let Some(text) = &event.text {
6430 text.chars().next().map(KeyCode::Char)
6431 } else {
6432 None
6433 }
6434 }
6435 };
6436
6437 if let Some(code) = key_code {
6438 handle_key_down::<S>(
6439 code,
6440 current_mods,
6441 &mut runtime,
6442 &pipeline,
6443 &effect_result_tx,
6444 &event_proxy,
6445 &async_registry,
6446 &mut active_services,
6447 &mut service_bindings,
6448 &mut next_service_instance_id,
6449 &window,
6450 elwt,
6451 &mut last_redraw_at,
6452 min_frame,
6453 &mut redraw_pending,
6454 text_trace_enabled,
6455 &mut pending_text_traces,
6456 &mut next_text_trace_seq,
6457 presented_frames,
6458 &mut last_blink_toggle,
6459 self.key_handler.as_ref(),
6460 &mut frame_trace,
6461 &mut invalidations,
6462 );
6463 }
6464 }
6465 }
6466 WindowEvent::Ime(ime) => {
6467 #[cfg(target_arch = "wasm32")]
6468 pending_web_input_at.get_or_insert_with(Instant::now);
6469 if let (Some(ir), Some(layout)) =
6470 (&pipeline.prev_ir, &pipeline.last_snapshot)
6471 {
6472 let (input_event, source) = match ime {
6473 Ime::Commit(text) => (
6474 Some(InputEvent::Ime(
6475 fission_core::event::ImeEvent::Commit {
6476 text: text.clone(),
6477 },
6478 )),
6479 Some(format!("ime_commit:{}", text.chars().count())),
6480 ),
6481 Ime::Preedit(text, _) => (
6482 Some(InputEvent::Ime(
6483 fission_core::event::ImeEvent::Preedit {
6484 text: text.clone(),
6485 },
6486 )),
6487 Some(format!("ime_preedit:{}", text.chars().count())),
6488 ),
6489 _ => (None, None),
6490 };
6491
6492 if let Some(e) = input_event {
6493 let target = focused_text_input_id(
6494 &runtime,
6495 pipeline.prev_ir.as_ref(),
6496 );
6497 let trace_seq = start_text_trace(
6498 text_trace_enabled && target.is_some(),
6499 &mut pending_text_traces,
6500 &mut next_text_trace_seq,
6501 source.unwrap_or_else(|| "ime".to_string()),
6502 target,
6503 presented_frames,
6504 );
6505 runtime.handle_input(e, ir, layout).ok();
6506 invalidations.mark_build();
6507 mark_text_trace_handled(
6508 &mut pending_text_traces,
6509 trace_seq,
6510 );
6511 if process_pending_effects(
6512 &mut runtime,
6513 &effect_result_tx,
6514 &event_proxy,
6515 &async_registry,
6516 &mut active_services,
6517 &mut service_bindings,
6518 &mut next_service_instance_id,
6519 ) {
6520 mark_text_trace_effects(
6521 &mut pending_text_traces,
6522 trace_seq,
6523 );
6524 invalidations.mark_build();
6525 request_redraw_logged(
6526 &window,
6527 elwt,
6528 &mut last_redraw_at,
6529 min_frame,
6530 &mut redraw_pending,
6531 &mut frame_trace,
6532 "ime:effects",
6533 );
6534 }
6535 reset_text_input_caret(
6536 &mut runtime,
6537 pipeline.prev_ir.as_ref(),
6538 &mut last_blink_toggle,
6539 );
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",
6548 );
6549 }
6550 }
6551 }
6552 _ => {}
6553 }
6554 }
6555 _ => {}
6556 }
6557 };
6558
6559 #[cfg(target_arch = "wasm32")]
6560 {
6561 event_loop.spawn(event_handler);
6562 Ok(())
6563 }
6564 #[cfg(not(target_arch = "wasm32"))]
6565 {
6566 event_loop
6567 .run(event_handler)
6568 .map_err(|e| anyhow::anyhow!("Event loop error: {}", e))
6569 }
6570 }
6571}
6572
6573fn build_font_context() -> FontContext {
6574 let use_system_fonts = std::env::var("FISSION_USE_SYSTEM_FONTS")
6575 .map(|v| matches!(v.to_ascii_lowercase().as_str(), "1" | "true" | "yes"))
6576 .unwrap_or(false);
6577 let options = CollectionOptions {
6578 shared: false,
6579 system_fonts: use_system_fonts,
6580 };
6581 FontContext {
6582 collection: Collection::new(options),
6583 source_cache: SourceCache::default(),
6584 }
6585}
6586
6587fn map_mouse_button(button: MouseButton) -> Option<PointerButton> {
6589 match button {
6590 MouseButton::Left => Some(PointerButton::Primary),
6591 MouseButton::Right => Some(PointerButton::Secondary),
6592 MouseButton::Middle => Some(PointerButton::Middle),
6593 MouseButton::Other(id) => Some(PointerButton::Other(id as u8)),
6594 _ => None,
6595 }
6596}
6597
6598fn clamp_copy_extent_to_texture(
6599 requested_width: u32,
6600 requested_height: u32,
6601 actual_width: u32,
6602 actual_height: u32,
6603) -> (u32, u32) {
6604 (
6605 requested_width.min(actual_width).max(1),
6606 requested_height.min(actual_height).max(1),
6607 )
6608}
6609
6610fn gpu_screenshot(
6611 device: &wgpu::Device,
6612 queue: &wgpu::Queue,
6613 texture: &wgpu::Texture,
6614 texture_width: u32,
6615 texture_height: u32,
6616 output_width: u32,
6617 output_height: u32,
6618 path: Option<&str>,
6619) -> fission_test_driver::TestResponse {
6620 let actual_texture_width = texture.width();
6621 let actual_texture_height = texture.height();
6622 let (texture_width, texture_height) = clamp_copy_extent_to_texture(
6623 texture_width,
6624 texture_height,
6625 actual_texture_width,
6626 actual_texture_height,
6627 );
6628 if output_width == 0 || output_height == 0 {
6629 return fission_test_driver::TestResponse::Error {
6630 message: "zero-size viewport".into(),
6631 };
6632 }
6633
6634 let bytes_per_pixel = 4u32;
6635 let unpadded_bytes_per_row = texture_width * bytes_per_pixel;
6636 let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT;
6637 let padded_bytes_per_row = (unpadded_bytes_per_row + align - 1) / align * align;
6638 let buffer_size = (padded_bytes_per_row * texture_height) as u64;
6639
6640 let staging = device.create_buffer(&wgpu::BufferDescriptor {
6641 label: Some("screenshot staging"),
6642 size: buffer_size,
6643 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
6644 mapped_at_creation: false,
6645 });
6646
6647 let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
6648 label: Some("screenshot copy"),
6649 });
6650
6651 encoder.copy_texture_to_buffer(
6652 wgpu::TexelCopyTextureInfo {
6653 texture,
6654 mip_level: 0,
6655 origin: wgpu::Origin3d::ZERO,
6656 aspect: wgpu::TextureAspect::All,
6657 },
6658 wgpu::TexelCopyBufferInfo {
6659 buffer: &staging,
6660 layout: wgpu::TexelCopyBufferLayout {
6661 offset: 0,
6662 bytes_per_row: Some(padded_bytes_per_row),
6663 rows_per_image: Some(texture_height),
6664 },
6665 },
6666 wgpu::Extent3d {
6667 width: texture_width,
6668 height: texture_height,
6669 depth_or_array_layers: 1,
6670 },
6671 );
6672
6673 queue.submit(Some(encoder.finish()));
6674
6675 let (tx, rx) = std::sync::mpsc::channel();
6676 staging
6677 .slice(..)
6678 .map_async(wgpu::MapMode::Read, move |result| {
6679 let _ = tx.send(result);
6680 });
6681 let _ = device.poll(wgpu::PollType::Wait);
6682
6683 match rx.recv() {
6684 Ok(Ok(())) => {}
6685 Ok(Err(e)) => {
6686 return fission_test_driver::TestResponse::Error {
6687 message: format!("buffer map failed: {:?}", e),
6688 };
6689 }
6690 Err(e) => {
6691 return fission_test_driver::TestResponse::Error {
6692 message: format!("buffer map channel error: {}", e),
6693 };
6694 }
6695 }
6696
6697 let data = staging.slice(..).get_mapped_range();
6698
6699 let mut rgba = Vec::with_capacity((texture_width * texture_height * 4) as usize);
6701 for row in 0..texture_height {
6702 let start = (row * padded_bytes_per_row) as usize;
6703 let end = start + (texture_width * bytes_per_pixel) as usize;
6704 rgba.extend_from_slice(&data[start..end]);
6705 }
6706
6707 drop(data);
6708 staging.unmap();
6709
6710 let (rgba, width, height) = if texture_width == output_width && texture_height == output_height
6711 {
6712 (rgba, texture_width, texture_height)
6713 } else if let Some(resized) = downscale_rgba_box(
6714 &rgba,
6715 texture_width,
6716 texture_height,
6717 output_width,
6718 output_height,
6719 ) {
6720 (resized, output_width, output_height)
6721 } else {
6722 let Some(image) = image::RgbaImage::from_raw(texture_width, texture_height, rgba) else {
6723 return fission_test_driver::TestResponse::Error {
6724 message: "failed to decode screenshot RGBA buffer".into(),
6725 };
6726 };
6727 let resized = image::imageops::resize(
6728 &image,
6729 output_width,
6730 output_height,
6731 image::imageops::FilterType::Triangle,
6732 );
6733 (resized.into_raw(), output_width, output_height)
6734 };
6735
6736 let mut png = Vec::new();
6737 {
6738 use image::ImageEncoder;
6739 let encoder = image::codecs::png::PngEncoder::new(&mut png);
6740 if let Err(e) = encoder.write_image(&rgba, width, height, image::ExtendedColorType::Rgba8) {
6741 return fission_test_driver::TestResponse::Error {
6742 message: format!("PNG encode failed: {}", e),
6743 };
6744 }
6745 }
6746
6747 if let Some(path) = path {
6748 match std::fs::write(path, &png) {
6749 Ok(()) => fission_test_driver::TestResponse::Ok {},
6750 Err(e) => fission_test_driver::TestResponse::Error {
6751 message: format!("PNG save failed: {}", e),
6752 },
6753 }
6754 } else {
6755 fission_test_driver::TestResponse::Screenshot {
6756 png_base64: base64::engine::general_purpose::STANDARD.encode(png),
6757 width,
6758 height,
6759 }
6760 }
6761}
6762
6763fn downscale_rgba_box(
6764 rgba: &[u8],
6765 input_width: u32,
6766 input_height: u32,
6767 output_width: u32,
6768 output_height: u32,
6769) -> Option<Vec<u8>> {
6770 if output_width == 0
6771 || output_height == 0
6772 || input_width % output_width != 0
6773 || input_height % output_height != 0
6774 {
6775 return None;
6776 }
6777
6778 let scale_x = input_width / output_width;
6779 let scale_y = input_height / output_height;
6780 if scale_x <= 1 && scale_y <= 1 {
6781 return None;
6782 }
6783
6784 let samples_per_pixel = scale_x.checked_mul(scale_y)?;
6785 let mut out = vec![0u8; (output_width * output_height * 4) as usize];
6786
6787 for out_y in 0..output_height {
6788 let src_y0 = out_y * scale_y;
6789 for out_x in 0..output_width {
6790 let src_x0 = out_x * scale_x;
6791 let mut sum = [0u32; 4];
6792 for dy in 0..scale_y {
6793 let src_y = src_y0 + dy;
6794 let row_offset = ((src_y * input_width) * 4) as usize;
6795 for dx in 0..scale_x {
6796 let src_x = src_x0 + dx;
6797 let src_index = row_offset + (src_x * 4) as usize;
6798 sum[0] += rgba[src_index] as u32;
6799 sum[1] += rgba[src_index + 1] as u32;
6800 sum[2] += rgba[src_index + 2] as u32;
6801 sum[3] += rgba[src_index + 3] as u32;
6802 }
6803 }
6804
6805 let dst_index = (((out_y * output_width) + out_x) * 4) as usize;
6806 out[dst_index] = (sum[0] / samples_per_pixel) as u8;
6807 out[dst_index + 1] = (sum[1] / samples_per_pixel) as u8;
6808 out[dst_index + 2] = (sum[2] / samples_per_pixel) as u8;
6809 out[dst_index + 3] = (sum[3] / samples_per_pixel) as u8;
6810 }
6811 }
6812
6813 Some(out)
6814}
6815
6816fn layout_size_to_image_dimensions(size: LayoutSize) -> (u32, u32) {
6817 let width = size.width.max(1.0).round() as u32;
6818 let height = size.height.max(1.0).round() as u32;
6819 (width.max(1), height.max(1))
6820}
6821
6822fn normalize_scale_factor(scale_factor: f64) -> f64 {
6823 if scale_factor.is_finite() && scale_factor > 0.0 {
6824 scale_factor
6825 } else {
6826 1.0
6827 }
6828}
6829
6830#[cfg(target_os = "ios")]
6831fn ios_effective_scale_factor(reported_scale_factor: f64) -> f64 {
6832 std::env::var("FISSION_IOS_SCALE_FACTOR")
6833 .ok()
6834 .and_then(|value| value.parse::<f64>().ok())
6835 .filter(|scale| scale.is_finite() && *scale > 0.0)
6836 .unwrap_or_else(|| {
6837 if reported_scale_factor >= 2.0 {
6838 reported_scale_factor
6839 } else {
6840 3.0
6841 }
6842 })
6843}
6844
6845#[cfg(target_arch = "wasm32")]
6846fn web_browser_viewport_state() -> Option<WindowViewportState> {
6847 let window = web_sys::window()?;
6848 let width = window.inner_width().ok()?.as_f64()? as f32;
6849 let height = window.inner_height().ok()?.as_f64()? as f32;
6850 if !width.is_finite() || !height.is_finite() || width <= 0.0 || height <= 0.0 {
6851 return None;
6852 }
6853 let scale_factor = normalize_scale_factor(window.device_pixel_ratio());
6854 Some(WindowViewportState {
6855 physical_size: logical_viewport_to_physical_size(
6856 LayoutSize::new(width, height),
6857 scale_factor,
6858 ),
6859 scale_factor,
6860 })
6861}
6862
6863fn physical_size_to_layout_size(size: PhysicalSize<u32>, scale_factor: f64) -> LayoutSize {
6864 let scale_factor = normalize_scale_factor(scale_factor);
6865 LayoutSize {
6866 width: (size.width as f64 / scale_factor) as f32,
6867 height: (size.height as f64 / scale_factor) as f32,
6868 }
6869}
6870
6871fn logical_viewport_to_render_target_size(size: LayoutSize, scale_factor: f64) -> (u32, u32) {
6872 let scale_factor = normalize_scale_factor(scale_factor);
6873 let width = (size.width.max(1.0) as f64 * scale_factor).ceil() as u32;
6874 let height = (size.height.max(1.0) as f64 * scale_factor).ceil() as u32;
6875 (width.max(1), height.max(1))
6876}
6877
6878fn logical_viewport_to_physical_size(size: LayoutSize, scale_factor: f64) -> PhysicalSize<u32> {
6879 let (width, height) = logical_viewport_to_render_target_size(size, scale_factor);
6880 PhysicalSize::new(width, height)
6881}
6882
6883fn recreate_target_texture(
6884 surface: &mut RenderSurface,
6885 render_cx: &RenderContext,
6886 width: u32,
6887 height: u32,
6888) {
6889 let device = &render_cx.devices[surface.dev_id].device;
6890 let size = wgpu::Extent3d {
6891 width: width.max(1),
6892 height: height.max(1),
6893 depth_or_array_layers: 1,
6894 };
6895 let new_texture = device.create_texture(&wgpu::TextureDescriptor {
6896 label: Some("fission_target_with_copy"),
6897 size,
6898 mip_level_count: 1,
6899 sample_count: 1,
6900 dimension: wgpu::TextureDimension::D2,
6901 format: wgpu::TextureFormat::Rgba8Unorm, usage: wgpu::TextureUsages::STORAGE_BINDING
6903 | wgpu::TextureUsages::TEXTURE_BINDING
6904 | wgpu::TextureUsages::RENDER_ATTACHMENT
6905 | wgpu::TextureUsages::COPY_SRC
6906 | wgpu::TextureUsages::COPY_DST,
6907 view_formats: &[],
6908 });
6909 let new_view = new_texture.create_view(&wgpu::TextureViewDescriptor::default());
6910 surface.target_texture = new_texture;
6911 surface.target_view = new_view;
6912}
6913
6914fn sync_tracked_target_texture_size_to_surface(
6915 target_texture_size: &mut (u32, u32),
6916 surface_size: PhysicalSize<u32>,
6917) {
6918 *target_texture_size = (surface_size.width.max(1), surface_size.height.max(1));
6919}
6920
6921#[cfg(any(test, not(any(target_os = "android", target_os = "ios"))))]
6922fn native_window_size_for_logical_viewport(size: LayoutSize) -> winit::dpi::LogicalSize<f64> {
6923 winit::dpi::LogicalSize::new(size.width as f64, size.height as f64)
6924}
6925
6926#[cfg(test)]
6927mod tests {
6928 use super::{
6929 animation_redraw_interval, clamp_copy_extent_to_texture, collect_startup_deep_links_from,
6930 cursor_icon_for, downscale_rgba_box, layout_size_to_image_dimensions,
6931 logical_viewport_to_physical_size, logical_viewport_to_render_target_size,
6932 native_window_size_for_logical_viewport, normalize_scale_factor,
6933 normalize_winit_scroll_delta, physical_position_to_layout_point,
6934 physical_size_to_layout_size, rect_visible_in_scroll_ancestors,
6935 repeating_animation_redraw_interval, resize_is_unsettled, resolve_build_viewport,
6936 sync_tracked_target_texture_size_to_surface, texture_plans_fit_device_limits,
6937 visual_rect_for_node, window_insets_from_safe_area_frames, LiveResizeController,
6938 WindowViewportState,
6939 };
6940 use crate::pipeline::CompositorTexturePlan;
6941 use crate::InvalidationSet;
6942 use fission_core::env::{ActiveAnimation, AnimationStateMap, ScrollStateMap};
6943 use fission_core::{AnimationPropertyId, DeepLinkConfig, WidgetId};
6944 use fission_ir::semantics::MouseCursor;
6945 use fission_ir::{CoreIR, FlexDirection, LayoutOp, Op};
6946 use fission_layout::{LayoutNodeGeometry, LayoutRect, LayoutSize, LayoutSnapshot};
6947 use std::collections::HashMap;
6948 use std::time::Duration;
6949 use winit::dpi::{PhysicalPosition, PhysicalSize};
6950 use winit::event::MouseScrollDelta;
6951 use winit::window::CursorIcon;
6952
6953 #[test]
6954 fn semantic_cursor_icons_map_to_winit_icons() {
6955 assert_eq!(cursor_icon_for(MouseCursor::Default), CursorIcon::Default);
6956 assert_eq!(cursor_icon_for(MouseCursor::Pointer), CursorIcon::Pointer);
6957 assert_eq!(cursor_icon_for(MouseCursor::Text), CursorIcon::Text);
6958 assert_eq!(
6959 cursor_icon_for(MouseCursor::NotAllowed),
6960 CursorIcon::NotAllowed
6961 );
6962 assert_eq!(
6963 cursor_icon_for(MouseCursor::VerticalText),
6964 CursorIcon::VerticalText
6965 );
6966 }
6967
6968 #[test]
6969 fn winit_scroll_delta_normalizes_to_positive_down_and_right() {
6970 assert_eq!(
6971 normalize_winit_scroll_delta(&MouseScrollDelta::LineDelta(-1.0, -2.0), 1.0),
6972 (50.0, 100.0)
6973 );
6974 assert_eq!(
6975 normalize_winit_scroll_delta(
6976 &MouseScrollDelta::PixelDelta(PhysicalPosition::new(-20.0, -40.0)),
6977 2.0,
6978 ),
6979 (10.0, 20.0)
6980 );
6981 }
6982
6983 #[test]
6984 fn physical_input_position_maps_into_layout_space() {
6985 let point = physical_position_to_layout_point(
6986 PhysicalPosition::new(240.0, 360.0),
6987 2.0,
6988 PhysicalPosition::new(0, 0),
6989 );
6990 assert_eq!(point, fission_render::LayoutPoint::new(120.0, 180.0));
6991 }
6992
6993 #[test]
6994 fn physical_input_position_subtracts_content_origin_before_scaling() {
6995 let point = physical_position_to_layout_point(
6996 PhysicalPosition::new(240.0, 460.0),
6997 2.0,
6998 PhysicalPosition::new(0, 100),
6999 );
7000 assert_eq!(point, fission_render::LayoutPoint::new(120.0, 180.0));
7001 }
7002
7003 #[test]
7004 fn safe_area_frames_convert_to_logical_window_insets() {
7005 let insets = window_insets_from_safe_area_frames(
7006 PhysicalPosition::new(0, 177),
7007 PhysicalPosition::new(0, 0),
7008 PhysicalSize::new(1206, 2343),
7009 PhysicalSize::new(1206, 2622),
7010 3.0,
7011 );
7012
7013 assert_eq!(insets.left, 0.0);
7014 assert_eq!(insets.right, 0.0);
7015 assert_eq!(insets.top, 59.0);
7016 assert_eq!(insets.bottom, 34.0);
7017 }
7018
7019 #[test]
7020 fn visual_rect_subtracts_ancestor_scroll_offset() {
7021 let scroll = WidgetId::from_u128(1);
7022 let child = WidgetId::from_u128(2);
7023 let mut ir = CoreIR::new();
7024 ir.add_node(
7025 child,
7026 Op::Paint(fission_ir::PaintOp::DrawRect {
7027 fill: None,
7028 stroke: None,
7029 corner_radius: 0.0,
7030 shadow: None,
7031 }),
7032 Vec::new(),
7033 );
7034 ir.add_node(
7035 scroll,
7036 Op::Layout(LayoutOp::Scroll {
7037 direction: FlexDirection::Column,
7038 show_scrollbar: true,
7039 width: None,
7040 height: None,
7041 min_width: None,
7042 max_width: None,
7043 min_height: None,
7044 max_height: None,
7045 padding: [0.0; 4],
7046 flex_grow: 0.0,
7047 flex_shrink: 1.0,
7048 }),
7049 vec![child],
7050 );
7051 ir.set_root(scroll);
7052
7053 let mut snapshot = LayoutSnapshot::new(LayoutSize::new(100.0, 100.0));
7054 snapshot.nodes.insert(
7055 scroll,
7056 LayoutNodeGeometry {
7057 rect: LayoutRect::new(0.0, 0.0, 100.0, 100.0),
7058 content_size: LayoutSize::new(100.0, 400.0),
7059 },
7060 );
7061 snapshot.nodes.insert(
7062 child,
7063 LayoutNodeGeometry {
7064 rect: LayoutRect::new(0.0, 150.0, 80.0, 20.0),
7065 content_size: LayoutSize::new(80.0, 20.0),
7066 },
7067 );
7068 let mut scroll_map = ScrollStateMap::default();
7069 scroll_map.set_offset(scroll, 120.0);
7070
7071 let visual = visual_rect_for_node(&ir, &snapshot, &scroll_map, child).unwrap();
7072 assert_eq!(visual, LayoutRect::new(0.0, 30.0, 80.0, 20.0));
7073 assert!(rect_visible_in_scroll_ancestors(
7074 &ir,
7075 &snapshot,
7076 &scroll_map,
7077 child,
7078 visual
7079 ));
7080 }
7081
7082 #[test]
7083 fn repeating_animation_uses_reduced_frame_rate() {
7084 let min_frame = Duration::from_millis(16);
7085 let repeat_frame = Duration::from_millis(66);
7086 assert_eq!(
7087 animation_redraw_interval(false, Some(repeat_frame), false, min_frame),
7088 Some(repeat_frame)
7089 );
7090 }
7091
7092 #[test]
7093 fn finite_animation_keeps_full_frame_rate() {
7094 let min_frame = Duration::from_millis(16);
7095 assert_eq!(
7096 animation_redraw_interval(true, None, false, min_frame),
7097 Some(min_frame)
7098 );
7099 assert_eq!(
7100 animation_redraw_interval(false, None, true, min_frame),
7101 Some(min_frame)
7102 );
7103 }
7104
7105 #[test]
7106 fn idle_video_does_not_force_full_frame_rate() {
7107 let min_frame = Duration::from_millis(16);
7108 let repeat_frame = Duration::from_millis(66);
7109 assert_eq!(
7110 animation_redraw_interval(false, Some(repeat_frame), false, min_frame),
7111 Some(repeat_frame)
7112 );
7113 }
7114
7115 #[test]
7116 fn no_repeat_interval_means_no_idle_animation_redraw() {
7117 let min_frame = Duration::from_millis(16);
7118 assert_eq!(
7119 animation_redraw_interval(false, None, false, min_frame),
7120 None
7121 );
7122 }
7123
7124 #[test]
7125 fn repeat_animation_interval_uses_low_priority_hint() {
7126 let mut animation = AnimationStateMap::default();
7127 animation.active.insert(
7128 (
7129 WidgetId::explicit("spinner"),
7130 AnimationPropertyId::opacity(),
7131 ),
7132 ActiveAnimation {
7133 target: WidgetId::explicit("spinner"),
7134 property: AnimationPropertyId::opacity(),
7135 start_value: 0.3,
7136 end_value: 1.0,
7137 start_time: 0,
7138 duration: 600,
7139 repeat: true,
7140 frame_interval_ms: Some(166),
7141 easing: fission_core::EasingFunction::Linear,
7142 },
7143 );
7144 assert_eq!(
7145 repeating_animation_redraw_interval(&animation, Duration::from_millis(66)),
7146 Some(Duration::from_millis(166))
7147 );
7148 }
7149
7150 #[test]
7151 fn repeat_animation_interval_chooses_fastest_active_repeat() {
7152 let mut animation = AnimationStateMap {
7153 values: HashMap::new(),
7154 active: HashMap::new(),
7155 };
7156 animation.active.insert(
7157 (WidgetId::explicit("slow"), AnimationPropertyId::opacity()),
7158 ActiveAnimation {
7159 target: WidgetId::explicit("slow"),
7160 property: AnimationPropertyId::opacity(),
7161 start_value: 0.3,
7162 end_value: 1.0,
7163 start_time: 0,
7164 duration: 600,
7165 repeat: true,
7166 frame_interval_ms: Some(200),
7167 easing: fission_core::EasingFunction::Linear,
7168 },
7169 );
7170 animation.active.insert(
7171 (WidgetId::explicit("fast"), AnimationPropertyId::opacity()),
7172 ActiveAnimation {
7173 target: WidgetId::explicit("fast"),
7174 property: AnimationPropertyId::opacity(),
7175 start_value: 0.3,
7176 end_value: 1.0,
7177 start_time: 0,
7178 duration: 600,
7179 repeat: true,
7180 frame_interval_ms: Some(100),
7181 easing: fission_core::EasingFunction::Linear,
7182 },
7183 );
7184 assert_eq!(
7185 repeating_animation_redraw_interval(&animation, Duration::from_millis(66)),
7186 Some(Duration::from_millis(100))
7187 );
7188 }
7189
7190 #[test]
7191 fn live_resize_reports_unsettled_until_deadline() {
7192 let settle = Duration::from_millis(90);
7193 let mut resize = LiveResizeController::new(settle);
7194 let now = std::time::Instant::now();
7195 resize.note_resize(now);
7196
7197 assert!(resize.is_live(now + Duration::from_millis(30)));
7198 assert!(resize_is_unsettled(
7199 false,
7200 false,
7201 resize.is_live(now + Duration::from_millis(30))
7202 ));
7203 assert!(!resize.is_live(now + Duration::from_millis(95)));
7204 }
7205
7206 #[test]
7207 fn viewport_resize_forces_build_viewport_refresh() {
7208 let target = LayoutSize::new(1440.0, 900.0);
7209 let mut invalidations = InvalidationSet::default();
7210
7211 let build_viewport = resolve_build_viewport(
7212 Some(LayoutSize::new(1024.0, 768.0)),
7213 target,
7214 true,
7215 &mut invalidations,
7216 );
7217
7218 assert!(invalidations.build);
7219 assert_eq!(build_viewport, target);
7220 }
7221
7222 #[test]
7223 fn stable_viewport_preserves_existing_build_viewport() {
7224 let target = LayoutSize::new(1024.0, 768.0);
7225 let mut invalidations = InvalidationSet::default();
7226
7227 let build_viewport = resolve_build_viewport(Some(target), target, true, &mut invalidations);
7228
7229 assert!(!invalidations.build);
7230 assert_eq!(build_viewport, target);
7231 }
7232
7233 #[test]
7234 fn oversized_texture_plan_forces_scene_fallback() {
7235 let plans = vec![CompositorTexturePlan {
7236 key: 1,
7237 bounds: LayoutRect::new(0.0, 0.0, 320.0, 9000.0),
7238 scene: Some(fission_render::RenderScene::new(LayoutRect::new(
7239 0.0, 0.0, 320.0, 9000.0,
7240 ))),
7241 scene_cache_key: Some(1),
7242 content_key: 1,
7243 local_dynamic: false,
7244 composite_dynamic: false,
7245 opacity: 1.0,
7246 transform: None,
7247 transform_clip: false,
7248 clip: None,
7249 children: Vec::new(),
7250 source_layer_path: None,
7251 }];
7252 assert!(!texture_plans_fit_device_limits(&plans, 1.0, 8192));
7253 }
7254
7255 #[test]
7256 fn nested_texture_plans_must_all_fit_device_limits() {
7257 let child = CompositorTexturePlan {
7258 key: 2,
7259 bounds: LayoutRect::new(0.0, 0.0, 400.0, 8400.0),
7260 scene: Some(fission_render::RenderScene::new(LayoutRect::new(
7261 0.0, 0.0, 400.0, 8400.0,
7262 ))),
7263 scene_cache_key: Some(2),
7264 content_key: 2,
7265 local_dynamic: false,
7266 composite_dynamic: false,
7267 opacity: 1.0,
7268 transform: None,
7269 transform_clip: false,
7270 clip: None,
7271 children: Vec::new(),
7272 source_layer_path: None,
7273 };
7274 let plans = vec![CompositorTexturePlan {
7275 key: 1,
7276 bounds: LayoutRect::new(0.0, 0.0, 800.0, 600.0),
7277 scene: None,
7278 scene_cache_key: None,
7279 content_key: 3,
7280 local_dynamic: false,
7281 composite_dynamic: false,
7282 opacity: 1.0,
7283 transform: None,
7284 transform_clip: false,
7285 clip: None,
7286 children: vec![child],
7287 source_layer_path: None,
7288 }];
7289 assert!(!texture_plans_fit_device_limits(&plans, 1.0, 8192));
7290 }
7291
7292 #[test]
7293 fn screenshot_dimensions_follow_logical_viewport() {
7294 let dims = layout_size_to_image_dimensions(fission_layout::LayoutSize::new(1600.0, 1200.0));
7295 assert_eq!(dims, (1600, 1200));
7296
7297 let rounded =
7298 layout_size_to_image_dimensions(fission_layout::LayoutSize::new(999.6, 700.4));
7299 assert_eq!(rounded, (1000, 700));
7300 }
7301
7302 #[test]
7303 fn simulated_resize_uses_physical_render_target_size() {
7304 let dims = logical_viewport_to_render_target_size(
7305 fission_layout::LayoutSize::new(1600.0, 1200.0),
7306 2.0,
7307 );
7308 assert_eq!(dims, (3200, 2400));
7309
7310 let fractional = logical_viewport_to_render_target_size(
7311 fission_layout::LayoutSize::new(430.0, 900.0),
7312 1.5,
7313 );
7314 assert_eq!(fractional, (645, 1350));
7315 }
7316
7317 #[test]
7318 fn physical_viewport_maps_to_logical_size_with_scale_factor() {
7319 let logical = physical_size_to_layout_size(PhysicalSize::new(1728, 1117), 1.5);
7320 assert_eq!(logical.width, 1152.0);
7321 assert!((logical.height - 744.6667).abs() < 0.001);
7322 }
7323
7324 #[test]
7325 fn scale_factor_change_preserves_logical_viewport_until_resize_arrives() {
7326 let viewport = WindowViewportState {
7327 physical_size: PhysicalSize::new(1600, 1200),
7328 scale_factor: 1.0,
7329 }
7330 .with_scale_factor(2.0);
7331
7332 assert_eq!(viewport.physical_size, PhysicalSize::new(3200, 2400));
7333 assert_eq!(
7334 viewport.logical_size(),
7335 fission_layout::LayoutSize::new(1600.0, 1200.0)
7336 );
7337 }
7338
7339 #[test]
7340 fn resized_event_overrides_scale_factor_prediction_authoritatively() {
7341 let viewport = WindowViewportState {
7342 physical_size: PhysicalSize::new(1600, 1200),
7343 scale_factor: 1.0,
7344 }
7345 .with_scale_factor(1.5)
7346 .with_physical_size(PhysicalSize::new(2412, 1809));
7347
7348 assert_eq!(viewport.physical_size, PhysicalSize::new(2412, 1809));
7349 assert_eq!(
7350 viewport.logical_size(),
7351 fission_layout::LayoutSize::new(1608.0, 1206.0)
7352 );
7353 }
7354
7355 #[test]
7356 fn fractional_logical_viewports_round_up_for_render_targets() {
7357 let physical =
7358 logical_viewport_to_physical_size(fission_layout::LayoutSize::new(430.2, 900.1), 1.5);
7359 assert_eq!(physical, PhysicalSize::new(646, 1351));
7360 }
7361
7362 #[test]
7363 fn scale_factor_prediction_never_undershoots_fractional_viewports() {
7364 let initial = WindowViewportState {
7365 physical_size: PhysicalSize::new(1728, 1117),
7366 scale_factor: 1.5,
7367 };
7368 let predicted = initial.with_scale_factor(2.0);
7369
7370 assert_eq!(predicted.physical_size, PhysicalSize::new(2304, 1490));
7371 assert!(predicted.logical_size().width >= initial.logical_size().width);
7372 assert!(predicted.logical_size().height >= initial.logical_size().height);
7373 }
7374
7375 #[test]
7376 fn logical_resize_updates_native_viewport_prediction() {
7377 let initial = WindowViewportState {
7378 physical_size: PhysicalSize::new(800, 632),
7379 scale_factor: 2.0,
7380 };
7381 let resized = initial.with_logical_size(fission_layout::LayoutSize::new(1600.0, 1200.0));
7382
7383 assert_eq!(resized.physical_size, PhysicalSize::new(3200, 2400));
7384 assert_eq!(
7385 resized.logical_size(),
7386 fission_layout::LayoutSize::new(1600.0, 1200.0)
7387 );
7388 }
7389
7390 #[test]
7391 fn logical_resize_requests_logical_window_dimensions() {
7392 let requested = native_window_size_for_logical_viewport(fission_layout::LayoutSize::new(
7393 1600.0, 2200.0,
7394 ));
7395
7396 assert_eq!(requested.width, 1600.0);
7397 assert_eq!(requested.height, 2200.0);
7398 }
7399
7400 #[test]
7401 fn invalid_scale_factors_fall_back_to_unit_scale() {
7402 assert_eq!(normalize_scale_factor(0.0), 1.0);
7403 assert_eq!(normalize_scale_factor(-2.0), 1.0);
7404 assert_eq!(normalize_scale_factor(f64::NAN), 1.0);
7405 assert_eq!(normalize_scale_factor(f64::INFINITY), 1.0);
7406 assert_eq!(normalize_scale_factor(1.5), 1.5);
7407 }
7408
7409 #[test]
7410 fn invalid_scale_factor_does_not_shrink_viewport_math() {
7411 let logical = physical_size_to_layout_size(PhysicalSize::new(1600, 1200), 0.0);
7412 assert_eq!(logical, fission_layout::LayoutSize::new(1600.0, 1200.0));
7413
7414 let render_target = logical_viewport_to_render_target_size(
7415 fission_layout::LayoutSize::new(1600.0, 1200.0),
7416 0.0,
7417 );
7418 assert_eq!(render_target, (1600, 1200));
7419 }
7420
7421 #[test]
7422 fn surface_resize_resets_custom_target_texture_tracking() {
7423 let mut tracked_target_texture_size = (1600, 1200);
7424
7425 sync_tracked_target_texture_size_to_surface(
7426 &mut tracked_target_texture_size,
7427 PhysicalSize::new(1055, 791),
7428 );
7429
7430 assert_eq!(tracked_target_texture_size, (1055, 791));
7431 assert_ne!(
7432 tracked_target_texture_size,
7433 logical_viewport_to_render_target_size(
7434 fission_layout::LayoutSize::new(1600.0, 1200.0),
7435 1.0,
7436 )
7437 );
7438 }
7439
7440 #[test]
7441 fn resize_settle_signal_tracks_real_resize_state() {
7442 assert!(resize_is_unsettled(true, false, false));
7443 assert!(resize_is_unsettled(false, true, false));
7444 assert!(resize_is_unsettled(false, false, true));
7445 assert!(!resize_is_unsettled(false, false, false));
7446 }
7447
7448 #[test]
7449 fn screenshot_copy_extent_never_exceeds_texture_bounds() {
7450 assert_eq!(
7451 clamp_copy_extent_to_texture(1600, 1200, 1055, 791),
7452 (1055, 791)
7453 );
7454 assert_eq!(clamp_copy_extent_to_texture(0, 0, 1055, 791), (1, 1));
7455 assert_eq!(
7456 clamp_copy_extent_to_texture(640, 480, 1055, 791),
7457 (640, 480)
7458 );
7459 }
7460
7461 #[test]
7462 fn integer_downscale_uses_fast_box_path() {
7463 let rgba = vec![
7464 10, 20, 30, 255, 30, 40, 50, 255, 50, 60, 70, 255, 70, 80, 90, 255,
7465 ];
7466 let downscaled = downscale_rgba_box(&rgba, 2, 2, 1, 1).expect("downscale");
7467 assert_eq!(downscaled, vec![40, 50, 60, 255]);
7468 }
7469
7470 #[test]
7471 fn startup_deep_link_collection_filters_to_declared_config() {
7472 let config = DeepLinkConfig::new()
7473 .scheme("fission")
7474 .domain("example.com")
7475 .path_prefix("/tasks");
7476
7477 let links = collect_startup_deep_links_from(
7478 &config,
7479 vec![
7480 "--ignored".to_string(),
7481 "fission://open/tasks/1".to_string(),
7482 "other://open/tasks/1".to_string(),
7483 ],
7484 vec!["https://example.com/tasks/2?source=email".to_string()],
7485 );
7486
7487 assert_eq!(links.len(), 2);
7488 assert_eq!(links[0].url, "https://example.com/tasks/2?source=email");
7489 assert!(links[0].cold_start);
7490 assert_eq!(links[1].url, "fission://open/tasks/1");
7491 assert!(links[1].cold_start);
7492 }
7493}