1use crate::action::{ActionEnvelope, ActionId, GlobalState};
2use crate::async_runtime::ServiceStopPayload;
3use crate::effect::{
4 ActionInput, Effect, EffectEnvelope, RuntimeEffect, ScrollAlignment, ScrollAxis,
5 ScrollBehavior, ScrollIntoViewRequest,
6};
7use crate::env::{RuntimeState, VideoStatus};
8use crate::registry::{
9 ActionRegistry, ResourcePolicy, RuntimeResourceDeclaration, RuntimeResourceKind, TimerResource,
10 VideoRegistration,
11};
12use crate::ui::custom_render::downcast_render_object;
13use crate::{BoxedReducer, EffectCallbackRegistry};
14use crate::{
15 Clipboard, Clock, CurrentTime, ImeHandler, InputEvent, KeyCode, KeyEvent, PointerButton,
16 PointerEvent, ResourceExecutionContext,
17};
18use anyhow::{anyhow, Result};
19use fission_diagnostics::prelude as diag;
20use fission_ir::{CoreIR, FlexDirection, FocusPolicy, LayoutOp, Op, WidgetId};
21use fission_layout::{LayoutPoint, LayoutRect, LayoutSize, LayoutSnapshot, TextMeasurer};
22use glam::{Mat4, Vec4};
23use serde_json;
24use std::any::TypeId;
25use std::collections::{HashMap, HashSet};
26use std::sync::Arc;
27
28#[derive(Debug, Default, Clone)]
29pub struct TickResult {
30 pub changed_motions: Vec<(WidgetId, crate::MotionPropertyId)>,
31 pub resource_actions_dispatched: usize,
35}
36
37#[derive(Debug, Clone)]
38enum ActiveResourceKind {
39 Job,
40 Service {
41 service_name: String,
42 slot_key: String,
43 },
44 Timer {
45 interval_ms: u64,
46 payload: Vec<u8>,
47 on_tick: Option<ActionEnvelope>,
48 next_fire_at: CurrentTime,
49 },
50}
51
52#[derive(Debug, Clone)]
53struct ActiveResource {
54 generation: u64,
55 deps: Option<Vec<u8>>,
56 policy: ResourcePolicy,
57 kind: ActiveResourceKind,
58}
59
60#[derive(Debug, Clone)]
61struct PendingScrollIntoView {
62 request: ScrollIntoViewRequest,
63 retries_remaining: u8,
64}
65
66#[derive(Debug, Clone, Copy)]
67struct FocusBarrierFrame {
68 id: WidgetId,
69 restore_target: Option<WidgetId>,
70}
71
72#[derive(Debug, Clone, Copy, PartialEq, Eq)]
73enum ScrollIntoViewOutcome {
74 Applied { changed: bool },
75 Retry,
76 Ignored,
77}
78
79pub struct Runtime {
112 pub(crate) reducers: HashMap<ActionId, Vec<BoxedReducer>>,
115 pub(crate) persistent_reducers: HashMap<ActionId, Vec<BoxedReducer>>,
118 effect_callbacks: Arc<EffectCallbackRegistry>,
120 pub app_states: HashMap<TypeId, Box<dyn GlobalState>>,
122 pub runtime_state: RuntimeState,
124 pub measurer: Option<Arc<dyn TextMeasurer>>,
126 pub clipboard_backend: Option<Arc<dyn Clipboard>>,
128 pub ime_handler: Option<Arc<dyn ImeHandler>>,
130 pub pending_effects: Vec<EffectEnvelope>,
132 pending_scroll_into_view: Vec<PendingScrollIntoView>,
134 focus_barriers: Vec<FocusBarrierFrame>,
136 pub next_req_id: u64,
138 active_resources: HashMap<String, ActiveResource>,
140 next_resource_generation: u64,
142}
143
144impl Default for Runtime {
145 fn default() -> Self {
146 let mut runtime = Self {
147 reducers: HashMap::new(),
148 persistent_reducers: HashMap::new(),
149 effect_callbacks: Arc::new(EffectCallbackRegistry::new()),
150 app_states: HashMap::new(),
151 runtime_state: RuntimeState::default(),
152 measurer: None,
153 clipboard_backend: None,
154 ime_handler: None,
155 pending_effects: Vec::new(),
156 pending_scroll_into_view: Vec::new(),
157 focus_barriers: Vec::new(),
158 next_req_id: 0,
159 active_resources: HashMap::new(),
160 next_resource_generation: 1,
161 };
162
163 runtime
164 .add_global_state(Box::new(runtime.runtime_state.local_widget_state.clone()))
165 .expect("Failed to add local widget state store");
166
167 runtime
168 .add_global_state(Box::new(Clock::default()))
169 .expect("Failed to add Clock state");
170
171 runtime.register_base_reducers();
172
173 runtime
174 }
175}
176
177impl Runtime {
178 pub fn with_measurer(mut self, measurer: Arc<dyn TextMeasurer>) -> Self {
179 self.measurer = Some(measurer);
180 self
181 }
182
183 pub fn with_clipboard(mut self, backend: Arc<dyn Clipboard>) -> Self {
184 self.clipboard_backend = Some(backend);
185 self
186 }
187
188 pub fn with_ime_handler(mut self, handler: Arc<dyn ImeHandler>) -> Self {
189 self.ime_handler = Some(handler);
190 self
191 }
192
193 pub fn reconcile_focus(&mut self, ir: &CoreIR) -> Result<bool> {
198 use crate::hit_test::{
199 focus_barriers_in_tree_order, get_all_focusable_nodes, is_descendant_or_self,
200 is_enabled_focus_node, preferred_focus_node_in_scope,
201 };
202
203 let active_barriers = focus_barriers_in_tree_order(ir);
204 let common_prefix = self
205 .focus_barriers
206 .iter()
207 .map(|frame| frame.id)
208 .zip(active_barriers.iter().copied())
209 .take_while(|(tracked, active)| tracked == active)
210 .count();
211 let popped_any = self.focus_barriers.len() > common_prefix;
212 let mut restore_target = None;
213 while self.focus_barriers.len() > common_prefix {
214 restore_target = self
215 .focus_barriers
216 .pop()
217 .and_then(|frame| frame.restore_target);
218 }
219
220 for (index, barrier_id) in active_barriers
221 .iter()
222 .copied()
223 .enumerate()
224 .skip(common_prefix)
225 {
226 let restore = if index == 0 {
227 restore_target
228 .filter(|id| is_enabled_focus_node(ir, *id))
229 .or_else(|| {
230 self.runtime_state
231 .interaction
232 .focused
233 .filter(|id| is_enabled_focus_node(ir, *id))
234 })
235 } else {
236 let parent_barrier_id = active_barriers[index - 1];
237 self.runtime_state
238 .interaction
239 .focused
240 .filter(|id| {
241 is_enabled_focus_node(ir, *id)
242 && is_descendant_or_self(ir, *id, parent_barrier_id)
243 && !is_descendant_or_self(ir, *id, barrier_id)
244 })
245 .or_else(|| preferred_focus_node_in_scope(ir, parent_barrier_id))
246 };
247 self.focus_barriers.push(FocusBarrierFrame {
248 id: barrier_id,
249 restore_target: restore,
250 });
251 }
252
253 let current = self.runtime_state.interaction.focused;
254 let next = if let Some(barrier_id) = active_barriers.last().copied() {
255 current
256 .filter(|id| {
257 is_enabled_focus_node(ir, *id) && is_descendant_or_self(ir, *id, barrier_id)
258 })
259 .or_else(|| {
260 restore_target.filter(|id| {
261 is_enabled_focus_node(ir, *id) && is_descendant_or_self(ir, *id, barrier_id)
262 })
263 })
264 .or_else(|| preferred_focus_node_in_scope(ir, barrier_id))
265 } else if popped_any {
266 restore_target
267 .filter(|id| is_enabled_focus_node(ir, *id))
268 .or_else(|| {
269 let nodes = get_all_focusable_nodes(ir);
270 nodes
271 .iter()
272 .copied()
273 .find(|id| {
274 matches!(
275 ir.nodes.get(id).map(|node| &node.op),
276 Some(Op::Semantics(semantics)) if semantics.autofocus
277 )
278 })
279 .or_else(|| nodes.first().copied())
280 })
281 } else {
282 current.filter(|id| is_enabled_focus_node(ir, *id))
283 };
284
285 if current == next {
286 return Ok(false);
287 }
288
289 self.clear_text_pending_on_blur(current, next);
290 self.dispatch_custom_blur_actions(ir, current)?;
291 self.runtime_state.interaction.set_focused(next);
292 if let Some(ime_handler) = &self.ime_handler {
293 let accepts_text = next.is_some_and(|id| {
294 matches!(
295 ir.nodes.get(&id).map(|node| &node.op),
296 Some(Op::Semantics(semantics))
297 if semantics.role == fission_ir::semantics::Role::TextInput
298 ) || ir
299 .custom_render_objects
300 .get(&id)
301 .and_then(downcast_render_object)
302 .is_some_and(|render_object| render_object.accepts_text_input())
303 });
304 ime_handler.set_ime_allowed(accepts_text);
305 }
306 Ok(true)
307 }
308
309 pub fn caret_from_point_in_text(
310 &self,
311 value: &str,
312 font_size: f32,
313 viewport_x: f32,
314 viewport_w: f32,
315 content_w: f32,
316 scroll_offset: f32,
317 point_x: f32,
318 ) -> usize {
319 crate::input::text::caret_from_point_in_text(
320 self.measurer.as_ref(),
321 value,
322 font_size,
323 viewport_x,
324 viewport_w,
325 content_w,
326 scroll_offset,
327 point_x,
328 )
329 }
330
331 pub fn register_reducer<S: GlobalState + 'static>(
333 &mut self,
334 action_id: ActionId,
335 reducer_fn: crate::action::Reducer<S>,
336 ) -> Result<()> {
337 let state_type_id = TypeId::of::<S>();
338
339 let boxed_reducer: BoxedReducer = Box::new(
341 move |app_states: &mut HashMap<TypeId, Box<dyn GlobalState>>,
342 action: &ActionEnvelope,
343 target: WidgetId,
344 _effects: &mut Vec<EffectEnvelope>,
345 _input: &ActionInput,
346 _callback_registry|
347 -> Result<()> {
348 if let Some(state_box) = app_states.get_mut(&state_type_id) {
349 let concrete_state = state_box.downcast_mut::<S>().ok_or_else(|| {
350 anyhow!("Failed to downcast GlobalState to concrete type for reducer")
351 })?;
352 reducer_fn(concrete_state, action, target.into())
353 } else {
354 anyhow::bail!("Target GlobalState for reducer not found in runtime.");
355 }
356 },
357 );
358
359 self.reducers
360 .entry(action_id)
361 .or_default()
362 .push(boxed_reducer);
363 Ok(())
364 }
365
366 pub fn register_base_reducers(&mut self) {
367 use crate::{AdvanceTo, Tick, ADVANCE_TO_ACTION_ID, TICK_ACTION_ID};
368
369 self.register_reducer::<Clock>(
370 *TICK_ACTION_ID,
371 |state: &mut Clock, action: &ActionEnvelope, _target| {
372 let tick_action: Tick = serde_json::from_slice(&action.payload)
373 .map_err(|e| anyhow!("Failed to deserialize Tick: {}", e))?;
374 state.advance_by(tick_action.dt)
375 },
376 )
377 .expect("Failed to register Tick reducer");
378
379 self.register_reducer::<Clock>(
380 *ADVANCE_TO_ACTION_ID,
381 |state: &mut Clock, action: &ActionEnvelope, _target| {
382 let advance_action: AdvanceTo = serde_json::from_slice(&action.payload)
383 .map_err(|e| anyhow!("Failed to deserialize AdvanceTo: {}", e))?;
384 state.set_to(advance_action.time)
385 },
386 )
387 .expect("Failed to register AdvanceTo reducer");
388 }
389
390 pub fn clear_reducers(&mut self) {
391 self.reducers.clear();
392 self.register_base_reducers();
393 }
394
395 pub fn absorb_registry<S: GlobalState>(&mut self, registry: ActionRegistry<S>) {
396 let new_reducers = registry.into_runtime_reducers();
397 for (id, mut list) in new_reducers {
398 self.reducers.entry(id).or_default().append(&mut list);
399 }
400 }
401
402 pub fn absorb_persistent_registry<S: GlobalState>(&mut self, registry: ActionRegistry<S>) {
408 let new_reducers = registry.into_runtime_reducers();
409 for (id, mut list) in new_reducers {
410 self.persistent_reducers
411 .entry(id)
412 .or_default()
413 .append(&mut list);
414 }
415 }
416
417 pub fn clock(&self) -> &Clock {
418 self.get_global_state::<Clock>()
419 .expect("Clock state must always be present")
420 }
421
422 pub fn get_global_state<S: GlobalState + 'static>(&self) -> Option<&S> {
423 self.app_states
424 .get(&TypeId::of::<S>())
425 .and_then(|s_box| s_box.downcast_ref::<S>())
426 }
427
428 pub fn get_global_state_mut<S: GlobalState + 'static>(&mut self) -> Option<&mut S> {
429 self.app_states
430 .get_mut(&TypeId::of::<S>())
431 .and_then(|s_box| s_box.downcast_mut::<S>())
432 }
433
434 pub fn add_global_state<S: GlobalState + 'static>(&mut self, state: Box<S>) -> Result<()> {
435 let type_id = TypeId::of::<S>();
436 if self.app_states.insert(type_id, state).is_some() {
437 anyhow::bail!("Global state of this type already registered.");
438 }
439 Ok(())
440 }
441
442 pub fn with_global_state<S: GlobalState + 'static>(mut self, state: S) -> Self {
443 self.app_states.insert(TypeId::of::<S>(), Box::new(state));
444 self
445 }
446
447 #[doc(hidden)]
448 pub fn get_app_state<S: GlobalState + 'static>(&self) -> Option<&S> {
449 self.get_global_state::<S>()
450 }
451
452 #[doc(hidden)]
453 pub fn get_app_state_mut<S: GlobalState + 'static>(&mut self) -> Option<&mut S> {
454 self.get_global_state_mut::<S>()
455 }
456
457 #[doc(hidden)]
458 pub fn add_app_state<S: GlobalState + 'static>(&mut self, state: Box<S>) -> Result<()> {
459 self.add_global_state(state)
460 }
461
462 pub fn dispatch(&mut self, action: ActionEnvelope, target: WidgetId) -> Result<()> {
463 self.dispatch_with_input(action, target, &ActionInput::None)
464 }
465
466 fn enqueue_effect(&mut self, mut envelope: EffectEnvelope) {
467 envelope.req_id = self.next_req_id;
468 self.next_req_id += 1;
469 self.pending_effects.push(envelope);
470 }
471
472 pub fn dispatch_with_input(
473 &mut self,
474 action: ActionEnvelope,
475 target: WidgetId,
476 input: &ActionInput,
477 ) -> Result<()> {
478 self.dispatch_node_with_input(action, target.into(), input)
479 }
480
481 fn dispatch_node(&mut self, action: ActionEnvelope, target: WidgetId) -> Result<()> {
482 self.dispatch_node_with_input(action, target, &ActionInput::None)
483 }
484
485 fn dispatch_node_with_input(
486 &mut self,
487 action: ActionEnvelope,
488 target: WidgetId,
489 input: &ActionInput,
490 ) -> Result<()> {
491 diag::emit(
492 diag::DiagCategory::Input,
493 diag::DiagLevel::Debug,
494 diag::DiagEventKind::InputEvent {
495 kind: "dispatch_start".into(),
496 target: Some(target.as_u128()),
497 position: None,
498 },
499 );
500
501 if crate::media::handle_video_action(&mut self.runtime_state.video, &action)? {
503 return Ok(());
504 }
505
506 let action_id = action.id;
507
508 if crate::scoped_action_handlers::dispatch_scoped_action_handler(&action, target, input)? {
509 return Ok(());
510 }
511
512 let mut effects = Vec::new();
514 let callback_registry = self.effect_callbacks.clone();
515
516 let mut callback_reducers = callback_registry.take(action_id);
517 for reducer_wrapper in callback_reducers.iter_mut() {
518 reducer_wrapper(
519 &mut self.app_states,
520 &action,
521 target,
522 &mut effects,
523 input,
524 &callback_registry,
525 )?;
526 }
527
528 if let Some(reducers) = self.persistent_reducers.get_mut(&action_id) {
529 diag::emit(
530 diag::DiagCategory::Input,
531 diag::DiagLevel::Debug,
532 diag::DiagEventKind::InputEvent {
533 kind: format!("persistent_reducers:{}", reducers.len()),
534 target: Some(target.as_u128()),
535 position: None,
536 },
537 );
538
539 let mut temp_reducers: Vec<BoxedReducer> = reducers.drain(..).collect();
540 for reducer_wrapper in temp_reducers.iter_mut() {
541 reducer_wrapper(
542 &mut self.app_states,
543 &action,
544 target,
545 &mut effects,
546 input,
547 &callback_registry,
548 )?;
549 }
550 reducers.extend(temp_reducers);
551 }
552
553 if let Some(reducers) = self.reducers.get_mut(&action_id) {
554 diag::emit(
555 diag::DiagCategory::Input,
556 diag::DiagLevel::Debug,
557 diag::DiagEventKind::InputEvent {
558 kind: format!("reducers:{}", reducers.len()),
559 target: Some(target.as_u128()),
560 position: None,
561 },
562 );
563
564 let mut temp_reducers: Vec<BoxedReducer> = reducers.drain(..).collect();
565 for reducer_wrapper in temp_reducers.iter_mut() {
566 reducer_wrapper(
567 &mut self.app_states,
568 &action,
569 target,
570 &mut effects,
571 input,
572 &callback_registry,
573 )?;
574 }
575 reducers.extend(temp_reducers);
576 }
577
578 for envelope in effects {
579 self.enqueue_effect(envelope);
580 }
581
582 diag::emit(
583 diag::DiagCategory::Input,
584 diag::DiagLevel::Debug,
585 diag::DiagEventKind::InputEvent {
586 kind: "dispatch_end".into(),
587 target: Some(target.as_u128()),
588 position: None,
589 },
590 );
591 Ok(())
592 }
593
594 pub fn tick(&mut self, dt: CurrentTime) -> Result<TickResult> {
595 use crate::Tick;
596 let action = Tick { dt };
597 let envelope: ActionEnvelope = action.into();
598 self.dispatch_node(envelope, WidgetId::derived(0, &[0]))?;
599
600 let resource_actions_dispatched = self.tick_resource_timers()?;
601
602 let current_time = self.clock().current_time();
603 let changed_motions =
604 crate::motion::tick_motion(&mut self.runtime_state.motion, current_time);
605 Ok(TickResult {
606 changed_motions,
607 resource_actions_dispatched,
608 })
609 }
610
611 fn tick_resource_timers(&mut self) -> Result<usize> {
612 let now = self.clock().current_time();
613 let mut ticks = Vec::new();
614
615 for resource in self.active_resources.values_mut() {
616 if let ActiveResourceKind::Timer {
617 interval_ms,
618 payload,
619 on_tick,
620 next_fire_at,
621 } = &mut resource.kind
622 {
623 let Some(action) = on_tick.clone() else {
624 continue;
625 };
626
627 let interval_ms = (*interval_ms).max(1);
628 while now >= *next_fire_at {
629 ticks.push((action.clone(), payload.clone()));
630 *next_fire_at = next_fire_at.saturating_add(interval_ms);
631 }
632 }
633 }
634
635 let dispatched = ticks.len();
636 for (action, payload) in ticks {
637 self.dispatch_node_with_input(
638 action,
639 WidgetId::derived(0, &[0]),
640 &ActionInput::TimerTick { payload },
641 )?;
642 }
643
644 Ok(dispatched)
645 }
646
647 pub fn sync_motion_declarations(
648 &mut self,
649 declarations: &[crate::MotionDeclaration],
650 layout: Option<&LayoutSnapshot>,
651 ) -> Vec<(WidgetId, crate::MotionPropertyId)> {
652 let current_time = self.clock().current_time();
653 let snapshot = self.runtime_state.clone();
654 let result = crate::motion::sync_motion_declarations(
655 &mut self.runtime_state.motion,
656 declarations,
657 &snapshot,
658 layout,
659 current_time,
660 );
661 result.changed
662 }
663
664 pub fn sync_video_nodes(&mut self, registrations: &[VideoRegistration]) {
665 let mut seen: HashSet<WidgetId> = HashSet::new();
666
667 for reg in registrations {
668 seen.insert(reg.node_id);
669 let entry = self
670 .runtime_state
671 .video
672 .states
673 .entry(reg.node_id)
674 .or_insert_with(crate::env::VideoState::default);
675 entry.asset_source = reg.source.clone();
676 entry.looped = reg.loop_playback;
677 entry.audio = reg.audio.clone();
678 if reg.autoplay && entry.status == VideoStatus::Stopped {
679 entry.status = VideoStatus::Playing;
680 }
681 }
682
683 self.runtime_state
684 .video
685 .states
686 .retain(|node_id, _| seen.contains(node_id));
687 }
688
689 pub fn sync_web_nodes(&mut self, registrations: &[crate::registry::WebRegistration]) {
690 let mut seen: HashSet<WidgetId> = HashSet::new();
691
692 for reg in registrations {
693 seen.insert(reg.node_id);
694 let entry = self
695 .runtime_state
696 .web
697 .states
698 .entry(reg.node_id)
699 .or_insert_with(crate::env::WebState::default);
700
701 if entry.url != reg.url {
703 entry.url = reg.url.clone();
704 entry.loading = true; }
706 entry.user_agent = reg.user_agent.clone();
707 }
708
709 self.runtime_state
710 .web
711 .states
712 .retain(|node_id, _| seen.contains(node_id));
713 }
714
715 pub fn queue_runtime_effect(&mut self, effect: RuntimeEffect) -> bool {
720 match effect {
721 RuntimeEffect::ScrollIntoView(request) => {
722 self.queue_scroll_into_view(request);
723 true
724 }
725 RuntimeEffect::Cancel { .. } | RuntimeEffect::ReleaseResource { .. } => false,
726 }
727 }
728
729 pub fn queue_scroll_into_view(&mut self, request: ScrollIntoViewRequest) {
731 self.pending_scroll_into_view.push(PendingScrollIntoView {
732 request,
733 retries_remaining: 1,
734 });
735 }
736
737 fn drain_scroll_into_view_effects(&mut self) {
738 let pending = std::mem::take(&mut self.pending_effects);
739
740 for env in pending {
741 let EffectEnvelope {
742 req_id,
743 effect,
744 on_ok,
745 on_err,
746 service_bindings,
747 resource,
748 } = env;
749
750 match effect {
751 Effect::Runtime(RuntimeEffect::ScrollIntoView(request)) => {
752 self.queue_scroll_into_view(request);
753 }
754 retained => self.pending_effects.push(EffectEnvelope {
755 req_id,
756 effect: retained,
757 on_ok,
758 on_err,
759 service_bindings,
760 resource,
761 }),
762 }
763 }
764 }
765
766 fn apply_pending_scroll_into_view(&mut self, ir: &CoreIR, layout: &LayoutSnapshot) -> bool {
767 self.drain_scroll_into_view_effects();
768
769 let mut needs_follow_up_frame = false;
770 let pending = std::mem::take(&mut self.pending_scroll_into_view);
771
772 for mut pending_request in pending {
773 match self.apply_scroll_into_view(&pending_request.request, ir, layout) {
774 ScrollIntoViewOutcome::Applied { changed } => {
775 needs_follow_up_frame |= changed;
776 }
777 ScrollIntoViewOutcome::Retry if pending_request.retries_remaining > 0 => {
778 pending_request.retries_remaining -= 1;
779 self.pending_scroll_into_view.push(pending_request);
780 needs_follow_up_frame = true;
781 }
782 ScrollIntoViewOutcome::Retry | ScrollIntoViewOutcome::Ignored => {}
783 }
784 }
785
786 needs_follow_up_frame
787 }
788
789 fn apply_scroll_into_view(
790 &mut self,
791 request: &ScrollIntoViewRequest,
792 ir: &CoreIR,
793 layout: &LayoutSnapshot,
794 ) -> ScrollIntoViewOutcome {
795 let Some(target_geom) = layout.get_node_geometry(request.target) else {
796 Self::emit_scroll_into_view_diag("missing_target", request, None);
797 return ScrollIntoViewOutcome::Retry;
798 };
799
800 let Some(container_id) = self.resolve_scroll_container(request, ir, layout) else {
801 Self::emit_scroll_into_view_diag("missing_container", request, None);
802 return ScrollIntoViewOutcome::Retry;
803 };
804
805 if !Self::is_descendant_or_self(ir, request.target, container_id) {
806 Self::emit_scroll_into_view_diag("target_not_descendant", request, Some(container_id));
807 return ScrollIntoViewOutcome::Ignored;
808 }
809
810 let Some(container_geom) = layout.get_node_geometry(container_id) else {
811 Self::emit_scroll_into_view_diag(
812 "missing_container_layout",
813 request,
814 Some(container_id),
815 );
816 return ScrollIntoViewOutcome::Retry;
817 };
818
819 let Some(direction) = Self::scroll_direction(ir, container_id) else {
820 Self::emit_scroll_into_view_diag("not_scroll_container", request, Some(container_id));
821 return ScrollIntoViewOutcome::Ignored;
822 };
823
824 if !Self::axis_matches(request.axis, direction) {
825 Self::emit_scroll_into_view_diag("axis_mismatch", request, Some(container_id));
826 return ScrollIntoViewOutcome::Ignored;
827 }
828
829 if matches!(request.behavior, ScrollBehavior::Smooth) {
830 Self::emit_scroll_into_view_diag(
831 "smooth_resolved_as_instant",
832 request,
833 Some(container_id),
834 );
835 }
836
837 let current_offset = self.runtime_state.scroll.get_offset(container_id);
838 let new_offset = match direction {
839 FlexDirection::Column => Self::compute_scroll_offset(
840 current_offset,
841 target_geom.rect.y() - container_geom.rect.y(),
842 target_geom.rect.height(),
843 container_geom.rect.height(),
844 container_geom.content_size.height,
845 request.padding[2],
846 request.padding[3],
847 request.alignment,
848 request.if_needed,
849 ),
850 FlexDirection::Row => Self::compute_scroll_offset(
851 current_offset,
852 target_geom.rect.x() - container_geom.rect.x(),
853 target_geom.rect.width(),
854 container_geom.rect.width(),
855 container_geom.content_size.width,
856 request.padding[0],
857 request.padding[1],
858 request.alignment,
859 request.if_needed,
860 ),
861 };
862
863 if (new_offset - current_offset).abs() > f32::EPSILON {
864 self.runtime_state
865 .scroll
866 .set_offset(container_id, new_offset);
867 ScrollIntoViewOutcome::Applied { changed: true }
868 } else {
869 ScrollIntoViewOutcome::Applied { changed: false }
870 }
871 }
872
873 fn resolve_scroll_container(
874 &self,
875 request: &ScrollIntoViewRequest,
876 ir: &CoreIR,
877 layout: &LayoutSnapshot,
878 ) -> Option<WidgetId> {
879 if let Some(container) = request.container {
880 return ir
881 .nodes
882 .contains_key(&container)
883 .then_some(container)
884 .filter(|id| layout.get_node_geometry(*id).is_some());
885 }
886
887 let mut current = ir.nodes.get(&request.target)?.parent;
888 while let Some(node_id) = current {
889 if let Some(direction) = Self::scroll_direction(ir, node_id) {
890 if Self::axis_matches(request.axis, direction)
891 && layout.get_node_geometry(node_id).is_some()
892 {
893 return Some(node_id);
894 }
895 }
896 current = ir.nodes.get(&node_id).and_then(|node| node.parent);
897 }
898
899 None
900 }
901
902 fn scroll_direction(ir: &CoreIR, node_id: WidgetId) -> Option<FlexDirection> {
903 match ir.nodes.get(&node_id).map(|node| &node.op) {
904 Some(Op::Layout(LayoutOp::Scroll { direction, .. })) => Some(*direction),
905 _ => None,
906 }
907 }
908
909 fn axis_matches(axis: ScrollAxis, direction: FlexDirection) -> bool {
910 matches!(
911 (axis, direction),
912 (ScrollAxis::Both, _)
913 | (ScrollAxis::Vertical, FlexDirection::Column)
914 | (ScrollAxis::Horizontal, FlexDirection::Row)
915 )
916 }
917
918 fn is_descendant_or_self(ir: &CoreIR, target: WidgetId, ancestor: WidgetId) -> bool {
919 let mut current = Some(target);
920 while let Some(node_id) = current {
921 if node_id == ancestor {
922 return true;
923 }
924 current = ir.nodes.get(&node_id).and_then(|node| node.parent);
925 }
926 false
927 }
928
929 fn compute_scroll_offset(
930 current_offset: f32,
931 target_content_start: f32,
932 target_size: f32,
933 viewport_size: f32,
934 content_size: f32,
935 padding_start: f32,
936 padding_end: f32,
937 alignment: ScrollAlignment,
938 if_needed: bool,
939 ) -> f32 {
940 let current_offset = Self::finite_or_zero(current_offset).max(0.0);
941 let viewport_size = Self::finite_or_zero(viewport_size).max(0.0);
942 let content_size = Self::finite_or_zero(content_size).max(0.0);
943 let target_size = Self::finite_or_zero(target_size).max(0.0);
944 let padding_start = Self::finite_or_zero(padding_start).max(0.0);
945 let padding_end = Self::finite_or_zero(padding_end).max(0.0);
946 let max_offset = (content_size - viewport_size).max(0.0);
947
948 if viewport_size <= f32::EPSILON || max_offset <= f32::EPSILON {
949 return 0.0;
950 }
951
952 let target_start = Self::finite_or_zero(target_content_start);
953 let target_end = target_start + target_size;
954 let reveal_start = target_start - padding_start;
955 let reveal_end = target_end + padding_end;
956 let viewport_start = current_offset;
957 let viewport_end = current_offset + viewport_size;
958
959 if if_needed && reveal_start >= viewport_start && reveal_end <= viewport_end {
960 return current_offset.min(max_offset);
961 }
962
963 let desired = match alignment {
964 ScrollAlignment::Start => reveal_start,
965 ScrollAlignment::Center => {
966 let padded_viewport = (viewport_size - padding_start - padding_end).max(0.0);
967 target_start - padding_start - (padded_viewport - target_size) * 0.5
968 }
969 ScrollAlignment::End => reveal_end - viewport_size,
970 ScrollAlignment::Nearest => {
971 if reveal_end - reveal_start > viewport_size {
972 reveal_start
973 } else if reveal_start < viewport_start {
974 reveal_start
975 } else if reveal_end > viewport_end {
976 reveal_end - viewport_size
977 } else {
978 current_offset
979 }
980 }
981 ScrollAlignment::Fraction(fraction) => {
982 let fraction = Self::finite_or_zero(fraction).clamp(0.0, 1.0);
983 let padded_viewport = (viewport_size - padding_start - padding_end).max(0.0);
984 target_start - padding_start - (padded_viewport - target_size) * fraction
985 }
986 };
987
988 Self::finite_or_zero(desired).clamp(0.0, max_offset)
989 }
990
991 fn finite_or_zero(value: f32) -> f32 {
992 if value.is_finite() {
993 value
994 } else {
995 0.0
996 }
997 }
998
999 fn emit_scroll_into_view_diag(
1000 kind: &'static str,
1001 request: &ScrollIntoViewRequest,
1002 container: Option<WidgetId>,
1003 ) {
1004 diag::emit(
1005 diag::DiagCategory::Input,
1006 diag::DiagLevel::Debug,
1007 diag::DiagEventKind::InputEvent {
1008 kind: format!(
1009 "scroll_into_view:{kind}:target={:?}:container={:?}",
1010 request.target,
1011 container.or(request.container)
1012 ),
1013 target: Some(request.target.as_u128()),
1014 position: None,
1015 },
1016 );
1017 }
1018
1019 pub fn post_layout_hook(&mut self, ir: &CoreIR, layout: &LayoutSnapshot) -> bool {
1024 let needs_follow_up_frame = self.apply_pending_scroll_into_view(ir, layout);
1025 let active_scroll_nodes: HashSet<WidgetId> = ir
1026 .nodes
1027 .iter()
1028 .filter_map(|(id, node)| match node.op {
1029 Op::Layout(LayoutOp::Scroll { .. }) => Some(*id),
1030 _ => None,
1031 })
1032 .collect();
1033 self.runtime_state
1034 .scroll
1035 .retain_active(&active_scroll_nodes);
1036 if self
1037 .runtime_state
1038 .gesture
1039 .scrollbar_drag
1040 .is_some_and(|drag| !active_scroll_nodes.contains(&drag.node_id))
1041 {
1042 self.runtime_state.gesture.scrollbar_drag = None;
1043 }
1044
1045 let mut current_heroes = HashMap::new();
1046
1047 for (id, node) in &ir.nodes {
1048 if let Op::Semantics(s) = &node.op {
1049 if let Some(tag) = &s.hero_tag {
1050 if let Some(geom) = layout.get_node_geometry(*id) {
1051 current_heroes.insert(tag.clone(), (*id, geom.rect));
1052 }
1053 }
1054 }
1055 }
1056
1057 for (tag, (_new_id, new_rect)) in ¤t_heroes {
1059 if let Some((_old_id, old_rect)) = self.runtime_state.hero.positions.get(tag) {
1060 if *new_rect != *old_rect {
1061 diag::emit(
1063 diag::DiagCategory::Layout,
1064 diag::DiagLevel::Debug,
1065 diag::DiagEventKind::AnchorPlacement {
1066 widget: 0,
1067 node: 0,
1068 rect_x: old_rect.origin.x,
1069 rect_y: old_rect.origin.y,
1070 rect_w: old_rect.size.width,
1071 rect_h: old_rect.size.height,
1072 place_left: new_rect.origin.x,
1073 place_top: new_rect.origin.y,
1074 note: Some(format!("Hero flight: {}", tag)),
1075 },
1076 );
1077 }
1078 }
1079 }
1080
1081 self.runtime_state.hero.positions = current_heroes;
1082 needs_follow_up_frame
1083 }
1084
1085 pub fn handle_input(
1086 &mut self,
1087 event: InputEvent,
1088 ir: &CoreIR,
1089 layout: &LayoutSnapshot,
1090 ) -> Result<()> {
1091 use crate::hit_test::{
1092 find_neighbor_focus_node, find_next_focus_node, hit_test_with_scroll, FocusDirection,
1093 };
1094 use crate::input::gesture::GestureController;
1095 use crate::input::hover::HoverController;
1096 use crate::input::selectable_text::SelectableTextController;
1097 use crate::input::slider::SliderController;
1098 use crate::input::text::TextInputController;
1099 use crate::input::{ControllerContext, InputController};
1100 use crate::scrollbar::scrollbar_hit_test;
1101 use crate::ui::custom_render::downcast_render_object;
1102
1103 self.reconcile_focus(ir)?;
1104
1105 if self.runtime_state.interaction.focused.is_none() {
1106 if let Some(autofocus_id) = Self::find_autofocus_node(ir) {
1107 self.runtime_state
1108 .interaction
1109 .set_focused(Some(autofocus_id));
1110 if let Some(ime_handler) = &self.ime_handler {
1111 let accepts_text = ir
1112 .nodes
1113 .get(&autofocus_id)
1114 .and_then(|node| match &node.op {
1115 Op::Semantics(semantics) => {
1116 Some(semantics.role == fission_ir::semantics::Role::TextInput)
1117 }
1118 _ => None,
1119 })
1120 .unwrap_or(false);
1121 ime_handler.set_ime_allowed(accepts_text);
1122 }
1123 }
1124 }
1125
1126 if matches!(event, InputEvent::Pointer(_)) {
1127 let dispatched_actions = {
1128 let mut ctx = ControllerContext {
1129 ir,
1130 layout,
1131 text_edit: &mut self.runtime_state.text_edit,
1132 selectable_text: &mut self.runtime_state.selectable_text,
1133 context_menu: &mut self.runtime_state.context_menu,
1134 interaction: &mut self.runtime_state.interaction,
1135 scroll: &mut self.runtime_state.scroll,
1136 gesture: &mut self.runtime_state.gesture,
1137 clipboard: self.clipboard_backend.as_ref(),
1138 measurer: self.measurer.as_ref(),
1139 dispatched_actions: Vec::new(),
1140 };
1141 let mut hover_controller = HoverController;
1142 let _ = hover_controller.handle_event(&mut ctx, &event);
1143 ctx.dispatched_actions
1144 };
1145 self.dispatch_input_actions(dispatched_actions)?;
1146 }
1147
1148 let pointer_targets_scrollbar = match &event {
1154 InputEvent::Pointer(PointerEvent::Down { point, button, .. })
1155 if matches!(button, PointerButton::Primary) =>
1156 {
1157 scrollbar_hit_test(ir, layout, &self.runtime_state.scroll, *point).is_some()
1158 }
1159 InputEvent::Pointer(PointerEvent::Move { .. })
1160 | InputEvent::Pointer(PointerEvent::Up { .. }) => {
1161 self.runtime_state.gesture.scrollbar_drag.is_some()
1162 }
1163 InputEvent::Pointer(PointerEvent::Scroll { point, .. }) => {
1164 scrollbar_hit_test(ir, layout, &self.runtime_state.scroll, *point).is_some()
1165 }
1166 _ => false,
1167 };
1168
1169 if !pointer_targets_scrollbar {
1170 if let Some(point) = Self::event_point(&event) {
1171 if let Some(hit_node_id) =
1172 hit_test_with_scroll(ir, layout, &self.runtime_state.scroll, point)
1173 {
1174 let mut target_ro: Option<(WidgetId, &fission_ir::AnyRenderObject)> = None;
1179 {
1180 let mut walk = Some(hit_node_id);
1181 while let Some(nid) = walk {
1182 if let Some(ro) = ir.custom_render_objects.get(&nid) {
1183 target_ro = Some((nid, ro));
1184 break;
1185 }
1186 walk = ir.nodes.get(&nid).and_then(|n| n.parent);
1187 }
1188 }
1189 if target_ro.is_none() {
1190 for (ro_nid, ro) in &ir.custom_render_objects {
1191 if let Some(rect) = layout.get_node_rect(*ro_nid) {
1192 if rect.contains(point) {
1193 target_ro = Some((*ro_nid, ro));
1194 break;
1195 }
1196 }
1197 }
1198 }
1199
1200 if let Some((nid, any_ro)) = target_ro {
1201 if let Some(render_obj) = downcast_render_object(any_ro) {
1202 let mut node_rect = layout
1203 .get_node_rect(nid)
1204 .unwrap_or(LayoutRect::new(0.0, 0.0, 0.0, 0.0));
1205 {
1208 let mut walk = ir.nodes.get(&nid).and_then(|n| n.parent);
1209 while let Some(pid) = walk {
1210 if let Some(pnode) = ir.nodes.get(&pid) {
1211 if let fission_ir::Op::Layout(
1212 fission_ir::LayoutOp::Scroll { direction, .. },
1213 ) = &pnode.op
1214 {
1215 let off = self.runtime_state.scroll.get_offset(pid);
1216 match direction {
1217 fission_ir::FlexDirection::Row => {
1218 node_rect.origin.x -= off
1219 }
1220 fission_ir::FlexDirection::Column => {
1221 node_rect.origin.y -= off
1222 }
1223 }
1224 }
1225 walk = pnode.parent;
1226 } else {
1227 break;
1228 }
1229 }
1230 }
1231 let result = render_obj.handle_event(nid, &event, node_rect);
1232 if result.handled {
1233 if matches!(event, InputEvent::Pointer(PointerEvent::Down { .. })) {
1235 let old_focused_id = self.runtime_state.interaction.focused;
1236 if Some(nid) != old_focused_id {
1237 self.clear_text_pending_on_blur(old_focused_id, Some(nid));
1238 self.dispatch_custom_blur_actions(ir, old_focused_id)?;
1239 }
1240 self.runtime_state.interaction.set_focused(Some(nid));
1241 if let Some(ime_handler) = &self.ime_handler {
1242 let accepts_text = render_obj.accepts_text_input();
1243 ime_handler.set_ime_allowed(accepts_text);
1244 if accepts_text {
1245 if let Some(rect) =
1246 render_obj.ime_cursor_area(node_rect)
1247 {
1248 ime_handler.set_ime_cursor_area(rect);
1249 }
1250 }
1251 }
1252 }
1253 for (target, envelope) in result.actions {
1255 self.dispatch_node(envelope, target)?;
1256 }
1257 self.update_focused_ime_state(ir, layout);
1258 return Ok(());
1259 }
1260 }
1261 }
1262 }
1263 }
1264 }
1265
1266 if matches!(event, InputEvent::Keyboard(_) | InputEvent::Ime(_)) {
1272 if let Some(focused_id) = self.runtime_state.interaction.focused {
1273 let mut walk_id = Some(focused_id);
1274 while let Some(nid) = walk_id {
1275 if let Some(any_ro) = ir.custom_render_objects.get(&nid) {
1276 if let Some(render_obj) = downcast_render_object(any_ro) {
1277 let node_rect = layout
1278 .get_node_rect(nid)
1279 .unwrap_or(LayoutRect::new(0.0, 0.0, 0.0, 0.0));
1280 let result = render_obj.handle_event(nid, &event, node_rect);
1281 if result.handled {
1282 for (target, envelope) in result.actions {
1283 self.dispatch_node(envelope, target)?;
1284 }
1285 self.update_focused_ime_state(ir, layout);
1286 return Ok(());
1287 }
1288 }
1289 }
1290 walk_id = ir.nodes.get(&nid).and_then(|n| n.parent);
1291 }
1292 }
1293 }
1294
1295 let (handled, dispatched_actions) = {
1296 let mut ctx = ControllerContext {
1297 ir,
1298 layout,
1299 text_edit: &mut self.runtime_state.text_edit,
1300 selectable_text: &mut self.runtime_state.selectable_text,
1301 context_menu: &mut self.runtime_state.context_menu,
1302 interaction: &mut self.runtime_state.interaction,
1303 scroll: &mut self.runtime_state.scroll,
1304 gesture: &mut self.runtime_state.gesture,
1305 clipboard: self.clipboard_backend.as_ref(),
1306 measurer: self.measurer.as_ref(),
1307 dispatched_actions: Vec::new(),
1308 };
1309
1310 let mut hover_controller = HoverController;
1311 let _ = hover_controller.handle_event(&mut ctx, &event);
1312
1313 let mut selectable_text_controller = SelectableTextController;
1314 let handled = if selectable_text_controller.handle_event(&mut ctx, &event) {
1315 true
1316 } else {
1317 let mut gesture_controller = GestureController;
1318 if gesture_controller.handle_event(&mut ctx, &event) {
1319 true
1320 } else {
1321 let mut text_controller = TextInputController;
1322 if text_controller.handle_event(&mut ctx, &event) {
1323 true
1324 } else {
1325 let mut slider_controller = SliderController;
1326 slider_controller.handle_event(&mut ctx, &event)
1327 }
1328 }
1329 };
1330 (handled, ctx.dispatched_actions)
1331 };
1332
1333 self.dispatch_input_actions(dispatched_actions)?;
1334
1335 if handled {
1336 if matches!(event, InputEvent::Pointer(PointerEvent::Up { .. })) {
1337 self.runtime_state.interaction.pressed.clear();
1338 self.runtime_state.interaction.last_down_point = None;
1339 }
1340 self.update_focused_ime_state(ir, layout);
1341 return Ok(());
1342 }
1343
1344 match event {
1345 InputEvent::Pointer(PointerEvent::Scroll { point, delta, .. }) => {
1346 let trace_scroll =
1347 std::env::var("FISSION_SCROLL_TRACE").ok().as_deref() == Some("1");
1348 if trace_scroll {
1349 eprintln!(
1350 "[scroll-trace] event point=({:.1},{:.1}) delta=({:.1},{:.1})",
1351 point.x, point.y, delta.x, delta.y
1352 );
1353 }
1354 let hit_node_id = scrollbar_hit_test(ir, layout, &self.runtime_state.scroll, point)
1355 .map(|hit| hit.geometry.node_id)
1356 .or_else(|| {
1357 hit_test_with_scroll(ir, layout, &self.runtime_state.scroll, point)
1358 });
1359 if let Some(hit_node_id) = hit_node_id {
1360 if trace_scroll {
1361 eprintln!("[scroll-trace] hit_node={}", hit_node_id.as_u128());
1362 }
1363 let mut current_id = Some(hit_node_id);
1364 while let Some(node_id) = current_id {
1365 if let Some(node) = ir.nodes.get(&node_id) {
1366 if let Op::Layout(LayoutOp::Scroll { direction, .. }) = &node.op {
1367 let current_offset = self.runtime_state.scroll.get_offset(node_id);
1368 let delta_val = match direction {
1369 FlexDirection::Row => delta.x,
1370 FlexDirection::Column => delta.y,
1371 };
1372 let mut new_offset = current_offset + delta_val;
1373
1374 let mut max_offset = 0.0f32;
1375 let mut viewport_w = 0.0f32;
1376 let mut viewport_h = 0.0f32;
1377 let mut content_w = 0.0f32;
1378 let mut content_h = 0.0f32;
1379 if let Some(geom) = layout.get_node_geometry(node_id) {
1380 viewport_w = geom.rect.width();
1381 viewport_h = geom.rect.height();
1382 content_w = geom.content_size.width;
1383 content_h = geom.content_size.height;
1384 max_offset = if matches!(direction, FlexDirection::Row) {
1385 (geom.content_size.width - geom.rect.width()).max(0.0)
1386 } else {
1387 (geom.content_size.height - geom.rect.height()).max(0.0)
1388 };
1389 new_offset = new_offset.clamp(0.0, max_offset);
1390 }
1391
1392 if trace_scroll {
1393 eprintln!(
1394 "[scroll-trace] scroll_node={} axis={} offset={:.1}->{:.1} max={:.1} viewport=({:.1},{:.1}) content=({:.1},{:.1})",
1395 node_id.as_u128(),
1396 match direction { FlexDirection::Row => "x", FlexDirection::Column => "y" },
1397 current_offset,
1398 new_offset,
1399 max_offset,
1400 viewport_w,
1401 viewport_h,
1402 content_w,
1403 content_h
1404 );
1405 }
1406
1407 {
1408 use fission_diagnostics::prelude as diag;
1409 diag::emit(
1410 diag::DiagCategory::Input,
1411 diag::DiagLevel::Debug,
1412 diag::DiagEventKind::ScrollUpdate {
1413 node: node_id.as_u128(),
1414 axis: match direction {
1415 FlexDirection::Row => "x".into(),
1416 FlexDirection::Column => "y".into(),
1417 },
1418 point_x: point.x,
1419 point_y: point.y,
1420 delta: delta_val,
1421 old_offset: current_offset,
1422 new_offset,
1423 max_offset,
1424 viewport_w,
1425 viewport_h,
1426 content_w,
1427 content_h,
1428 },
1429 );
1430 }
1431
1432 self.runtime_state.scroll.set_offset(node_id, new_offset);
1433 if (new_offset - current_offset).abs() > 0.001 {
1437 break;
1438 }
1439 }
1441 current_id = node.parent;
1442 } else {
1443 break;
1444 }
1445 }
1446 } else if trace_scroll {
1447 eprintln!("[scroll-trace] hit_test: no node");
1448 }
1449 }
1450 InputEvent::Keyboard(KeyEvent::Down {
1451 key_code,
1452 modifiers,
1453 }) => match key_code {
1454 KeyCode::Tab => {
1455 let reverse = (modifiers & 1) != 0;
1456 let old_focus = self.runtime_state.interaction.focused;
1457 let next =
1458 find_next_focus_node(ir, self.runtime_state.interaction.focused, reverse);
1459 if next != old_focus {
1460 self.clear_text_pending_on_blur(old_focus, next);
1461 self.dispatch_custom_blur_actions(ir, old_focus)?;
1462 }
1463 self.runtime_state.interaction.set_focused(next);
1464 }
1465 KeyCode::Up | KeyCode::Down | KeyCode::Left | KeyCode::Right => {
1466 let reverse = matches!(key_code, KeyCode::Up | KeyCode::Left);
1467 let old_focus = self.runtime_state.interaction.focused;
1468 let next = if let Some(focused) = old_focus {
1469 let dir = match key_code {
1470 KeyCode::Up => FocusDirection::Up,
1471 KeyCode::Down => FocusDirection::Down,
1472 KeyCode::Left => FocusDirection::Left,
1473 KeyCode::Right => FocusDirection::Right,
1474 _ => unreachable!(),
1475 };
1476 find_neighbor_focus_node(ir, layout, focused, dir)
1477 .or_else(|| find_next_focus_node(ir, Some(focused), reverse))
1478 } else {
1479 find_next_focus_node(ir, None, reverse)
1480 };
1481 if next != old_focus {
1482 self.clear_text_pending_on_blur(old_focus, next);
1483 self.dispatch_custom_blur_actions(ir, old_focus)?;
1484 self.runtime_state.interaction.set_focused(next);
1485 }
1486 }
1487 KeyCode::Enter | KeyCode::Space => {
1488 if let Some(focused_id) = self.runtime_state.interaction.focused {
1489 let mut current_id = Some(focused_id);
1490 while let Some(node_id) = current_id {
1491 if let Some(node) = ir.nodes.get(&node_id) {
1492 if let Op::Semantics(semantics) = &node.op {
1493 if let Some(action_entry) =
1494 semantics.actions.entries.iter().find(|entry| {
1495 entry.trigger
1496 == fission_ir::semantics::ActionTrigger::Default
1497 })
1498 {
1499 if let Some(payload) = &action_entry.payload_data {
1500 let envelope = ActionEnvelope {
1501 id: ActionId::from_u128(action_entry.action_id),
1502 payload: payload.clone(),
1503 };
1504 let input = crate::input::scoped_action_input(
1505 ir,
1506 node_id,
1507 ActionInput::None,
1508 );
1509 return self.dispatch_node_with_input(
1510 envelope, node_id, &input,
1511 );
1512 }
1513 }
1514 }
1515 current_id = node.parent;
1516 } else {
1517 break;
1518 }
1519 }
1520 }
1521 }
1522 _ => {}
1523 },
1524 InputEvent::Pointer(PointerEvent::Down { point, .. }) => {
1525 if let Some(hit_node_id) =
1526 hit_test_with_scroll(ir, layout, &self.runtime_state.scroll, point)
1527 {
1528 diag::emit(
1529 diag::DiagCategory::Input,
1530 diag::DiagLevel::Debug,
1531 diag::DiagEventKind::InputEvent {
1532 kind: "pointer_down_hit".into(),
1533 target: Some(hit_node_id.as_u128()),
1534 position: Some((point.x, point.y)),
1535 },
1536 );
1537 let mut focus_candidate = Some(hit_node_id);
1538 while let Some(node_id) = focus_candidate {
1539 if let Some(node) = ir.nodes.get(&node_id) {
1540 if let Op::Semantics(s) = &node.op {
1541 if s.focusable {
1542 if s.focus_policy == FocusPolicy::PreserveCurrentOnPointer {
1543 break;
1544 }
1545 let old_focused_id = self.runtime_state.interaction.focused;
1546 if Some(node_id) != old_focused_id {
1547 self.clear_text_pending_on_blur(
1548 old_focused_id,
1549 Some(node_id),
1550 );
1551 self.dispatch_custom_blur_actions(ir, old_focused_id)?;
1552
1553 if s.role == fission_ir::semantics::Role::TextInput {
1554 if let Some(ime_handler) = &self.ime_handler {
1555 ime_handler.set_ime_allowed(true);
1556 }
1557 } else if let Some(ime_handler) = &self.ime_handler {
1558 ime_handler.set_ime_allowed(false);
1559 }
1560 }
1561 self.runtime_state.interaction.set_focused(Some(node_id));
1562 break;
1563 }
1564 }
1565 focus_candidate = node.parent;
1566 } else {
1567 break;
1568 }
1569 }
1570 if focus_candidate.is_none() {
1571 let old_focused_id = self.runtime_state.interaction.focused;
1572 if let Some(old_focused_id) = self.runtime_state.interaction.focused {
1573 if let Some(old_node) = ir.nodes.get(&old_focused_id) {
1574 if let Op::Semantics(s) = &old_node.op {
1575 if s.role == fission_ir::semantics::Role::TextInput {
1576 if let Some(ime_handler) = &self.ime_handler {
1577 ime_handler.set_ime_allowed(false);
1578 }
1579 }
1580 }
1581 }
1582 }
1583 self.clear_text_pending_on_blur(old_focused_id, None);
1584 self.dispatch_custom_blur_actions(ir, old_focused_id)?;
1585 self.runtime_state.interaction.set_focused(None);
1586 }
1587
1588 let mut current_pressed_id = Some(hit_node_id);
1589 while let Some(node_id) = current_pressed_id {
1590 self.runtime_state.interaction.set_pressed(node_id, true);
1591 if let Some(node) = ir.nodes.get(&node_id) {
1592 current_pressed_id = node.parent;
1593 } else {
1594 break;
1595 }
1596 }
1597 self.runtime_state.interaction.last_down_point = Some(point);
1598
1599 if let Some(focused_id) = self.runtime_state.interaction.focused {
1600 if let Some(node) = ir.nodes.get(&focused_id) {
1601 if let Op::Semantics(s) = &node.op {
1602 if s.role == fission_ir::semantics::Role::TextInput {
1603 if let Some(ime_handler) = &self.ime_handler {
1604 ime_handler.set_ime_cursor_area(LayoutRect::new(
1605 point.x, point.y, 2.0, 16.0,
1606 ));
1607 }
1608 }
1609 }
1610 }
1611 }
1612 } else {
1613 let old_focused_id = self.runtime_state.interaction.focused;
1614 if let Some(old_focused_id) = self.runtime_state.interaction.focused {
1615 if let Some(old_node) = ir.nodes.get(&old_focused_id) {
1616 if let Op::Semantics(s) = &old_node.op {
1617 if s.role == fission_ir::semantics::Role::TextInput {
1618 if let Some(ime_handler) = &self.ime_handler {
1619 ime_handler.set_ime_allowed(false);
1620 }
1621 }
1622 }
1623 }
1624 }
1625 self.clear_text_pending_on_blur(old_focused_id, None);
1626 self.dispatch_custom_blur_actions(ir, old_focused_id)?;
1627 self.runtime_state.interaction.set_focused(None);
1628 }
1629 }
1630 InputEvent::Pointer(PointerEvent::Up { point, .. }) => {
1631 self.runtime_state.interaction.pressed.clear();
1632 self.runtime_state.interaction.last_down_point = None;
1633 if let Some(hit_node_id) =
1634 hit_test_with_scroll(ir, layout, &self.runtime_state.scroll, point)
1635 {
1636 let mut current_id = Some(hit_node_id);
1637 while let Some(node_id) = current_id {
1638 if let Some(node) = ir.nodes.get(&node_id) {
1639 if let Op::Semantics(semantics) = &node.op {
1640 if semantics.role == fission_ir::semantics::Role::TextInput {
1641 } else if let Some(action_entry) =
1643 semantics.actions.entries.iter().find(|entry| {
1644 entry.trigger
1645 == fission_ir::semantics::ActionTrigger::Default
1646 })
1647 {
1648 if let Some(payload) = &action_entry.payload_data {
1649 let envelope = ActionEnvelope {
1650 id: ActionId::from_u128(action_entry.action_id),
1651 payload: payload.clone(),
1652 };
1653 diag::emit(
1654 diag::DiagCategory::Input,
1655 diag::DiagLevel::Debug,
1656 diag::DiagEventKind::InputEvent {
1657 kind: "pointer_up_dispatch".into(),
1658 target: Some(node_id.as_u128()),
1659 position: Some((point.x, point.y)),
1660 },
1661 );
1662 let input = crate::input::scoped_action_input(
1663 ir,
1664 node_id,
1665 ActionInput::None,
1666 );
1667 return self
1668 .dispatch_node_with_input(envelope, node_id, &input);
1669 }
1670 }
1671 }
1672 current_id = node.parent;
1673 } else {
1674 break;
1675 }
1676 }
1677 }
1678 }
1679 _ => {}
1680 }
1681 self.update_focused_ime_state(ir, layout);
1682 Ok(())
1683 }
1684
1685 pub fn clear_hover_state(&mut self, ir: &CoreIR, point: Option<LayoutPoint>) -> Result<bool> {
1686 use crate::input::hover::HoverController;
1687 use crate::input::ControllerContext;
1688
1689 let dispatched_actions = {
1690 let layout = &LayoutSnapshot::new(LayoutSize::ZERO);
1691 let mut ctx = ControllerContext {
1692 ir,
1693 layout,
1694 text_edit: &mut self.runtime_state.text_edit,
1695 selectable_text: &mut self.runtime_state.selectable_text,
1696 context_menu: &mut self.runtime_state.context_menu,
1697 interaction: &mut self.runtime_state.interaction,
1698 scroll: &mut self.runtime_state.scroll,
1699 gesture: &mut self.runtime_state.gesture,
1700 clipboard: self.clipboard_backend.as_ref(),
1701 measurer: self.measurer.as_ref(),
1702 dispatched_actions: Vec::new(),
1703 };
1704 let changed = HoverController::clear(&mut ctx, point);
1705 (changed, ctx.dispatched_actions)
1706 };
1707 self.dispatch_input_actions(dispatched_actions.1)?;
1708 Ok(dispatched_actions.0)
1709 }
1710
1711 fn dispatch_input_actions(
1712 &mut self,
1713 dispatched_actions: Vec<(WidgetId, ActionEnvelope, ActionInput)>,
1714 ) -> Result<()> {
1715 for (target, action, input) in dispatched_actions {
1716 self.dispatch_node_with_input(action, target, &input)?;
1717 }
1718 Ok(())
1719 }
1720
1721 fn update_focused_ime_state(&mut self, ir: &CoreIR, layout: &LayoutSnapshot) {
1722 let Some(ime_handler) = self.ime_handler.clone() else {
1723 return;
1724 };
1725 let Some(focused_id) = self.runtime_state.interaction.focused else {
1726 ime_handler.set_ime_allowed(false);
1727 return;
1728 };
1729
1730 let mut walk = Some(focused_id);
1731 while let Some(node_id) = walk {
1732 if let Some(any_ro) = ir.custom_render_objects.get(&node_id) {
1733 if let Some(render_obj) = crate::ui::custom_render::downcast_render_object(any_ro) {
1734 let accepts_text = render_obj.accepts_text_input();
1735 ime_handler.set_ime_allowed(accepts_text);
1736 if accepts_text {
1737 let rect =
1738 Self::visual_node_rect(ir, layout, &self.runtime_state.scroll, node_id)
1739 .unwrap_or(LayoutRect::new(0.0, 0.0, 0.0, 0.0));
1740 if let Some(cursor_area) = render_obj.ime_cursor_area(rect) {
1741 ime_handler.set_ime_cursor_area(cursor_area);
1742 }
1743 }
1744 return;
1745 }
1746 }
1747 walk = ir.nodes.get(&node_id).and_then(|node| node.parent);
1748 }
1749
1750 let accepts_text = ir
1751 .nodes
1752 .get(&focused_id)
1753 .and_then(|node| match &node.op {
1754 Op::Semantics(semantics) => {
1755 Some(semantics.role == fission_ir::semantics::Role::TextInput)
1756 }
1757 _ => None,
1758 })
1759 .unwrap_or(false);
1760 ime_handler.set_ime_allowed(accepts_text);
1761
1762 if accepts_text {
1763 let cursor_area = {
1764 let mut ctx = crate::input::ControllerContext {
1765 ir,
1766 layout,
1767 text_edit: &mut self.runtime_state.text_edit,
1768 selectable_text: &mut self.runtime_state.selectable_text,
1769 context_menu: &mut self.runtime_state.context_menu,
1770 interaction: &mut self.runtime_state.interaction,
1771 scroll: &mut self.runtime_state.scroll,
1772 gesture: &mut self.runtime_state.gesture,
1773 clipboard: self.clipboard_backend.as_ref(),
1774 measurer: self.measurer.as_ref(),
1775 dispatched_actions: Vec::new(),
1776 };
1777 crate::input::text::TextInputController::ime_cursor_area(&mut ctx, focused_id)
1778 };
1779 if let Some(cursor_area) = cursor_area {
1780 ime_handler.set_ime_cursor_area(cursor_area);
1781 }
1782 }
1783 }
1784
1785 fn visual_node_rect(
1786 ir: &CoreIR,
1787 layout: &LayoutSnapshot,
1788 scroll: &crate::env::ScrollStateMap,
1789 node_id: WidgetId,
1790 ) -> Option<LayoutRect> {
1791 let mut rect = layout.get_node_rect(node_id)?;
1792 let mut walk = ir.nodes.get(&node_id).and_then(|node| node.parent);
1793 while let Some(parent_id) = walk {
1794 let Some(parent) = ir.nodes.get(&parent_id) else {
1795 break;
1796 };
1797 if let Op::Layout(LayoutOp::Scroll { direction, .. }) = &parent.op {
1798 let offset = scroll.get_offset(parent_id);
1799 match direction {
1800 FlexDirection::Row => rect.origin.x -= offset,
1801 FlexDirection::Column => rect.origin.y -= offset,
1802 }
1803 }
1804 walk = parent.parent;
1805 }
1806 Some(rect)
1807 }
1808
1809 fn clear_text_pending_on_blur(
1810 &mut self,
1811 old_focus: Option<WidgetId>,
1812 new_focus: Option<WidgetId>,
1813 ) {
1814 if old_focus == new_focus {
1815 return;
1816 }
1817 if let Some(old_id) = old_focus {
1818 if let Some(st) = self.runtime_state.text_edit.states.get_mut(&old_id) {
1819 st.pending_model_sync = false;
1820 st.clear_preedit();
1821 }
1822 }
1823 }
1824
1825 fn dispatch_custom_blur_actions(
1826 &mut self,
1827 ir: &CoreIR,
1828 old_focus: Option<WidgetId>,
1829 ) -> Result<()> {
1830 if let Some(old_id) = old_focus {
1831 if let Some(any_ro) = ir.custom_render_objects.get(&old_id) {
1832 if let Some(render_obj) = crate::ui::custom_render::downcast_render_object(any_ro) {
1833 if render_obj.accepts_text_input() {
1834 if let Some(ime_handler) = &self.ime_handler {
1835 ime_handler.set_ime_allowed(false);
1836 }
1837 }
1838 for (target, envelope) in render_obj.blur_actions(old_id) {
1839 self.dispatch_node(envelope, target)?;
1840 }
1841 }
1842 }
1843 }
1844 Ok(())
1845 }
1846
1847 pub fn hit_test(
1848 &self,
1849 point: LayoutPoint,
1850 ir: &CoreIR,
1851 snapshot: &LayoutSnapshot,
1852 ) -> Option<WidgetId> {
1853 if let Some(root) = ir.root {
1854 return self.hit_test_recursive(root, point, ir, snapshot);
1855 }
1856 None
1857 }
1858
1859 fn hit_test_recursive(
1860 &self,
1861 node_id: WidgetId,
1862 point: LayoutPoint,
1863 ir: &CoreIR,
1864 snapshot: &LayoutSnapshot,
1865 ) -> Option<WidgetId> {
1866 if let Some(geom) = snapshot.nodes.get(&node_id) {
1867 if geom.rect.contains(point) {
1868 if let Some(node) = ir.nodes.get(&node_id) {
1869 for child in node.children.iter().rev() {
1870 let mut child_point = point;
1871
1872 if let Op::Layout(LayoutOp::Scroll { direction, .. }) = &node.op {
1873 if !geom.rect.contains(point) {
1874 continue;
1875 }
1876 let offset = self.runtime_state.scroll.get_offset(node_id);
1877 match direction {
1878 FlexDirection::Row => child_point.x += offset,
1879 FlexDirection::Column => child_point.y += offset,
1880 }
1881 }
1882
1883 if let Op::Layout(LayoutOp::Transform { transform }) = &node.op {
1884 let mat = Mat4::from_cols_array(transform);
1885 let local_x = point.x - geom.rect.origin.x;
1922 let local_y = point.y - geom.rect.origin.y;
1923
1924 let p = Vec4::new(local_x, local_y, 0.0, 1.0);
1925 let inv = mat.inverse();
1926 let transformed = inv * p;
1927
1928 child_point = LayoutPoint::new(
1929 transformed.x + geom.rect.origin.x,
1930 transformed.y + geom.rect.origin.y,
1931 );
1932 }
1933
1934 if let Some(hit) =
1935 self.hit_test_recursive(*child, child_point, ir, snapshot)
1936 {
1937 return Some(hit);
1938 }
1939 }
1940
1941 match &node.op {
1942 Op::Paint(_)
1943 | Op::Layout(LayoutOp::Scroll { .. })
1944 | Op::Layout(LayoutOp::Embed { .. }) => return Some(node_id),
1945 _ => return None,
1946 }
1947 }
1948 return None;
1949 }
1950 }
1951 None
1952 }
1953
1954 fn event_point(event: &InputEvent) -> Option<LayoutPoint> {
1960 match event {
1961 InputEvent::Pointer(PointerEvent::Down { point, .. })
1962 | InputEvent::Pointer(PointerEvent::Up { point, .. })
1963 | InputEvent::Pointer(PointerEvent::Move { point, .. })
1964 | InputEvent::Pointer(PointerEvent::Scroll { point, .. }) => Some(*point),
1965 _ => None,
1966 }
1967 }
1968
1969 fn find_autofocus_node(ir: &CoreIR) -> Option<WidgetId> {
1970 fn walk(ir: &CoreIR, node_id: WidgetId) -> Option<WidgetId> {
1971 let node = ir.nodes.get(&node_id)?;
1972 if let Op::Semantics(semantics) = &node.op {
1973 if semantics.autofocus && semantics.focusable && !semantics.disabled {
1974 return Some(node_id);
1975 }
1976 }
1977 for child_id in &node.children {
1978 if let Some(found) = walk(ir, *child_id) {
1979 return Some(found);
1980 }
1981 }
1982 None
1983 }
1984
1985 ir.root.and_then(|root| walk(ir, root))
1986 }
1987
1988 pub fn reconcile_resources(
1989 &mut self,
1990 declarations: Vec<RuntimeResourceDeclaration>,
1991 ) -> Result<()> {
1992 let now = self.clock().current_time();
1993 let mut existing = std::mem::take(&mut self.active_resources);
1994 let mut next = HashMap::new();
1995
1996 for declaration in declarations {
1997 let key = declaration.key.clone();
1998 match existing.remove(&key) {
1999 Some(current)
2000 if current.policy == declaration.policy
2001 && current.deps == declaration.deps
2002 && current.matches_kind(&declaration.kind) =>
2003 {
2004 next.insert(key, current);
2005 }
2006 Some(current) if declaration.policy == ResourcePolicy::PreserveOnChange => {
2007 next.insert(key, current);
2008 }
2009 Some(current) => {
2010 self.stop_resource(&key, ¤t);
2011 let replacement = self.start_resource(declaration, now);
2012 next.insert(key, replacement);
2013 }
2014 None => {
2015 let resource = self.start_resource(declaration, now);
2016 next.insert(key, resource);
2017 }
2018 }
2019 }
2020
2021 for (key, resource) in existing {
2022 self.stop_resource(&key, &resource);
2023 }
2024
2025 self.active_resources = next;
2026 Ok(())
2027 }
2028
2029 pub fn resource_generation(&self, key: &str) -> Option<u64> {
2030 self.active_resources
2031 .get(key)
2032 .map(|resource| resource.generation)
2033 }
2034
2035 pub fn is_resource_current(&self, resource: &ResourceExecutionContext) -> bool {
2036 self.resource_generation(&resource.key) == Some(resource.generation)
2037 }
2038
2039 fn start_resource(
2040 &mut self,
2041 declaration: RuntimeResourceDeclaration,
2042 now: CurrentTime,
2043 ) -> ActiveResource {
2044 let generation = self.next_resource_generation;
2045 self.next_resource_generation += 1;
2046
2047 let context = ResourceExecutionContext {
2048 key: declaration.key.clone(),
2049 generation,
2050 };
2051
2052 let kind = match declaration.kind {
2053 RuntimeResourceKind::Job(mut job) => {
2054 job.effect.resource = Some(context);
2055 self.enqueue_effect(job.effect);
2056 ActiveResourceKind::Job
2057 }
2058 RuntimeResourceKind::Service(mut service) => {
2059 service.effect.resource = Some(context);
2060 let (service_name, slot_key) = match &service.effect.effect {
2061 crate::Effect::StartService(payload) => {
2062 (payload.service_name.clone(), payload.slot_key.clone())
2063 }
2064 _ => unreachable!("service resource must lower to StartService"),
2065 };
2066 self.enqueue_effect(service.effect);
2067 ActiveResourceKind::Service {
2068 service_name,
2069 slot_key,
2070 }
2071 }
2072 RuntimeResourceKind::Timer(timer) => self.start_timer_resource(timer, now),
2073 };
2074
2075 ActiveResource {
2076 generation,
2077 deps: declaration.deps,
2078 policy: declaration.policy,
2079 kind,
2080 }
2081 }
2082
2083 fn start_timer_resource(&self, timer: TimerResource, now: CurrentTime) -> ActiveResourceKind {
2084 let interval_ms = timer.interval_ms.max(1);
2085 ActiveResourceKind::Timer {
2086 interval_ms,
2087 payload: timer.payload,
2088 on_tick: timer.on_tick,
2089 next_fire_at: if timer.immediate {
2090 now
2091 } else {
2092 now.saturating_add(interval_ms)
2093 },
2094 }
2095 }
2096
2097 fn stop_resource(&mut self, key: &str, resource: &ActiveResource) {
2098 if let ActiveResourceKind::Service {
2099 service_name,
2100 slot_key,
2101 } = &resource.kind
2102 {
2103 self.enqueue_effect(EffectEnvelope {
2104 req_id: 0,
2105 effect: crate::Effect::StopService(ServiceStopPayload {
2106 service_name: service_name.clone(),
2107 slot_key: slot_key.clone(),
2108 }),
2109 on_ok: None,
2110 on_err: None,
2111 service_bindings: None,
2112 resource: Some(ResourceExecutionContext {
2113 key: key.to_string(),
2114 generation: resource.generation,
2115 }),
2116 });
2117 }
2118 }
2119}
2120
2121impl ActiveResource {
2122 fn matches_kind(&self, kind: &RuntimeResourceKind) -> bool {
2123 matches!(
2124 (&self.kind, kind),
2125 (ActiveResourceKind::Job, RuntimeResourceKind::Job(_))
2126 | (
2127 ActiveResourceKind::Timer { .. },
2128 RuntimeResourceKind::Timer(_)
2129 )
2130 | (
2131 ActiveResourceKind::Service { .. },
2132 RuntimeResourceKind::Service(_)
2133 )
2134 )
2135 }
2136}