1use anyhow::Result;
11use std::collections::{HashMap, VecDeque};
12use std::num::NonZeroU32;
13use std::sync::mpsc;
14use std::sync::{Arc, Mutex};
15use std::time::{Duration, Instant};
16use winit::{
17 dpi::PhysicalPosition,
18 event::{ElementState, Event, Ime, KeyEvent, MouseButton, MouseScrollDelta, WindowEvent},
19 event_loop::{ControlFlow, EventLoop, EventLoopWindowTarget},
20 keyboard::PhysicalKey,
21 window::{Window, WindowBuilder},
22};
23
24use fission_core::env::{VideoState, VideoStateMap, VideoStatus};
25use fission_core::lowering::{build_layout_tree, LoweringContext};
26use fission_core::{
27 Action, ActionId, AppState, BuildCtx, Clock, Env, ImeHandler, InputEvent, KeyCode,
28 KeyEvent as FissionKeyEvent, Lower, Node, PointerButton, PointerEvent, Runtime, ScrollStateMap,
29 View, Widget,
30};
31use fission_core::{ActionInput, Effect, EffectPayload, SystemEffect};
32use fission_diagnostics::prelude as diag;
33use fission_ir::{op::Color as IrColor, CoreIR, FlexDirection, NodeId, Op, PaintOp, WidgetNodeId};
34use fission_layout::{LayoutEngine, LayoutSize};
35use fission_render::{
36 Color as RenderColor, DisplayList, LayoutPoint, LayoutRect, LayoutUnit, Renderer,
37};
38use fission_render_vello::parley::FontContext;
39use fission_render_vello::{VelloRenderer, VelloTextMeasurer};
40use fission_shell::{Platform, VideoBackend, VideoEvent, VideoPlayer};
41use fission_theme::fonts;
42use fontique::{Blob, Collection, CollectionOptions, FontInfoOverride, SourceCache};
43
44use pollster::block_on;
46use vello::util::{RenderContext, RenderSurface};
47use vello::wgpu;
48use vello::{AaConfig, AaSupport, Renderer as VelloSceneRenderer, RendererOptions, Scene};
49
50mod pipeline;
51pub use pipeline::Pipeline;
52mod video_backend;
53#[cfg(target_os = "macos")]
54use video_backend::MacVideoBackend;
55#[cfg(not(target_os = "macos"))]
56use video_backend::MockVideoBackend;
57
58mod clipboard;
59use clipboard::DesktopClipboard;
60mod ime;
61use ime::DesktopImeHandler;
62pub mod test_control;
63
64use fission_core::action::ActionEnvelope;
65
66type EffectResult = (
70 u64,
71 std::result::Result<EffectPayload, String>,
72 Option<ActionEnvelope>,
73 Option<ActionEnvelope>,
74);
75
76pub type AppEffectHandler = Box<
81 dyn Fn(Vec<u8>, u64, Option<ActionEnvelope>, Option<ActionEnvelope>, mpsc::Sender<EffectResult>)
82 + Send
83 + Sync,
84>;
85
86struct ActivePlayer {
87 player: Box<dyn VideoPlayer>,
88 last_status: Option<VideoStatus>,
89 last_rate: Option<f32>,
90 last_volume: Option<f32>,
91 last_muted: Option<bool>,
92}
93
94fn request_redraw_throttled(
95 window: &Window,
96 elwt: &EventLoopWindowTarget<()>,
97 last_redraw_at: &mut Instant,
98 min_frame: Duration,
99 redraw_pending: &mut bool,
100) {
101 let now = Instant::now();
102 let next = *last_redraw_at + min_frame;
103 if now >= next {
104 *last_redraw_at = now;
105 *redraw_pending = false;
106 window.request_redraw();
107 } else {
108 *redraw_pending = true;
109 elwt.set_control_flow(ControlFlow::WaitUntil(next));
110 }
111}
112
113fn process_pending_effects(
119 runtime: &mut Runtime,
120 effect_tx: &mpsc::Sender<EffectResult>,
121 app_effect_handler: Option<&AppEffectHandler>,
122) -> bool {
123 use std::process::Command;
124
125 let pending = std::mem::take(&mut runtime.pending_effects);
126 if pending.is_empty() {
127 return false;
128 }
129
130 let mut dispatched_callback = false;
131
132 for env in pending {
133 match env.effect {
134 Effect::System(ref system) => {
135 match system {
136 SystemEffect::OpenUrl { url, in_app } => {
138 diag::emit(
139 diag::DiagCategory::Input,
140 diag::DiagLevel::Info,
141 diag::DiagEventKind::InputEvent {
142 kind: format!("system_effect:OpenUrl in_app={}", in_app),
143 target: None,
144 position: None,
145 },
146 );
147
148 let result = if cfg!(target_os = "macos") {
149 Command::new("open").arg(url).spawn().map(|_| ())
150 } else if cfg!(target_os = "windows") {
151 Command::new("cmd")
152 .args(["/C", "start", url])
153 .spawn()
154 .map(|_| ())
155 } else {
156 Command::new("xdg-open").arg(url).spawn().map(|_| ())
157 };
158
159 if let Err(e) = result {
160 diag::emit(
161 diag::DiagCategory::Input,
162 diag::DiagLevel::Error,
163 diag::DiagEventKind::InputEvent {
164 kind: format!("system_effect:OpenUrl failed: {}", e),
165 target: None,
166 position: None,
167 },
168 );
169 }
170
171 if let Some(on_ok) = env.on_ok {
173 let _ = runtime.dispatch_with_input(
174 on_ok,
175 NodeId::derived(0, &[0]),
176 &ActionInput::EffectOk {
177 req_id: env.req_id,
178 payload: EffectPayload::Empty,
179 },
180 );
181 dispatched_callback = true;
182 }
183 }
184
185 SystemEffect::Authenticate { url, .. } => {
187 diag::emit(
188 diag::DiagCategory::Input,
189 diag::DiagLevel::Info,
190 diag::DiagEventKind::InputEvent {
191 kind: "system_effect:Authenticate".into(),
192 target: None,
193 position: None,
194 },
195 );
196 let _ = if cfg!(target_os = "macos") {
197 Command::new("open").arg(url).spawn()
198 } else if cfg!(target_os = "windows") {
199 Command::new("cmd").args(["/C", "start", url]).spawn()
200 } else {
201 Command::new("xdg-open").arg(url).spawn()
202 };
203
204 if let Some(on_ok) = env.on_ok {
205 let _ = runtime.dispatch_with_input(
206 on_ok,
207 NodeId::derived(0, &[0]),
208 &ActionInput::EffectOk {
209 req_id: env.req_id,
210 payload: EffectPayload::Empty,
211 },
212 );
213 dispatched_callback = true;
214 }
215 }
216
217 SystemEffect::Alert { title, message } => {
219 diag::emit(
220 diag::DiagCategory::Input,
221 diag::DiagLevel::Info,
222 diag::DiagEventKind::InputEvent {
223 kind: format!("system_effect:Alert title={}", title),
224 target: None,
225 position: None,
226 },
227 );
228 eprintln!("[alert] {}: {}", title, message);
229
230 if let Some(on_ok) = env.on_ok {
231 let _ = runtime.dispatch_with_input(
232 on_ok,
233 NodeId::derived(0, &[0]),
234 &ActionInput::EffectOk {
235 req_id: env.req_id,
236 payload: EffectPayload::Empty,
237 },
238 );
239 dispatched_callback = true;
240 }
241 }
242
243 SystemEffect::FileRead { path } => {
245 let tx = effect_tx.clone();
246 let on_ok = env.on_ok.clone();
247 let on_err = env.on_err.clone();
248 let req_id = env.req_id;
249 let path = path.clone();
250 std::thread::spawn(move || {
251 match std::fs::read_to_string(&path) {
252 Ok(content) => {
253 let payload = EffectPayload::InlineBytes(content.into_bytes());
254 let _ = tx.send((req_id, Ok(payload), on_ok, on_err));
255 }
256 Err(e) => {
257 let _ = tx.send((req_id, Err(e.to_string()), on_ok, on_err));
258 }
259 }
260 });
261 }
262
263 SystemEffect::HttpGet { url, headers } => {
265 let tx = effect_tx.clone();
266 let on_ok = env.on_ok.clone();
267 let on_err = env.on_err.clone();
268 let req_id = env.req_id;
269 let url = url.clone();
270 let headers = headers.clone();
271 std::thread::spawn(move || {
272 let result = (|| -> std::result::Result<Vec<u8>, String> {
275 use std::io::{Read, Write};
276 use std::net::TcpStream;
277
278 let url_trimmed = url.trim();
280 let (scheme, rest) = if let Some(r) = url_trimmed.strip_prefix("https://") {
281 ("https", r)
282 } else if let Some(r) = url_trimmed.strip_prefix("http://") {
283 ("http", r)
284 } else {
285 return Err(format!("unsupported URL scheme: {}", url_trimmed));
286 };
287
288 let (host_port, path) = match rest.find('/') {
289 Some(i) => (&rest[..i], &rest[i..]),
290 None => (rest, "/"),
291 };
292
293 let default_port: u16 = if scheme == "https" { 443 } else { 80 };
294 let (host, port) = match host_port.rfind(':') {
295 Some(i) => {
296 let p = host_port[i + 1..].parse::<u16>().unwrap_or(default_port);
297 (&host_port[..i], p)
298 }
299 None => (host_port, default_port),
300 };
301
302 if scheme == "https" {
303 return Err("HTTPS not supported in minimal HttpGet executor; use a custom app effect handler for TLS".into());
304 }
305
306 let addr = format!("{}:{}", host, port);
307 let mut stream = TcpStream::connect(&addr)
308 .map_err(|e| format!("connect {}: {}", addr, e))?;
309 stream
310 .set_read_timeout(Some(std::time::Duration::from_secs(30)))
311 .ok();
312
313 let mut request = format!(
314 "GET {} HTTP/1.1\r\nHost: {}\r\nConnection: close\r\n",
315 path, host
316 );
317 for (k, v) in &headers {
318 request.push_str(&format!("{}: {}\r\n", k, v));
319 }
320 request.push_str("\r\n");
321
322 stream
323 .write_all(request.as_bytes())
324 .map_err(|e| format!("write: {}", e))?;
325
326 let mut buf = Vec::new();
327 stream
328 .read_to_end(&mut buf)
329 .map_err(|e| format!("read: {}", e))?;
330
331 let body_start = buf
333 .windows(4)
334 .position(|w| w == b"\r\n\r\n")
335 .map(|i| i + 4)
336 .unwrap_or(0);
337
338 Ok(buf[body_start..].to_vec())
339 })();
340
341 match result {
342 Ok(bytes) => {
343 let payload = EffectPayload::InlineBytes(bytes);
344 let _ = tx.send((req_id, Ok(payload), on_ok, on_err));
345 }
346 Err(msg) => {
347 let _ = tx.send((req_id, Err(msg), on_ok, on_err));
348 }
349 }
350 });
351 }
352
353 SystemEffect::Cancel { .. } | SystemEffect::ReleaseResource { .. } => {
355 diag::emit(
356 diag::DiagCategory::Input,
357 diag::DiagLevel::Debug,
358 diag::DiagEventKind::InputEvent {
359 kind: format!("system_effect:{:?} (no-op)", system),
360 target: None,
361 position: None,
362 },
363 );
364 }
365 }
366 }
367
368 Effect::App(payload) => {
370 if let Some(handler) = app_effect_handler {
371 handler(
372 payload,
373 env.req_id,
374 env.on_ok.clone(),
375 env.on_err.clone(),
376 effect_tx.clone(),
377 );
378 } else {
379 diag::emit(
380 diag::DiagCategory::Input,
381 diag::DiagLevel::Warn,
382 diag::DiagEventKind::InputEvent {
383 kind: "app_effect:unhandled (no handler registered)".into(),
384 target: None,
385 position: None,
386 },
387 );
388 }
389 }
390 }
391 }
392
393 dispatched_callback
394}
395
396fn drain_effect_results(
401 runtime: &mut Runtime,
402 effect_rx: &mpsc::Receiver<EffectResult>,
403) -> bool {
404 let mut dispatched = false;
405
406 while let Ok((req_id, result, on_ok, on_err)) = effect_rx.try_recv() {
407 match result {
408 Ok(payload) => {
409 if let Some(action) = on_ok {
410 let _ = runtime.dispatch_with_input(
411 action,
412 NodeId::derived(0, &[0]),
413 &ActionInput::EffectOk { req_id, payload },
414 );
415 dispatched = true;
416 }
417 }
418 Err(msg) => {
419 if let Some(action) = on_err {
420 let _ = runtime.dispatch_with_input(
421 action,
422 NodeId::derived(0, &[0]),
423 &ActionInput::EffectErr {
424 req_id,
425 message: msg,
426 },
427 );
428 dispatched = true;
429 }
430 }
431 }
432 }
433
434 dispatched
435}
436
437fn focused_text_input_id(runtime: &Runtime, ir: Option<&CoreIR>) -> Option<NodeId> {
438 let focused = runtime.runtime_state.interaction.focused?;
439 let ir = ir?;
440 let mut current = Some(focused);
441 while let Some(id) = current {
442 let node = ir.nodes.get(&id)?;
443 if let Op::Semantics(sem) = &node.op {
444 if sem.role == fission_ir::Role::TextInput {
445 return Some(id);
446 }
447 }
448 current = node.parent;
449 }
450 None
451}
452
453fn reset_text_input_caret(
454 runtime: &mut Runtime,
455 ir: Option<&CoreIR>,
456 last_blink_toggle: &mut Instant,
457) {
458 if let Some(id) = focused_text_input_id(runtime, ir) {
459 runtime.runtime_state.caret_visible.insert(id, true);
460 *last_blink_toggle = Instant::now();
461 }
462}
463
464#[derive(Debug, Clone)]
465struct PendingTextTrace {
466 seq: u64,
467 source: String,
468 target: Option<NodeId>,
469 started_at: Instant,
470 handled_at: Option<Instant>,
471 effects_at: Option<Instant>,
472 present_after_frame: u64,
473}
474
475fn start_text_trace(
476 enabled: bool,
477 traces: &mut VecDeque<PendingTextTrace>,
478 next_seq: &mut u64,
479 source: String,
480 target: Option<NodeId>,
481 presented_frames: u64,
482) -> Option<u64> {
483 if !enabled {
484 return None;
485 }
486 *next_seq += 1;
487 let seq = *next_seq;
488 traces.push_back(PendingTextTrace {
489 seq,
490 source,
491 target,
492 started_at: Instant::now(),
493 handled_at: None,
494 effects_at: None,
495 present_after_frame: presented_frames + 1,
496 });
497 Some(seq)
498}
499
500fn mark_text_trace_handled(traces: &mut VecDeque<PendingTextTrace>, seq: Option<u64>) {
501 if let Some(seq) = seq {
502 if let Some(trace) = traces.iter_mut().rev().find(|trace| trace.seq == seq) {
503 trace.handled_at = Some(Instant::now());
504 }
505 }
506}
507
508fn mark_text_trace_effects(traces: &mut VecDeque<PendingTextTrace>, seq: Option<u64>) {
509 if let Some(seq) = seq {
510 if let Some(trace) = traces.iter_mut().rev().find(|trace| trace.seq == seq) {
511 trace.effects_at = Some(Instant::now());
512 }
513 }
514}
515
516fn set_text_trace_target(
517 traces: &mut VecDeque<PendingTextTrace>,
518 seq: Option<u64>,
519 target: Option<NodeId>,
520) {
521 if let Some(seq) = seq {
522 if let Some(trace) = traces.iter_mut().rev().find(|trace| trace.seq == seq) {
523 trace.target = target;
524 }
525 }
526}
527
528fn cancel_text_trace(traces: &mut VecDeque<PendingTextTrace>, seq: Option<u64>) {
529 if let Some(seq) = seq {
530 traces.retain(|trace| trace.seq != seq);
531 }
532}
533
534fn flush_text_traces(
535 enabled: bool,
536 traces: &mut VecDeque<PendingTextTrace>,
537 presented_frames: u64,
538) {
539 if !enabled {
540 traces.clear();
541 return;
542 }
543
544 loop {
545 let should_flush = traces
546 .front()
547 .map(|trace| trace.present_after_frame <= presented_frames)
548 .unwrap_or(false);
549 if !should_flush {
550 break;
551 }
552
553 let Some(trace) = traces.pop_front() else {
554 break;
555 };
556 let now = Instant::now();
557 let handled_at = trace.handled_at.unwrap_or(now);
558 let effects_at = trace.effects_at.unwrap_or(handled_at);
559 let total_ms = now.duration_since(trace.started_at).as_secs_f64() * 1000.0;
560 let handle_ms = handled_at
561 .duration_since(trace.started_at)
562 .as_secs_f64()
563 * 1000.0;
564 let effects_ms = effects_at
565 .duration_since(handled_at)
566 .as_secs_f64()
567 * 1000.0;
568 let queue_ms = now.duration_since(effects_at).as_secs_f64() * 1000.0;
569
570 let target_u128 = trace.target.map(|id| id.as_u128());
571 let msg = format!(
572 "text_input_latency seq={} src={} handle_ms={:.2} effects_ms={:.2} queue_ms={:.2} total_ms={:.2} frame={}",
573 trace.seq, trace.source, handle_ms, effects_ms, queue_ms, total_ms, presented_frames
574 );
575 eprintln!("[text-trace] {}", msg);
576 diag::emit(
577 diag::DiagCategory::Input,
578 diag::DiagLevel::Info,
579 diag::DiagEventKind::InputEvent {
580 kind: msg,
581 target: target_u128,
582 position: None,
583 },
584 );
585 }
586}
587
588pub type KeyHandler<S> = Arc<dyn Fn(&mut S, &fission_core::KeyCode, u8) -> bool + Send + Sync>;
592
593pub type FrameHook<S> = Arc<dyn Fn(&mut S) -> bool + Send + Sync>;
597
598pub struct DesktopApp<S: AppState, W: Widget<S>> {
605 runtime: Runtime,
606 layout_engine: LayoutEngine,
607 root_widget: W,
608 env: Env,
609 pipeline: Pipeline,
610 measurer: Arc<VelloTextMeasurer>,
611 sync_env: Option<Arc<dyn Fn(&S, &mut Env) + Send + Sync>>,
612 key_handler: Option<KeyHandler<S>>,
613 frame_hook: Option<FrameHook<S>>,
614 title: String,
615 effect_result_tx: mpsc::Sender<EffectResult>,
617 effect_result_rx: mpsc::Receiver<EffectResult>,
618 app_effect_handler: Option<AppEffectHandler>,
620 _phantom: std::marker::PhantomData<S>,
621}
622
623impl<S: AppState + Default, W: Widget<S> + 'static> DesktopApp<S, W> {
624 pub fn new(root_widget: W) -> Self {
625 let mut runtime = Runtime::default();
626 runtime.add_app_state(Box::new(S::default())).unwrap();
627
628 const DEFAULT_FONT_FAMILY: &str = "Fission Default";
629 let font_cx = Arc::new(Mutex::new(build_font_context()));
630 {
631 let mut font_cx = font_cx.lock().unwrap();
632 let font_data = fonts::default_font_bytes().to_vec();
633 let info_override = FontInfoOverride {
634 family_name: Some(DEFAULT_FONT_FAMILY),
635 ..Default::default()
636 };
637 font_cx
638 .collection
639 .register_fonts(Blob::from(font_data), Some(info_override));
640 }
641 let measurer = Arc::new(VelloTextMeasurer::new_with_default_family(
642 font_cx.clone(),
643 DEFAULT_FONT_FAMILY,
644 ));
645 let env = Env::new(measurer.clone() as Arc<dyn fission_layout::TextMeasurer>);
646 let clipboard: Arc<dyn fission_core::env::Clipboard> = Arc::new(DesktopClipboard::new());
647
648 let layout_engine = LayoutEngine::new().with_measurer(measurer.clone());
649 let runtime = runtime
650 .with_measurer(measurer.clone())
651 .with_clipboard(clipboard);
652
653 let (effect_result_tx, effect_result_rx) = mpsc::channel();
654
655 Self {
656 runtime,
657 layout_engine,
658 root_widget,
659 env,
660 pipeline: Pipeline::new(),
661 measurer,
662 sync_env: None,
663 key_handler: None,
664 frame_hook: None,
665 title: "Fission".into(),
666 effect_result_tx,
667 effect_result_rx,
668 app_effect_handler: None,
669 _phantom: std::marker::PhantomData,
670 }
671 }
672
673 pub fn with_key_handler<F>(mut self, handler: F) -> Self
674 where
675 F: Fn(&mut S, &fission_core::KeyCode, u8) -> bool + Send + Sync + 'static,
676 {
677 self.key_handler = Some(Arc::new(handler));
678 self
679 }
680
681 pub fn with_title(mut self, title: impl Into<String>) -> Self {
682 self.title = title.into();
683 self
684 }
685
686 pub fn with_state_init<F>(mut self, init: F) -> Self
688 where
689 F: FnOnce(&mut S),
690 {
691 if let Some(state) = self.runtime.get_app_state_mut::<S>() {
692 init(state);
693 }
694 self
695 }
696
697 pub fn with_env(mut self, env: Env) -> Self {
698 self.env = env;
699 self
700 }
701
702 pub fn with_sync_env<F>(mut self, f: F) -> Self
703 where
704 F: Fn(&S, &mut Env) + Send + Sync + 'static,
705 {
706 self.sync_env = Some(Arc::new(f));
707 self
708 }
709
710 pub fn with_frame_hook<F>(mut self, f: F) -> Self
714 where
715 F: Fn(&mut S) -> bool + Send + Sync + 'static,
716 {
717 self.frame_hook = Some(Arc::new(f));
718 self
719 }
720
721 pub fn with_app_effect_handler<F>(mut self, handler: F) -> Self
727 where
728 F: Fn(Vec<u8>, u64, Option<ActionEnvelope>, Option<ActionEnvelope>, mpsc::Sender<EffectResult>)
729 + Send
730 + Sync
731 + 'static,
732 {
733 self.app_effect_handler = Some(Box::new(handler));
734 self
735 }
736
737 pub fn register_reducer(
738 &mut self,
739 action_id: ActionId,
740 reducer: fn(&mut S, &fission_core::ActionEnvelope, NodeId) -> Result<()>,
741 ) -> Result<()> {
742 self.runtime.register_reducer::<S>(action_id, reducer)
743 }
744
745 pub fn absorb_registry(&mut self, registry: fission_core::ActionRegistry<S>) {
746 self.runtime.absorb_persistent_registry(registry);
747 }
748
749 pub fn run(mut self) -> Result<()> {
750 diag::emit(
751 diag::DiagCategory::Frame,
752 diag::DiagLevel::Info,
753 diag::DiagEventKind::FrameStart { root: None },
754 );
755 diag::init_from_env();
756 let event_loop =
757 EventLoop::new().map_err(|e| anyhow::anyhow!("Event loop error: {}", e))?;
758 let window = Arc::new(
759 WindowBuilder::new()
760 .with_title(&self.title)
761 .build(&event_loop)
762 .map_err(|e| anyhow::anyhow!("Window build error: {}", e))?,
763 );
764
765 let ime_handler: Arc<dyn ImeHandler> = Arc::new(DesktopImeHandler::new(window.clone()));
766 self.runtime = self.runtime.with_ime_handler(ime_handler);
767
768 let mut render_cx = RenderContext::new();
770 let mut surface = block_on(render_cx.create_surface(
771 window.clone(),
772 window.inner_size().width,
773 window.inner_size().height,
774 wgpu::PresentMode::AutoVsync,
775 ))
776 .unwrap();
777
778 let device_handle = &render_cx.devices[surface.dev_id];
780 surface.config.alpha_mode = wgpu::CompositeAlphaMode::PostMultiplied;
781 surface
782 .surface
783 .configure(&device_handle.device, &surface.config);
784
785 recreate_target_texture(&mut surface, &render_cx);
787
788 let mut vello_renderer = VelloSceneRenderer::new(
789 &device_handle.device,
790 RendererOptions {
791 use_cpu: false,
792 antialiasing_support: AaSupport::all(),
793 num_init_threads: None,
794 pipeline_cache: None,
795 },
796 )
797 .unwrap();
798
799 let mut scene = Scene::new();
800
801 window.request_redraw();
802
803 let mut runtime = self.runtime;
804 let mut layout_engine = self.layout_engine;
805 let root_widget = self.root_widget;
806 let mut env = self.env;
807 let mut pipeline = self.pipeline;
808 let measurer = self.measurer;
809 let effect_result_tx = self.effect_result_tx;
810 let effect_result_rx = self.effect_result_rx;
811 let app_effect_handler = self.app_effect_handler;
812
813 #[cfg(target_os = "macos")]
814 let video_backend: Arc<dyn VideoBackend> = Arc::new(MacVideoBackend::new(&window));
815 #[cfg(not(target_os = "macos"))]
816 let video_backend: Arc<dyn VideoBackend> = Arc::new(MockVideoBackend::new());
817 let mut players: HashMap<WidgetNodeId, ActivePlayer> = HashMap::new();
818
819 let mut last_cursor_position: Option<PhysicalPosition<f64>> = None;
820 let max_fps = std::env::var("FISSION_MAX_FPS")
821 .ok()
822 .and_then(|v| v.parse::<u32>().ok())
823 .filter(|v| *v > 0)
824 .unwrap_or(60);
825 let min_frame = Duration::from_secs_f32(1.0 / max_fps as f32);
826 let mut last_redraw_at = Instant::now()
827 .checked_sub(min_frame)
828 .unwrap_or_else(Instant::now);
829 let mut redraw_pending = false;
830 let mut last_frame_time = Instant::now();
831 let blink_enabled = std::env::var("FISSION_TEXTINPUT_BLINK")
832 .map(|v| !matches!(v.to_ascii_lowercase().as_str(), "0" | "false" | "no"))
833 .unwrap_or(true);
834 let blink_period = Duration::from_millis(
835 std::env::var("FISSION_TEXTINPUT_BLINK_MS")
836 .ok()
837 .and_then(|v| v.parse::<u64>().ok())
838 .filter(|v| *v > 0)
839 .unwrap_or(530),
840 );
841 let mut last_blink_toggle = Instant::now();
842 let mut blink_focus_id: Option<NodeId> = None;
843 let text_trace_enabled = std::env::var("FISSION_TEXT_TRACE")
844 .map(|v| matches!(v.to_ascii_lowercase().as_str(), "1" | "true" | "yes"))
845 .unwrap_or(false);
846 let mut presented_frames: u64 = 0;
847 let mut next_text_trace_seq: u64 = 0;
848 let mut pending_text_traces: VecDeque<PendingTextTrace> = VecDeque::new();
849
850 let mut current_mods: u8 = 0;
851
852 let test_control_rx: Option<test_control::CommandReceiver> = std::env::var("FISSION_TEST_CONTROL_PORT")
854 .ok()
855 .and_then(|v| v.parse::<u16>().ok())
856 .map(|port| {
857 let (tx, rx) = test_control::create_channel();
858 test_control::spawn_server(port, tx);
859 rx
860 });
861 let mut pending_screenshot: Option<(String, test_control::ResponseSender)> = None;
862
863 event_loop
864 .run(move |event, elwt| {
865 elwt.set_control_flow(ControlFlow::Wait);
866
867 match event {
868 Event::AboutToWait => {
869 let now = Instant::now();
870 let dt = now.duration_since(last_frame_time);
871 last_frame_time = now;
872
873 let dt_ms = dt.as_millis() as u64;
875 if let Err(e) = runtime.tick(dt_ms) {
876 eprintln!("Runtime tick error: {:?}", e);
877 }
878
879 let surfaces = pipeline.take_video_surfaces();
881 let mut active_nodes = std::collections::HashSet::new();
882
883 for surface in &surfaces {
884 active_nodes.insert(surface.widget_id);
885
886 if !players.contains_key(&surface.widget_id) {
888 if let Some(state) = runtime.runtime_state.video.states.get(&surface.widget_id) {
889 let source = &state.asset_source;
890 if !source.is_empty() {
891 let player = video_backend.create_player(source);
892 if let Some(state) = runtime.runtime_state.video.states.get_mut(&surface.widget_id) {
893 state.surface_id = Some(player.surface_id());
894 }
895 players.insert(surface.widget_id, ActivePlayer {
896 player,
897 last_status: None,
898 last_rate: None,
899 last_volume: None,
900 last_muted: None,
901 });
902 }
903 }
904 }
905 }
906
907 players.retain(|id, _| active_nodes.contains(id));
909
910 video_backend.present_surfaces(&surfaces);
912
913 for (widget_id, active_player) in players.iter_mut() {
915 if let Some(video_state) = runtime.runtime_state.video.states.get_mut(widget_id) {
916 let player = &mut active_player.player;
917
918 if active_player.last_status != Some(video_state.status) {
920 match video_state.status {
921 VideoStatus::Playing => player.play(),
922 VideoStatus::Paused => player.pause(),
923 VideoStatus::Stopped => player.stop(),
924 _ => {}
925 }
926 active_player.last_status = Some(video_state.status);
927 }
928
929 for event in player.poll_events() {
931 match event {
932 VideoEvent::Ready { duration } => {
933 video_state.duration_ms = Some(duration);
934 if video_state.status == VideoStatus::Playing {
935 player.play();
936 }
937 },
938 VideoEvent::Ended => {
939 video_state.status = VideoStatus::Ended;
940 active_player.last_status = Some(VideoStatus::Ended);
941 request_redraw_throttled(&window, elwt, &mut last_redraw_at, min_frame, &mut redraw_pending);
942 },
943 VideoEvent::Error(e) => {
944 eprintln!("Video playback error for {:?}: {:?}", widget_id, e);
945 video_state.status = VideoStatus::Error;
946 active_player.last_status = Some(VideoStatus::Error);
947 request_redraw_throttled(&window, elwt, &mut last_redraw_at, min_frame, &mut redraw_pending);
948 },
949 }
950 }
951 video_state.position_ms = player.position();
953
954 if active_player.last_rate != Some(video_state.rate) {
955 player.set_rate(video_state.rate);
956 active_player.last_rate = Some(video_state.rate);
957 }
958 if active_player.last_volume != Some(video_state.volume) {
959 player.set_volume(video_state.volume);
960 active_player.last_volume = Some(video_state.volume);
961 }
962 if active_player.last_muted != Some(video_state.muted) {
963 player.set_muted(video_state.muted);
964 active_player.last_muted = Some(video_state.muted);
965 }
966
967 if let Some(seek_pos) = video_state.pending_seek.take() {
968 player.seek_to(seek_pos);
969 }
970 }
971 }
972
973 let has_finite_animation = runtime.runtime_state.animation.active.values()
978 .any(|a| !a.repeat);
979 let has_repeating_animation = runtime.runtime_state.animation.active.values()
980 .any(|a| a.repeat);
981 let needs_redraw = has_finite_animation || !players.is_empty();
982
983 let focused_text_input = focused_text_input_id(&runtime, pipeline.prev_ir.as_ref());
984 if focused_text_input != blink_focus_id {
985 if let Some(prev) = blink_focus_id {
986 runtime.runtime_state.caret_visible.remove(&prev);
987 }
988 blink_focus_id = focused_text_input;
989 if let Some(id) = blink_focus_id {
990 runtime.runtime_state.caret_visible.insert(id, true);
991 last_blink_toggle = now;
992 request_redraw_throttled(&window, elwt, &mut last_redraw_at, min_frame, &mut redraw_pending);
993 }
994 }
995
996 if blink_enabled {
1003 if let Some(id) = blink_focus_id {
1004 if now.duration_since(last_blink_toggle) >= blink_period {
1005 let visible = runtime.runtime_state.caret_visible.get(&id).copied().unwrap_or(true);
1006 runtime.runtime_state.caret_visible.insert(id, !visible);
1007 last_blink_toggle = now;
1008 }
1012 }
1013 }
1014
1015 let blink_wake_at = if blink_enabled && blink_focus_id.is_some() {
1016 Some(last_blink_toggle + blink_period)
1017 } else {
1018 None
1019 };
1020
1021 if let Some(ref rx) = test_control_rx {
1023 while let Ok((cmd, responder)) = rx.try_recv() {
1024 use fission_test_driver::{TestCommand, TestResponse, TextItem, SemanticNode};
1025 let resp = match cmd {
1026 TestCommand::Tap { x, y } => {
1027 let point = LayoutPoint::new(x, y);
1028 if let (Some(ir), Some(snap)) = (pipeline.prev_ir.as_ref(), pipeline.last_snapshot.as_ref()) {
1029 let _ = runtime.handle_input(InputEvent::Pointer(PointerEvent::Down { point, button: PointerButton::Primary }), ir, snap);
1030 let _ = runtime.handle_input(InputEvent::Pointer(PointerEvent::Up { point, button: PointerButton::Primary }), ir, snap);
1031 }
1032 TestResponse::Ok {}
1033 }
1034 TestCommand::TapText { text } => {
1035 if let (Some(ir), Some(snap)) = (pipeline.prev_ir.as_ref(), pipeline.last_snapshot.as_ref()) {
1036 let mut found = None;
1037 for (id, node) in &ir.nodes {
1038 let txt = match &node.op {
1039 fission_ir::Op::Paint(fission_ir::PaintOp::DrawText { text: t, .. }) => Some(t.as_str()),
1040 fission_ir::Op::Paint(fission_ir::PaintOp::DrawRichText { runs, .. }) => {
1041 let combined: String = runs.iter().map(|r| r.text.clone()).collect();
1043 if combined.contains(&text) { Some("") } else { None }
1044 }
1045 _ => None,
1046 };
1047 if let Some(t) = txt {
1048 if t.contains(&text) || t.is_empty() {
1049 let check_id = node.parent.unwrap_or(*id);
1051 if let Some(rect) = snap.get_node_rect(check_id).or_else(|| snap.get_node_rect(*id)) {
1052 found = Some((rect.x() + rect.width() / 2.0, rect.y() + rect.height() / 2.0));
1053 break;
1054 }
1055 }
1056 }
1057 }
1058 if let Some((cx, cy)) = found {
1059 let point = LayoutPoint::new(cx, cy);
1060 let _ = runtime.handle_input(InputEvent::Pointer(PointerEvent::Down { point, button: PointerButton::Primary }), ir, snap);
1061 let _ = runtime.handle_input(InputEvent::Pointer(PointerEvent::Up { point, button: PointerButton::Primary }), ir, snap);
1062 TestResponse::Ok {}
1063 } else {
1064 TestResponse::Error { message: format!("text '{}' not found", text) }
1065 }
1066 } else {
1067 TestResponse::Error { message: "no frame rendered yet".into() }
1068 }
1069 }
1070 TestCommand::Scroll { x, y, dx, dy } => {
1071 let point = LayoutPoint::new(x, y);
1072 let delta = LayoutPoint::new(dx, dy);
1073 if let (Some(ir), Some(snap)) = (pipeline.prev_ir.as_ref(), pipeline.last_snapshot.as_ref()) {
1074 let _ = runtime.handle_input(InputEvent::Pointer(PointerEvent::Scroll { point, delta }), ir, snap);
1075 }
1076 TestResponse::Ok {}
1077 }
1078 TestCommand::TypeText { text } => {
1079 if let (Some(ir), Some(snap)) = (pipeline.prev_ir.as_ref(), pipeline.last_snapshot.as_ref()) {
1080 for ch in text.chars() {
1081 let key = if ch == ' ' { KeyCode::Space } else if ch == '\n' { KeyCode::Enter } else { KeyCode::Char(ch) };
1082 let _ = runtime.handle_input(InputEvent::Keyboard(FissionKeyEvent::Down { key_code: key, modifiers: 0 }), ir, snap);
1083 }
1084 }
1085 TestResponse::Ok {}
1086 }
1087 TestCommand::PressKey { key, modifiers } => {
1088 let kc = match key.as_str() {
1089 "Enter" => KeyCode::Enter,
1090 "Escape" => KeyCode::Escape,
1091 "Tab" => KeyCode::Tab,
1092 "Backspace" => KeyCode::Backspace,
1093 "Left" => KeyCode::Left,
1094 "Right" => KeyCode::Right,
1095 "Up" => KeyCode::Up,
1096 "Down" => KeyCode::Down,
1097 "Home" => KeyCode::Home,
1098 "End" => KeyCode::End,
1099 "Space" => KeyCode::Space,
1100 s if s.len() == 1 => KeyCode::Char(s.chars().next().unwrap()),
1101 _ => KeyCode::Space,
1102 };
1103 let mut handled = false;
1105 if let Some(handler) = &self.key_handler {
1106 let handler = handler.clone();
1107 if let Some(state) = runtime.get_app_state_mut::<S>() {
1108 handled = handler(state, &kc, modifiers);
1109 }
1110 }
1111 if !handled {
1112 if let (Some(ir), Some(snap)) = (pipeline.prev_ir.as_ref(), pipeline.last_snapshot.as_ref()) {
1113 let _ = runtime.handle_input(InputEvent::Keyboard(FissionKeyEvent::Down { key_code: kc, modifiers }), ir, snap);
1114 }
1115 }
1116 TestResponse::Ok {}
1117 }
1118 TestCommand::Screenshot { path } => {
1119 pending_screenshot = Some((path, responder));
1120 window.request_redraw();
1121 continue; }
1123 TestCommand::GetText {} => {
1124 let mut items = Vec::new();
1125 if let (Some(ir), Some(snap)) = (pipeline.prev_ir.as_ref(), pipeline.last_snapshot.as_ref()) {
1126 for (id, node) in &ir.nodes {
1127 let text_content = match &node.op {
1128 fission_ir::Op::Paint(fission_ir::PaintOp::DrawText { text, .. }) => Some(text.clone()),
1129 fission_ir::Op::Paint(fission_ir::PaintOp::DrawRichText { runs, .. }) => {
1130 Some(runs.iter().map(|r| r.text.clone()).collect::<String>())
1131 }
1132 _ => None,
1133 };
1134 if let Some(text) = text_content {
1135 if text.is_empty() { continue; }
1136 let check_id = node.parent.unwrap_or(*id);
1137 let rect = snap.get_node_rect(check_id).or_else(|| snap.get_node_rect(*id));
1138 let (x, y, w, h) = rect.map(|r| (r.x(), r.y(), r.width(), r.height())).unwrap_or((0.0, 0.0, 0.0, 0.0));
1139 items.push(TextItem { text, x, y, width: w, height: h });
1140 }
1141 }
1142 }
1143 TestResponse::Text { items }
1144 }
1145 TestCommand::GetTree {} => {
1146 let mut nodes = Vec::new();
1147 if let Some(ir) = &pipeline.prev_ir {
1148 for (id, node) in &ir.nodes {
1149 if let fission_ir::Op::Semantics(sem) = &node.op {
1150 let rect = pipeline.last_snapshot.as_ref()
1151 .and_then(|s| s.get_node_rect(*id));
1152 let (x, y, w, h) = rect.map(|r| (r.x(), r.y(), r.width(), r.height())).unwrap_or((0.0, 0.0, 0.0, 0.0));
1153 nodes.push(SemanticNode {
1154 role: format!("{:?}", sem.role),
1155 label: sem.label.clone(),
1156 value: sem.value.clone(),
1157 focusable: sem.focusable,
1158 x, y, width: w, height: h,
1159 });
1160 }
1161 }
1162 }
1163 TestResponse::Tree { nodes }
1164 }
1165 TestCommand::Wait { ms } => {
1166 std::thread::sleep(std::time::Duration::from_millis(ms));
1167 TestResponse::Ok {}
1168 }
1169 TestCommand::Pump {} => {
1170 pending_screenshot = Some(("__pump__".into(), responder));
1172 window.request_redraw();
1173 continue; }
1175 TestCommand::Quit {} => {
1176 elwt.exit();
1177 TestResponse::Ok {}
1178 }
1179 };
1180 let _ = responder.send(resp);
1181 window.request_redraw();
1182 }
1183 }
1184
1185 let effect_results_dispatched = drain_effect_results(&mut runtime, &effect_result_rx);
1188 if effect_results_dispatched {
1189 if process_pending_effects(&mut runtime, &effect_result_tx, app_effect_handler.as_ref()) {
1192 request_redraw_throttled(&window, elwt, &mut last_redraw_at, min_frame, &mut redraw_pending);
1193 }
1194 window.request_redraw();
1195 }
1196
1197 let frame_hook_wants_redraw = if let Some(ref hook) = self.frame_hook {
1199 let hook = hook.clone();
1200 if let Some(state) = runtime.get_app_state_mut::<S>() {
1201 hook(state)
1202 } else {
1203 false
1204 }
1205 } else {
1206 false
1207 };
1208 if frame_hook_wants_redraw {
1209 request_redraw_throttled(&window, elwt, &mut last_redraw_at, min_frame, &mut redraw_pending);
1210 }
1211
1212 let frame_hook_wake_at = if self.frame_hook.is_some() {
1217 Some(now + Duration::from_secs(2))
1218 } else {
1219 None
1220 };
1221
1222 if needs_redraw || redraw_pending || effect_results_dispatched || frame_hook_wants_redraw {
1223 request_redraw_throttled(&window, elwt, &mut last_redraw_at, min_frame, &mut redraw_pending);
1224 let mut wake_at = last_redraw_at + min_frame;
1225 if let Some(blink_at) = blink_wake_at {
1226 if blink_at < wake_at {
1227 wake_at = blink_at;
1228 }
1229 }
1230 if let Some(hook_at) = frame_hook_wake_at {
1231 if hook_at < wake_at {
1232 wake_at = hook_at;
1233 }
1234 }
1235 elwt.set_control_flow(ControlFlow::WaitUntil(wake_at));
1236 } else if let Some(blink_at) = blink_wake_at {
1237 let mut wake_at = blink_at;
1238 if let Some(hook_at) = frame_hook_wake_at {
1239 if hook_at < wake_at {
1240 wake_at = hook_at;
1241 }
1242 }
1243 elwt.set_control_flow(ControlFlow::WaitUntil(wake_at));
1244 } else if let Some(hook_at) = frame_hook_wake_at {
1245 elwt.set_control_flow(ControlFlow::WaitUntil(hook_at));
1246 } else if has_repeating_animation {
1247 let anim_at = now + Duration::from_millis(200);
1249 request_redraw_throttled(&window, elwt, &mut last_redraw_at, min_frame, &mut redraw_pending);
1250 elwt.set_control_flow(ControlFlow::WaitUntil(anim_at));
1251 } else {
1252 elwt.set_control_flow(ControlFlow::Wait);
1253 }
1254 }
1255 Event::WindowEvent { window_id, event } if window_id == window.id() => {
1256 match event {
1257 WindowEvent::Resized(size) => {
1258 if size.width > 0 && size.height > 0 {
1259 pipeline.last_viewport = None;
1261 request_redraw_throttled(&window, elwt, &mut last_redraw_at, min_frame, &mut redraw_pending);
1262 }
1263 }
1264 WindowEvent::ScaleFactorChanged { .. } => {
1265 request_redraw_throttled(&window, elwt, &mut last_redraw_at, min_frame, &mut redraw_pending);
1266 }
1267 WindowEvent::RedrawRequested => {
1268 redraw_pending = false;
1269 diag::begin_frame(None);
1270 if process_pending_effects(&mut runtime, &effect_result_tx, app_effect_handler.as_ref()) {
1273 request_redraw_throttled(&window, elwt, &mut last_redraw_at, min_frame, &mut redraw_pending);
1274 }
1275 let size = window.inner_size();
1276 if size.width > 0 && size.height > 0 {
1277 if size.width != surface.config.width || size.height != surface.config.height {
1278 render_cx.resize_surface(&mut surface, size.width, size.height);
1279 let device_handle = &render_cx.devices[surface.dev_id];
1281 surface.config.alpha_mode = wgpu::CompositeAlphaMode::PostMultiplied;
1282 surface.surface.configure(&device_handle.device, &surface.config);
1283 recreate_target_texture(&mut surface, &render_cx);
1285 }
1286
1287 let scale_factor = window.scale_factor();
1288 let layout_width = (size.width as f64 / scale_factor) as f32;
1289 let layout_height = (size.height as f64 / scale_factor) as f32;
1290 env.viewport_size = LayoutSize {
1291 width: layout_width,
1292 height: layout_height,
1293 };
1294
1295 if let Some(sync) = &self.sync_env {
1296 let state = runtime.get_app_state::<S>().unwrap();
1297 sync(state, &mut env);
1298 }
1299
1300 let (node_tree, registry, anims, videos, web_views, portals) = {
1301 let state = runtime.get_app_state::<S>().unwrap();
1302 let view = View::new(state, &runtime.runtime_state, &env, pipeline.last_snapshot.as_ref());
1303 let mut ctx = BuildCtx::new();
1304 let node = root_widget.build(&mut ctx, &view);
1305 let anims = ctx.take_animation_requests();
1306 let videos = ctx.take_video_registrations();
1307 let web_views = ctx.take_web_registrations();
1308 let portals_with_ids = ctx.take_portals();
1309
1310 let portals = portals_with_ids.into_iter().map(|(id, node)| {
1311 if let Some(id) = id {
1312 let wrapper_id = fission_core::NodeId::derived(id.as_u128(), &[0x0000_F001]);
1314 fission_core::ui::Container::new(node)
1315 .id(wrapper_id)
1316 .width(env.viewport_size.width)
1317 .height(env.viewport_size.height)
1318 .into_node()
1319 } else {
1320 node
1321 }
1322 }).collect::<Vec<_>>();
1323
1324 {
1326 use fission_diagnostics::prelude as diag;
1327 diag::emit(
1328 diag::DiagCategory::Layout,
1329 diag::DiagLevel::Debug,
1330 diag::DiagEventKind::PortalsComposed { portal_count: portals.len() as u32 },
1331 );
1332 }
1333 (node, ctx.registry, anims, videos, web_views, portals)
1334 };
1335
1336 runtime.clear_reducers();
1337 runtime.absorb_registry(registry);
1338 for (target, req) in anims {
1339 runtime.enqueue_animation(target, req);
1340 }
1341 runtime.sync_video_nodes(&videos);
1342 runtime.sync_web_nodes(&web_views);
1343
1344 let final_root = fission_core::Node::Overlay(
1348 fission_core::ui::Overlay {
1349 id: None,
1350 content: Box::new(node_tree),
1351 overlay: Box::new(fission_core::Node::ZStack(
1352 fission_core::ui::ZStack {
1353 children: portals,
1354 ..Default::default()
1355 },
1356 )),
1357 }
1358 );
1359
1360 let mut lower_cx = LoweringContext::new(&env, &runtime.runtime_state, runtime.measurer.as_ref(), pipeline.last_snapshot.as_ref());
1361 let root_id = final_root.lower(&mut lower_cx);
1362 lower_cx.ir.root = Some(root_id);
1363 let cx_ir = lower_cx.ir;
1364
1365 let viewport = LayoutSize {
1366 width: layout_width,
1367 height: layout_height,
1368 };
1369
1370 scene.reset();
1372
1373 let mut renderer_wrapper = VelloRenderer::new(&mut scene, measurer.clone(), scale_factor);
1374
1375 match pipeline.render(
1376 cx_ir,
1377 viewport,
1378 &mut layout_engine,
1379 &runtime.runtime_state.scroll,
1380 &mut renderer_wrapper,
1381 &runtime.runtime_state.video,
1382 &runtime.runtime_state.web,
1383 &env,
1384 ) {
1385 Ok(_stats) => {
1386 let surface_texture = surface.surface.get_current_texture().expect("failed to get texture");
1387 let device_handle = &render_cx.devices[surface.dev_id];
1388
1389 let render_params = vello::RenderParams {
1390 base_color: vello::peniko::Color::from_rgb8(30, 30, 30),
1391 width: size.width,
1392 height: size.height,
1393 antialiasing_method: vello::AaConfig::Area,
1394 };
1395
1396 vello_renderer.render_to_texture(
1397 &device_handle.device,
1398 &device_handle.queue,
1399 &scene,
1400 &surface.target_view,
1401 &render_params,
1402 ).expect("failed to render");
1403
1404 let surface_view = surface_texture.texture.create_view(&wgpu::TextureViewDescriptor::default());
1405
1406 let mut encoder = device_handle.device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
1407 label: Some("Surface Blit"),
1408 });
1409
1410 surface.blitter.copy(
1411 &device_handle.device,
1412 &mut encoder,
1413 &surface.target_view,
1414 &surface_view,
1415 );
1416
1417 device_handle.queue.submit(Some(encoder.finish()));
1418
1419 if let Some((path, responder)) = pending_screenshot.take() {
1421 if path == "__pump__" {
1422 let _ = responder.send(fission_test_driver::TestResponse::Ok {});
1423 } else {
1424 let resp = gpu_screenshot(
1425 &device_handle.device,
1426 &device_handle.queue,
1427 &surface.target_texture,
1428 size.width,
1429 size.height,
1430 &path,
1431 );
1432 let _ = responder.send(resp);
1433 }
1434 }
1435
1436 surface_texture.present();
1437
1438 presented_frames = presented_frames.saturating_add(1);
1439 flush_text_traces(
1440 text_trace_enabled,
1441 &mut pending_text_traces,
1442 presented_frames,
1443 );
1444
1445 diag::end_frame(diag::FrameStats::default());
1446 }
1447 Err(e) => {
1448 eprintln!("Pipeline error: {:?}", e);
1449 }
1450 }
1451 }
1452 }
1453 WindowEvent::CloseRequested => {
1454 elwt.exit();
1455 }
1456 WindowEvent::CursorMoved { position, .. } => {
1458 if let (Some(ir), Some(layout)) = (&pipeline.prev_ir, &pipeline.last_snapshot) {
1459 last_cursor_position = Some(position);
1460 let scale_factor = window.scale_factor();
1461 let point = LayoutPoint {
1462 x: (position.x / scale_factor) as f32,
1463 y: (position.y / scale_factor) as f32,
1464 };
1465 let event = InputEvent::Pointer(PointerEvent::Move { point });
1466 if let Err(e) = runtime.handle_input(event, ir, layout) {
1467 eprintln!("Input handling error: {:?}", e);
1468 }
1469 if process_pending_effects(&mut runtime, &effect_result_tx, app_effect_handler.as_ref()) {
1470 request_redraw_throttled(&window, elwt, &mut last_redraw_at, min_frame, &mut redraw_pending);
1471 }
1472 request_redraw_throttled(&window, elwt, &mut last_redraw_at, min_frame, &mut redraw_pending);
1473 }
1474 }
1475 WindowEvent::MouseInput { state, button, .. } => {
1476 if let (Some(ir), Some(layout)) = (&pipeline.prev_ir, &pipeline.last_snapshot) {
1477 if let Some(position) = last_cursor_position {
1478 let scale_factor = window.scale_factor();
1479 let point = LayoutPoint {
1480 x: (position.x / scale_factor) as f32,
1481 y: (position.y / scale_factor) as f32,
1482 };
1483 if let Some(btn) = map_mouse_button(button) {
1484 if let Some(event) = build_pointer_event(state, btn, point) {
1485 let trace_seq = if text_trace_enabled && state.is_pressed() {
1486 start_text_trace(
1487 text_trace_enabled,
1488 &mut pending_text_traces,
1489 &mut next_text_trace_seq,
1490 "pointer_down".to_string(),
1491 None,
1492 presented_frames,
1493 )
1494 } else {
1495 None
1496 };
1497 if let Err(e) = runtime.handle_input(event, ir, layout) {
1499 eprintln!("Input handling error: {:?}", e);
1500 } else {
1501 }
1503 mark_text_trace_handled(&mut pending_text_traces, trace_seq);
1504 if process_pending_effects(&mut runtime, &effect_result_tx, app_effect_handler.as_ref()) {
1505 mark_text_trace_effects(&mut pending_text_traces, trace_seq);
1506 request_redraw_throttled(&window, elwt, &mut last_redraw_at, min_frame, &mut redraw_pending);
1507 }
1508 if state.is_pressed() {
1509 let target = focused_text_input_id(&runtime, pipeline.prev_ir.as_ref());
1510 if target.is_some() {
1511 set_text_trace_target(&mut pending_text_traces, trace_seq, target);
1512 } else {
1513 cancel_text_trace(&mut pending_text_traces, trace_seq);
1514 }
1515 reset_text_input_caret(&mut runtime, pipeline.prev_ir.as_ref(), &mut last_blink_toggle);
1516 }
1517 request_redraw_throttled(&window, elwt, &mut last_redraw_at, min_frame, &mut redraw_pending);
1518 }
1519 }
1520 }
1521 }
1522 }
1523 WindowEvent::MouseWheel { delta, .. } => {
1524 if let (Some(ir), Some(layout)) = (&pipeline.prev_ir, &pipeline.last_snapshot) {
1525 if let Some(position) = last_cursor_position {
1526 let scale_factor = window.scale_factor();
1527 let point = LayoutPoint {
1528 x: (position.x / scale_factor) as f32,
1529 y: (position.y / scale_factor) as f32,
1530 };
1531
1532 let scroll_delta = match delta {
1533 MouseScrollDelta::LineDelta(x, y) => LayoutPoint { x: -x * 50.0, y: -y * 50.0 },
1534 MouseScrollDelta::PixelDelta(p) => LayoutPoint {
1535 x: -(p.x / scale_factor) as f32,
1536 y: -(p.y / scale_factor) as f32,
1537 },
1538 };
1539
1540 let event = InputEvent::Pointer(PointerEvent::Scroll { point, delta: scroll_delta });
1541 if std::env::var("FISSION_SCROLL_TRACE").ok().as_deref() == Some("1") {
1542 eprintln!(
1543 "[scroll-trace] mousewheel raw={:?} point=({:.1},{:.1}) delta=({:.1},{:.1})",
1544 delta,
1545 point.x,
1546 point.y,
1547 scroll_delta.x,
1548 scroll_delta.y
1549 );
1550 }
1551 if let Err(e) = runtime.handle_input(event, ir, layout) {
1552 eprintln!("Scroll error: {:?}", e);
1553 }
1554 if process_pending_effects(&mut runtime, &effect_result_tx, app_effect_handler.as_ref()) {
1555 request_redraw_throttled(&window, elwt, &mut last_redraw_at, min_frame, &mut redraw_pending);
1556 }
1557 request_redraw_throttled(&window, elwt, &mut last_redraw_at, min_frame, &mut redraw_pending);
1558 }
1559 }
1560 }
1561 WindowEvent::ModifiersChanged(modifiers) => {
1562 current_mods = 0;
1563 if modifiers.state().shift_key() { current_mods |= 1; }
1564 if modifiers.state().alt_key() { current_mods |= 2; }
1565 if modifiers.state().control_key() { current_mods |= 4; }
1566 if modifiers.state().super_key() { current_mods |= 8; }
1567 }
1568 WindowEvent::KeyboardInput { event, .. } => {
1569 if event.state.is_pressed() {
1570 use winit::keyboard::{Key, NamedKey};
1571 let key_code = match event.logical_key {
1572 Key::Named(NamedKey::Space) => Some(KeyCode::Space),
1573 Key::Named(NamedKey::Enter) => Some(KeyCode::Enter),
1574 Key::Named(NamedKey::Escape) => Some(KeyCode::Escape),
1575 Key::Named(NamedKey::Backspace) => Some(KeyCode::Backspace),
1576 Key::Named(NamedKey::Tab) => Some(KeyCode::Tab),
1577 Key::Named(NamedKey::ArrowLeft) => Some(KeyCode::Left),
1578 Key::Named(NamedKey::ArrowRight) => Some(KeyCode::Right),
1579 Key::Named(NamedKey::ArrowUp) => Some(KeyCode::Up),
1580 Key::Named(NamedKey::ArrowDown) => Some(KeyCode::Down),
1581 Key::Named(NamedKey::Home) => Some(KeyCode::Home),
1582 Key::Named(NamedKey::End) => Some(KeyCode::End),
1583 _ => {
1584 if let Some(text) = &event.text {
1585 text.chars().next().map(KeyCode::Char)
1586 } else {
1587 None
1588 }
1589 }
1590 };
1591
1592 if let (Some(code), Some(ir), Some(layout)) = (key_code, &pipeline.prev_ir, &pipeline.last_snapshot) {
1593 let mut key_handled_by_app = false;
1595 if let Some(handler) = &self.key_handler {
1596 let handler = handler.clone();
1597 if let Some(state) = runtime.get_app_state_mut::<S>() {
1598 if handler(state, &code, current_mods) {
1599 if process_pending_effects(&mut runtime, &effect_result_tx, app_effect_handler.as_ref()) {
1600 request_redraw_throttled(&window, elwt, &mut last_redraw_at, min_frame, &mut redraw_pending);
1601 }
1602 request_redraw_throttled(&window, elwt, &mut last_redraw_at, min_frame, &mut redraw_pending);
1603 key_handled_by_app = true;
1604 }
1605 }
1606 }
1607
1608 if key_handled_by_app {
1609 } else {
1611
1612 let target = focused_text_input_id(&runtime, pipeline.prev_ir.as_ref());
1613 let trace_seq = start_text_trace(
1614 text_trace_enabled && target.is_some(),
1615 &mut pending_text_traces,
1616 &mut next_text_trace_seq,
1617 format!("keyboard:{:?}", code),
1618 target,
1619 presented_frames,
1620 );
1621 let input_event = InputEvent::Keyboard(FissionKeyEvent::Down {
1622 key_code: code,
1623 modifiers: current_mods,
1624 });
1625 if let Err(e) = runtime.handle_input(input_event, ir, layout) {
1626 eprintln!("Keyboard error: {:?}", e);
1627 }
1628 mark_text_trace_handled(&mut pending_text_traces, trace_seq);
1629 if process_pending_effects(&mut runtime, &effect_result_tx, app_effect_handler.as_ref()) {
1630 mark_text_trace_effects(&mut pending_text_traces, trace_seq);
1631 request_redraw_throttled(&window, elwt, &mut last_redraw_at, min_frame, &mut redraw_pending);
1632 }
1633 reset_text_input_caret(&mut runtime, pipeline.prev_ir.as_ref(), &mut last_blink_toggle);
1634 request_redraw_throttled(&window, elwt, &mut last_redraw_at, min_frame, &mut redraw_pending);
1635 } }
1637 }
1638 }
1639 WindowEvent::Ime(ime) => {
1640 if let (Some(ir), Some(layout)) = (&pipeline.prev_ir, &pipeline.last_snapshot) {
1641 let (input_event, source) = match ime {
1642 Ime::Commit(text) => (
1643 Some(InputEvent::Ime(fission_core::event::ImeEvent::Commit { text: text.clone() })),
1644 Some(format!("ime_commit:{}", text.chars().count())),
1645 ),
1646 Ime::Preedit(text, _) => (
1647 Some(InputEvent::Ime(fission_core::event::ImeEvent::Preedit { text: text.clone() })),
1648 Some(format!("ime_preedit:{}", text.chars().count())),
1649 ),
1650 _ => (None, None),
1651 };
1652
1653 if let Some(e) = input_event {
1654 let target = focused_text_input_id(&runtime, pipeline.prev_ir.as_ref());
1655 let trace_seq = start_text_trace(
1656 text_trace_enabled && target.is_some(),
1657 &mut pending_text_traces,
1658 &mut next_text_trace_seq,
1659 source.unwrap_or_else(|| "ime".to_string()),
1660 target,
1661 presented_frames,
1662 );
1663 runtime.handle_input(e, ir, layout).ok();
1664 mark_text_trace_handled(&mut pending_text_traces, trace_seq);
1665 if process_pending_effects(&mut runtime, &effect_result_tx, app_effect_handler.as_ref()) {
1666 mark_text_trace_effects(&mut pending_text_traces, trace_seq);
1667 request_redraw_throttled(&window, elwt, &mut last_redraw_at, min_frame, &mut redraw_pending);
1668 }
1669 reset_text_input_caret(&mut runtime, pipeline.prev_ir.as_ref(), &mut last_blink_toggle);
1670 request_redraw_throttled(&window, elwt, &mut last_redraw_at, min_frame, &mut redraw_pending);
1671 }
1672 }
1673 }
1674 _ => {}
1675 }
1676 }
1677 _ => {}
1678 }
1679 })
1680 .map_err(|e| anyhow::anyhow!("Event loop error: {}", e))
1681 }
1682}
1683
1684fn build_font_context() -> FontContext {
1685 let use_system_fonts = std::env::var("FISSION_USE_SYSTEM_FONTS")
1686 .map(|v| matches!(v.to_ascii_lowercase().as_str(), "1" | "true" | "yes"))
1687 .unwrap_or(false);
1688 let options = CollectionOptions {
1689 shared: false,
1690 system_fonts: use_system_fonts,
1691 };
1692 FontContext {
1693 collection: Collection::new(options),
1694 source_cache: SourceCache::default(),
1695 }
1696}
1697
1698fn map_mouse_button(button: MouseButton) -> Option<PointerButton> {
1700 match button {
1701 MouseButton::Left => Some(PointerButton::Primary),
1702 MouseButton::Right => Some(PointerButton::Secondary),
1703 MouseButton::Middle => Some(PointerButton::Middle),
1704 MouseButton::Other(id) => Some(PointerButton::Other(id as u8)),
1705 _ => None,
1706 }
1707}
1708
1709fn build_pointer_event(
1710 state: ElementState,
1711 button: PointerButton,
1712 point: LayoutPoint,
1713) -> Option<InputEvent> {
1714 let pointer_event = match state {
1715 ElementState::Pressed => PointerEvent::Down { point, button },
1716 ElementState::Released => PointerEvent::Up { point, button },
1717 };
1718
1719 Some(InputEvent::Pointer(pointer_event))
1720}
1721
1722fn gpu_screenshot(
1723 device: &wgpu::Device,
1724 queue: &wgpu::Queue,
1725 texture: &wgpu::Texture,
1726 width: u32,
1727 height: u32,
1728 path: &str,
1729) -> fission_test_driver::TestResponse {
1730 if width == 0 || height == 0 {
1731 return fission_test_driver::TestResponse::Error {
1732 message: "zero-size viewport".into(),
1733 };
1734 }
1735
1736 let bytes_per_pixel = 4u32;
1737 let unpadded_bytes_per_row = width * bytes_per_pixel;
1738 let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT;
1739 let padded_bytes_per_row = (unpadded_bytes_per_row + align - 1) / align * align;
1740 let buffer_size = (padded_bytes_per_row * height) as u64;
1741
1742 let staging = device.create_buffer(&wgpu::BufferDescriptor {
1743 label: Some("screenshot staging"),
1744 size: buffer_size,
1745 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
1746 mapped_at_creation: false,
1747 });
1748
1749 let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
1750 label: Some("screenshot copy"),
1751 });
1752
1753 encoder.copy_texture_to_buffer(
1754 wgpu::TexelCopyTextureInfo {
1755 texture,
1756 mip_level: 0,
1757 origin: wgpu::Origin3d::ZERO,
1758 aspect: wgpu::TextureAspect::All,
1759 },
1760 wgpu::TexelCopyBufferInfo {
1761 buffer: &staging,
1762 layout: wgpu::TexelCopyBufferLayout {
1763 offset: 0,
1764 bytes_per_row: Some(padded_bytes_per_row),
1765 rows_per_image: Some(height),
1766 },
1767 },
1768 wgpu::Extent3d {
1769 width,
1770 height,
1771 depth_or_array_layers: 1,
1772 },
1773 );
1774
1775 queue.submit(Some(encoder.finish()));
1776
1777 let (tx, rx) = std::sync::mpsc::channel();
1778 staging.slice(..).map_async(wgpu::MapMode::Read, move |result| {
1779 let _ = tx.send(result);
1780 });
1781 let _ = device.poll(wgpu::PollType::Wait);
1782
1783 match rx.recv() {
1784 Ok(Ok(())) => {}
1785 Ok(Err(e)) => {
1786 return fission_test_driver::TestResponse::Error {
1787 message: format!("buffer map failed: {:?}", e),
1788 };
1789 }
1790 Err(e) => {
1791 return fission_test_driver::TestResponse::Error {
1792 message: format!("buffer map channel error: {}", e),
1793 };
1794 }
1795 }
1796
1797 let data = staging.slice(..).get_mapped_range();
1798
1799 let mut rgba = Vec::with_capacity((width * height * 4) as usize);
1801 for row in 0..height {
1802 let start = (row * padded_bytes_per_row) as usize;
1803 let end = start + (width * bytes_per_pixel) as usize;
1804 rgba.extend_from_slice(&data[start..end]);
1805 }
1806
1807 drop(data);
1808 staging.unmap();
1809
1810 match image::save_buffer(path, &rgba, width, height, image::ColorType::Rgba8) {
1811 Ok(()) => fission_test_driver::TestResponse::Ok {},
1812 Err(e) => fission_test_driver::TestResponse::Error {
1813 message: format!("PNG save failed: {}", e),
1814 },
1815 }
1816}
1817
1818fn recreate_target_texture(
1819 surface: &mut RenderSurface,
1820 render_cx: &RenderContext,
1821) {
1822 let device = &render_cx.devices[surface.dev_id].device;
1823 let size = wgpu::Extent3d {
1824 width: surface.config.width.max(1),
1825 height: surface.config.height.max(1),
1826 depth_or_array_layers: 1,
1827 };
1828 let new_texture = device.create_texture(&wgpu::TextureDescriptor {
1829 label: Some("fission_target_with_copy"),
1830 size,
1831 mip_level_count: 1,
1832 sample_count: 1,
1833 dimension: wgpu::TextureDimension::D2,
1834 format: wgpu::TextureFormat::Rgba8Unorm, usage: wgpu::TextureUsages::STORAGE_BINDING
1836 | wgpu::TextureUsages::TEXTURE_BINDING
1837 | wgpu::TextureUsages::COPY_SRC,
1838 view_formats: &[],
1839 });
1840 let new_view = new_texture.create_view(&wgpu::TextureViewDescriptor::default());
1841 surface.target_texture = new_texture;
1842 surface.target_view = new_view;
1843}