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 #[doc(hidden)]
480 pub fn discard_pending_effects(&mut self) -> usize {
481 let discarded = self.pending_effects.len() + self.effect_callbacks.clear();
482 self.pending_effects.clear();
483 discarded
484 }
485
486 pub fn dispatch_with_input(
487 &mut self,
488 action: ActionEnvelope,
489 target: WidgetId,
490 input: &ActionInput,
491 ) -> Result<()> {
492 self.dispatch_node_with_input(action, target.into(), input)
493 }
494
495 fn dispatch_node(&mut self, action: ActionEnvelope, target: WidgetId) -> Result<()> {
496 self.dispatch_node_with_input(action, target, &ActionInput::None)
497 }
498
499 fn dispatch_node_with_input(
500 &mut self,
501 action: ActionEnvelope,
502 target: WidgetId,
503 input: &ActionInput,
504 ) -> Result<()> {
505 let action_id = action.id;
506 let result = self.try_dispatch_node_with_input(action, target, input);
507 if let Err(error) = &result {
508 crate::registry::emit_action_dispatch_failure(action_id, target, error);
509 }
510 result
511 }
512
513 fn try_dispatch_node_with_input(
514 &mut self,
515 action: ActionEnvelope,
516 target: WidgetId,
517 input: &ActionInput,
518 ) -> Result<()> {
519 diag::emit(
520 diag::DiagCategory::Input,
521 diag::DiagLevel::Debug,
522 diag::DiagEventKind::InputEvent {
523 kind: "dispatch_start".into(),
524 target: Some(target.as_u128()),
525 position: None,
526 },
527 );
528
529 if crate::media::handle_video_action(&mut self.runtime_state.video, &action)? {
531 return Ok(());
532 }
533
534 let action_id = action.id;
535
536 if crate::scoped_action_handlers::dispatch_scoped_action_handler(&action, target, input)? {
537 return Ok(());
538 }
539
540 let mut effects = Vec::new();
542 let callback_registry = self.effect_callbacks.clone();
543
544 let mut callback_reducers = callback_registry.take(action_id);
545 for reducer_wrapper in callback_reducers.iter_mut() {
546 reducer_wrapper(
547 &mut self.app_states,
548 &action,
549 target,
550 &mut effects,
551 input,
552 &callback_registry,
553 )?;
554 }
555
556 if let Some(reducers) = self.persistent_reducers.get_mut(&action_id) {
557 diag::emit(
558 diag::DiagCategory::Input,
559 diag::DiagLevel::Debug,
560 diag::DiagEventKind::InputEvent {
561 kind: format!("persistent_reducers:{}", reducers.len()),
562 target: Some(target.as_u128()),
563 position: None,
564 },
565 );
566
567 let mut temp_reducers: Vec<BoxedReducer> = reducers.drain(..).collect();
568 let dispatch_result = temp_reducers.iter_mut().try_for_each(|reducer_wrapper| {
569 reducer_wrapper(
570 &mut self.app_states,
571 &action,
572 target,
573 &mut effects,
574 input,
575 &callback_registry,
576 )
577 });
578 reducers.extend(temp_reducers);
579 dispatch_result?;
580 }
581
582 if let Some(reducers) = self.reducers.get_mut(&action_id) {
583 diag::emit(
584 diag::DiagCategory::Input,
585 diag::DiagLevel::Debug,
586 diag::DiagEventKind::InputEvent {
587 kind: format!("reducers:{}", reducers.len()),
588 target: Some(target.as_u128()),
589 position: None,
590 },
591 );
592
593 let mut temp_reducers: Vec<BoxedReducer> = reducers.drain(..).collect();
594 let dispatch_result = temp_reducers.iter_mut().try_for_each(|reducer_wrapper| {
595 reducer_wrapper(
596 &mut self.app_states,
597 &action,
598 target,
599 &mut effects,
600 input,
601 &callback_registry,
602 )
603 });
604 reducers.extend(temp_reducers);
605 dispatch_result?;
606 }
607
608 for envelope in effects {
609 self.enqueue_effect(envelope);
610 }
611
612 diag::emit(
613 diag::DiagCategory::Input,
614 diag::DiagLevel::Debug,
615 diag::DiagEventKind::InputEvent {
616 kind: "dispatch_end".into(),
617 target: Some(target.as_u128()),
618 position: None,
619 },
620 );
621 Ok(())
622 }
623
624 pub fn tick(&mut self, dt: CurrentTime) -> Result<TickResult> {
625 use crate::Tick;
626 let action = Tick { dt };
627 let envelope: ActionEnvelope = action.into();
628 self.dispatch_node(envelope, WidgetId::derived(0, &[0]))?;
629
630 let resource_actions_dispatched = self.tick_resource_timers()?;
631
632 let current_time = self.clock().current_time();
633 let changed_motions =
634 crate::motion::tick_motion(&mut self.runtime_state.motion, current_time);
635 Ok(TickResult {
636 changed_motions,
637 resource_actions_dispatched,
638 })
639 }
640
641 fn tick_resource_timers(&mut self) -> Result<usize> {
642 let now = self.clock().current_time();
643 let mut ticks = Vec::new();
644
645 for resource in self.active_resources.values_mut() {
646 if let ActiveResourceKind::Timer {
647 interval_ms,
648 payload,
649 on_tick,
650 next_fire_at,
651 } = &mut resource.kind
652 {
653 let Some(action) = on_tick.clone() else {
654 continue;
655 };
656
657 let interval_ms = (*interval_ms).max(1);
658 while now >= *next_fire_at {
659 ticks.push((action.clone(), payload.clone()));
660 *next_fire_at = next_fire_at.saturating_add(interval_ms);
661 }
662 }
663 }
664
665 let dispatched = ticks.len();
666 for (action, payload) in ticks {
667 self.dispatch_node_with_input(
668 action,
669 WidgetId::derived(0, &[0]),
670 &ActionInput::TimerTick { payload },
671 )?;
672 }
673
674 Ok(dispatched)
675 }
676
677 pub fn sync_motion_declarations(
678 &mut self,
679 declarations: &[crate::MotionDeclaration],
680 layout: Option<&LayoutSnapshot>,
681 ) -> Vec<(WidgetId, crate::MotionPropertyId)> {
682 let current_time = self.clock().current_time();
683 let snapshot = self.runtime_state.clone();
684 let result = crate::motion::sync_motion_declarations(
685 &mut self.runtime_state.motion,
686 declarations,
687 &snapshot,
688 layout,
689 current_time,
690 );
691 result.changed
692 }
693
694 pub fn sync_video_nodes(&mut self, registrations: &[VideoRegistration]) {
695 let mut seen: HashSet<WidgetId> = HashSet::new();
696
697 for reg in registrations {
698 seen.insert(reg.node_id);
699 let entry = self
700 .runtime_state
701 .video
702 .states
703 .entry(reg.node_id)
704 .or_insert_with(crate::env::VideoState::default);
705 entry.asset_source = reg.source.clone();
706 entry.looped = reg.loop_playback;
707 entry.audio = reg.audio.clone();
708 if reg.autoplay && entry.status == VideoStatus::Stopped {
709 entry.status = VideoStatus::Playing;
710 }
711 }
712
713 self.runtime_state
714 .video
715 .states
716 .retain(|node_id, _| seen.contains(node_id));
717 }
718
719 pub fn sync_web_nodes(&mut self, registrations: &[crate::registry::WebRegistration]) {
720 let mut seen: HashSet<WidgetId> = HashSet::new();
721
722 for reg in registrations {
723 seen.insert(reg.node_id);
724 let entry = self
725 .runtime_state
726 .web
727 .states
728 .entry(reg.node_id)
729 .or_insert_with(crate::env::WebState::default);
730
731 if entry.url != reg.url {
733 entry.url = reg.url.clone();
734 entry.loading = true; }
736 entry.user_agent = reg.user_agent.clone();
737 }
738
739 self.runtime_state
740 .web
741 .states
742 .retain(|node_id, _| seen.contains(node_id));
743 }
744
745 pub fn queue_runtime_effect(&mut self, effect: RuntimeEffect) -> bool {
750 match effect {
751 RuntimeEffect::ScrollIntoView(request) => {
752 self.queue_scroll_into_view(request);
753 true
754 }
755 RuntimeEffect::Cancel { .. } | RuntimeEffect::ReleaseResource { .. } => false,
756 }
757 }
758
759 pub fn queue_scroll_into_view(&mut self, request: ScrollIntoViewRequest) {
761 self.pending_scroll_into_view.push(PendingScrollIntoView {
762 request,
763 retries_remaining: 1,
764 });
765 }
766
767 fn drain_scroll_into_view_effects(&mut self) {
768 let pending = std::mem::take(&mut self.pending_effects);
769
770 for env in pending {
771 let EffectEnvelope {
772 req_id,
773 effect,
774 on_ok,
775 on_err,
776 service_bindings,
777 resource,
778 } = env;
779
780 match effect {
781 Effect::Runtime(RuntimeEffect::ScrollIntoView(request)) => {
782 self.queue_scroll_into_view(request);
783 }
784 retained => self.pending_effects.push(EffectEnvelope {
785 req_id,
786 effect: retained,
787 on_ok,
788 on_err,
789 service_bindings,
790 resource,
791 }),
792 }
793 }
794 }
795
796 fn apply_pending_scroll_into_view(&mut self, ir: &CoreIR, layout: &LayoutSnapshot) -> bool {
797 self.drain_scroll_into_view_effects();
798
799 let mut needs_follow_up_frame = false;
800 let pending = std::mem::take(&mut self.pending_scroll_into_view);
801
802 for mut pending_request in pending {
803 match self.apply_scroll_into_view(&pending_request.request, ir, layout) {
804 ScrollIntoViewOutcome::Applied { changed } => {
805 needs_follow_up_frame |= changed;
806 }
807 ScrollIntoViewOutcome::Retry if pending_request.retries_remaining > 0 => {
808 pending_request.retries_remaining -= 1;
809 self.pending_scroll_into_view.push(pending_request);
810 needs_follow_up_frame = true;
811 }
812 ScrollIntoViewOutcome::Retry | ScrollIntoViewOutcome::Ignored => {}
813 }
814 }
815
816 needs_follow_up_frame
817 }
818
819 fn apply_scroll_into_view(
820 &mut self,
821 request: &ScrollIntoViewRequest,
822 ir: &CoreIR,
823 layout: &LayoutSnapshot,
824 ) -> ScrollIntoViewOutcome {
825 let Some(target_geom) = layout.get_node_geometry(request.target) else {
826 Self::emit_scroll_into_view_diag("missing_target", request, None);
827 return ScrollIntoViewOutcome::Retry;
828 };
829
830 let Some(container_id) = self.resolve_scroll_container(request, ir, layout) else {
831 Self::emit_scroll_into_view_diag("missing_container", request, None);
832 return ScrollIntoViewOutcome::Retry;
833 };
834
835 if !Self::is_descendant_or_self(ir, request.target, container_id) {
836 Self::emit_scroll_into_view_diag("target_not_descendant", request, Some(container_id));
837 return ScrollIntoViewOutcome::Ignored;
838 }
839
840 let Some(container_geom) = layout.get_node_geometry(container_id) else {
841 Self::emit_scroll_into_view_diag(
842 "missing_container_layout",
843 request,
844 Some(container_id),
845 );
846 return ScrollIntoViewOutcome::Retry;
847 };
848
849 let Some(direction) = Self::scroll_direction(ir, container_id) else {
850 Self::emit_scroll_into_view_diag("not_scroll_container", request, Some(container_id));
851 return ScrollIntoViewOutcome::Ignored;
852 };
853
854 if !Self::axis_matches(request.axis, direction) {
855 Self::emit_scroll_into_view_diag("axis_mismatch", request, Some(container_id));
856 return ScrollIntoViewOutcome::Ignored;
857 }
858
859 if matches!(request.behavior, ScrollBehavior::Smooth) {
860 Self::emit_scroll_into_view_diag(
861 "smooth_resolved_as_instant",
862 request,
863 Some(container_id),
864 );
865 }
866
867 let current_offset = self.runtime_state.scroll.get_offset(container_id);
868 let new_offset = match direction {
869 FlexDirection::Column => Self::compute_scroll_offset(
870 current_offset,
871 target_geom.rect.y() - container_geom.rect.y(),
872 target_geom.rect.height(),
873 container_geom.rect.height(),
874 container_geom.content_size.height,
875 request.padding[2],
876 request.padding[3],
877 request.alignment,
878 request.if_needed,
879 ),
880 FlexDirection::Row => Self::compute_scroll_offset(
881 current_offset,
882 target_geom.rect.x() - container_geom.rect.x(),
883 target_geom.rect.width(),
884 container_geom.rect.width(),
885 container_geom.content_size.width,
886 request.padding[0],
887 request.padding[1],
888 request.alignment,
889 request.if_needed,
890 ),
891 };
892
893 if (new_offset - current_offset).abs() > f32::EPSILON {
894 self.runtime_state
895 .scroll
896 .set_offset(container_id, new_offset);
897 ScrollIntoViewOutcome::Applied { changed: true }
898 } else {
899 ScrollIntoViewOutcome::Applied { changed: false }
900 }
901 }
902
903 fn resolve_scroll_container(
904 &self,
905 request: &ScrollIntoViewRequest,
906 ir: &CoreIR,
907 layout: &LayoutSnapshot,
908 ) -> Option<WidgetId> {
909 if let Some(container) = request.container {
910 return ir
911 .nodes
912 .contains_key(&container)
913 .then_some(container)
914 .filter(|id| layout.get_node_geometry(*id).is_some());
915 }
916
917 let mut current = ir.nodes.get(&request.target)?.parent;
918 while let Some(node_id) = current {
919 if let Some(direction) = Self::scroll_direction(ir, node_id) {
920 if Self::axis_matches(request.axis, direction)
921 && layout.get_node_geometry(node_id).is_some()
922 {
923 return Some(node_id);
924 }
925 }
926 current = ir.nodes.get(&node_id).and_then(|node| node.parent);
927 }
928
929 None
930 }
931
932 fn scroll_direction(ir: &CoreIR, node_id: WidgetId) -> Option<FlexDirection> {
933 match ir.nodes.get(&node_id).map(|node| &node.op) {
934 Some(Op::Layout(LayoutOp::Scroll { direction, .. })) => Some(*direction),
935 _ => None,
936 }
937 }
938
939 fn axis_matches(axis: ScrollAxis, direction: FlexDirection) -> bool {
940 matches!(
941 (axis, direction),
942 (ScrollAxis::Both, _)
943 | (ScrollAxis::Vertical, FlexDirection::Column)
944 | (ScrollAxis::Horizontal, FlexDirection::Row)
945 )
946 }
947
948 fn is_descendant_or_self(ir: &CoreIR, target: WidgetId, ancestor: WidgetId) -> bool {
949 let mut current = Some(target);
950 while let Some(node_id) = current {
951 if node_id == ancestor {
952 return true;
953 }
954 current = ir.nodes.get(&node_id).and_then(|node| node.parent);
955 }
956 false
957 }
958
959 fn compute_scroll_offset(
960 current_offset: f32,
961 target_content_start: f32,
962 target_size: f32,
963 viewport_size: f32,
964 content_size: f32,
965 padding_start: f32,
966 padding_end: f32,
967 alignment: ScrollAlignment,
968 if_needed: bool,
969 ) -> f32 {
970 let current_offset = Self::finite_or_zero(current_offset).max(0.0);
971 let viewport_size = Self::finite_or_zero(viewport_size).max(0.0);
972 let content_size = Self::finite_or_zero(content_size).max(0.0);
973 let target_size = Self::finite_or_zero(target_size).max(0.0);
974 let padding_start = Self::finite_or_zero(padding_start).max(0.0);
975 let padding_end = Self::finite_or_zero(padding_end).max(0.0);
976 let max_offset = (content_size - viewport_size).max(0.0);
977
978 if viewport_size <= f32::EPSILON || max_offset <= f32::EPSILON {
979 return 0.0;
980 }
981
982 let target_start = Self::finite_or_zero(target_content_start);
983 let target_end = target_start + target_size;
984 let reveal_start = target_start - padding_start;
985 let reveal_end = target_end + padding_end;
986 let viewport_start = current_offset;
987 let viewport_end = current_offset + viewport_size;
988
989 if if_needed && reveal_start >= viewport_start && reveal_end <= viewport_end {
990 return current_offset.min(max_offset);
991 }
992
993 let desired = match alignment {
994 ScrollAlignment::Start => reveal_start,
995 ScrollAlignment::Center => {
996 let padded_viewport = (viewport_size - padding_start - padding_end).max(0.0);
997 target_start - padding_start - (padded_viewport - target_size) * 0.5
998 }
999 ScrollAlignment::End => reveal_end - viewport_size,
1000 ScrollAlignment::Nearest => {
1001 if reveal_end - reveal_start > viewport_size {
1002 reveal_start
1003 } else if reveal_start < viewport_start {
1004 reveal_start
1005 } else if reveal_end > viewport_end {
1006 reveal_end - viewport_size
1007 } else {
1008 current_offset
1009 }
1010 }
1011 ScrollAlignment::Fraction(fraction) => {
1012 let fraction = Self::finite_or_zero(fraction).clamp(0.0, 1.0);
1013 let padded_viewport = (viewport_size - padding_start - padding_end).max(0.0);
1014 target_start - padding_start - (padded_viewport - target_size) * fraction
1015 }
1016 };
1017
1018 Self::finite_or_zero(desired).clamp(0.0, max_offset)
1019 }
1020
1021 fn finite_or_zero(value: f32) -> f32 {
1022 if value.is_finite() {
1023 value
1024 } else {
1025 0.0
1026 }
1027 }
1028
1029 fn emit_scroll_into_view_diag(
1030 kind: &'static str,
1031 request: &ScrollIntoViewRequest,
1032 container: Option<WidgetId>,
1033 ) {
1034 diag::emit(
1035 diag::DiagCategory::Input,
1036 diag::DiagLevel::Debug,
1037 diag::DiagEventKind::InputEvent {
1038 kind: format!(
1039 "scroll_into_view:{kind}:target={:?}:container={:?}",
1040 request.target,
1041 container.or(request.container)
1042 ),
1043 target: Some(request.target.as_u128()),
1044 position: None,
1045 },
1046 );
1047 }
1048
1049 pub fn post_layout_hook(&mut self, ir: &CoreIR, layout: &LayoutSnapshot) -> bool {
1054 let needs_follow_up_frame = self.apply_pending_scroll_into_view(ir, layout);
1055 let active_scroll_nodes: HashSet<WidgetId> = ir
1056 .nodes
1057 .iter()
1058 .filter_map(|(id, node)| match node.op {
1059 Op::Layout(LayoutOp::Scroll { .. }) => Some(*id),
1060 _ => None,
1061 })
1062 .collect();
1063 self.runtime_state
1064 .scroll
1065 .retain_active(&active_scroll_nodes);
1066 if self
1067 .runtime_state
1068 .gesture
1069 .scrollbar_drag
1070 .is_some_and(|drag| !active_scroll_nodes.contains(&drag.node_id))
1071 {
1072 self.runtime_state.gesture.scrollbar_drag = None;
1073 }
1074
1075 let mut current_heroes = HashMap::new();
1076
1077 for (id, node) in &ir.nodes {
1078 if let Op::Semantics(s) = &node.op {
1079 if let Some(tag) = &s.hero_tag {
1080 if let Some(geom) = layout.get_node_geometry(*id) {
1081 current_heroes.insert(tag.clone(), (*id, geom.rect));
1082 }
1083 }
1084 }
1085 }
1086
1087 for (tag, (_new_id, new_rect)) in ¤t_heroes {
1089 if let Some((_old_id, old_rect)) = self.runtime_state.hero.positions.get(tag) {
1090 if *new_rect != *old_rect {
1091 diag::emit(
1093 diag::DiagCategory::Layout,
1094 diag::DiagLevel::Debug,
1095 diag::DiagEventKind::AnchorPlacement {
1096 widget: 0,
1097 node: 0,
1098 rect_x: old_rect.origin.x,
1099 rect_y: old_rect.origin.y,
1100 rect_w: old_rect.size.width,
1101 rect_h: old_rect.size.height,
1102 place_left: new_rect.origin.x,
1103 place_top: new_rect.origin.y,
1104 note: Some(format!("Hero flight: {}", tag)),
1105 },
1106 );
1107 }
1108 }
1109 }
1110
1111 self.runtime_state.hero.positions = current_heroes;
1112 needs_follow_up_frame
1113 }
1114
1115 pub fn handle_input(
1116 &mut self,
1117 event: InputEvent,
1118 ir: &CoreIR,
1119 layout: &LayoutSnapshot,
1120 ) -> Result<()> {
1121 use crate::hit_test::{
1122 find_neighbor_focus_node, find_next_focus_node, hit_test_with_scroll, FocusDirection,
1123 };
1124 use crate::input::gesture::GestureController;
1125 use crate::input::hover::HoverController;
1126 use crate::input::selectable_text::SelectableTextController;
1127 use crate::input::slider::SliderController;
1128 use crate::input::text::TextInputController;
1129 use crate::input::{ControllerContext, InputController};
1130 use crate::scrollbar::scrollbar_hit_test;
1131 use crate::ui::custom_render::downcast_render_object;
1132
1133 self.reconcile_focus(ir)?;
1134
1135 if self.runtime_state.interaction.focused.is_none() {
1136 if let Some(autofocus_id) = Self::find_autofocus_node(ir) {
1137 self.runtime_state
1138 .interaction
1139 .set_focused(Some(autofocus_id));
1140 if let Some(ime_handler) = &self.ime_handler {
1141 let accepts_text = ir
1142 .nodes
1143 .get(&autofocus_id)
1144 .and_then(|node| match &node.op {
1145 Op::Semantics(semantics) => {
1146 Some(semantics.role == fission_ir::semantics::Role::TextInput)
1147 }
1148 _ => None,
1149 })
1150 .unwrap_or(false);
1151 ime_handler.set_ime_allowed(accepts_text);
1152 }
1153 }
1154 }
1155
1156 if matches!(event, InputEvent::Pointer(_)) {
1157 let dispatched_actions = {
1158 let mut ctx = ControllerContext {
1159 ir,
1160 layout,
1161 text_edit: &mut self.runtime_state.text_edit,
1162 selectable_text: &mut self.runtime_state.selectable_text,
1163 context_menu: &mut self.runtime_state.context_menu,
1164 interaction: &mut self.runtime_state.interaction,
1165 scroll: &mut self.runtime_state.scroll,
1166 gesture: &mut self.runtime_state.gesture,
1167 clipboard: self.clipboard_backend.as_ref(),
1168 measurer: self.measurer.as_ref(),
1169 dispatched_actions: Vec::new(),
1170 };
1171 let mut hover_controller = HoverController;
1172 let _ = hover_controller.handle_event(&mut ctx, &event);
1173 ctx.dispatched_actions
1174 };
1175 self.dispatch_input_actions(dispatched_actions)?;
1176 }
1177
1178 let pointer_targets_scrollbar = match &event {
1184 InputEvent::Pointer(PointerEvent::Down { point, button, .. })
1185 if matches!(button, PointerButton::Primary) =>
1186 {
1187 scrollbar_hit_test(ir, layout, &self.runtime_state.scroll, *point).is_some()
1188 }
1189 InputEvent::Pointer(PointerEvent::Move { .. })
1190 | InputEvent::Pointer(PointerEvent::Up { .. }) => {
1191 self.runtime_state.gesture.scrollbar_drag.is_some()
1192 }
1193 InputEvent::Pointer(PointerEvent::Scroll { point, .. }) => {
1194 scrollbar_hit_test(ir, layout, &self.runtime_state.scroll, *point).is_some()
1195 }
1196 _ => false,
1197 };
1198
1199 if !pointer_targets_scrollbar {
1200 if let Some(point) = Self::event_point(&event) {
1201 if let Some(hit_node_id) =
1202 hit_test_with_scroll(ir, layout, &self.runtime_state.scroll, point)
1203 {
1204 let mut target_ro: Option<(WidgetId, &fission_ir::AnyRenderObject)> = None;
1209 {
1210 let mut walk = Some(hit_node_id);
1211 while let Some(nid) = walk {
1212 if let Some(ro) = ir.custom_render_objects.get(&nid) {
1213 target_ro = Some((nid, ro));
1214 break;
1215 }
1216 walk = ir.nodes.get(&nid).and_then(|n| n.parent);
1217 }
1218 }
1219 if target_ro.is_none() {
1220 for (ro_nid, ro) in &ir.custom_render_objects {
1221 if let Some(rect) = layout.get_node_rect(*ro_nid) {
1222 if rect.contains(point) {
1223 target_ro = Some((*ro_nid, ro));
1224 break;
1225 }
1226 }
1227 }
1228 }
1229
1230 if let Some((nid, any_ro)) = target_ro {
1231 if let Some(render_obj) = downcast_render_object(any_ro) {
1232 let mut node_rect = layout
1233 .get_node_rect(nid)
1234 .unwrap_or(LayoutRect::new(0.0, 0.0, 0.0, 0.0));
1235 {
1238 let mut walk = ir.nodes.get(&nid).and_then(|n| n.parent);
1239 while let Some(pid) = walk {
1240 if let Some(pnode) = ir.nodes.get(&pid) {
1241 if let fission_ir::Op::Layout(
1242 fission_ir::LayoutOp::Scroll { direction, .. },
1243 ) = &pnode.op
1244 {
1245 let off = self.runtime_state.scroll.get_offset(pid);
1246 match direction {
1247 fission_ir::FlexDirection::Row => {
1248 node_rect.origin.x -= off
1249 }
1250 fission_ir::FlexDirection::Column => {
1251 node_rect.origin.y -= off
1252 }
1253 }
1254 }
1255 walk = pnode.parent;
1256 } else {
1257 break;
1258 }
1259 }
1260 }
1261 let result = render_obj.handle_event(nid, &event, node_rect);
1262 if result.handled {
1263 if matches!(event, InputEvent::Pointer(PointerEvent::Down { .. })) {
1265 let old_focused_id = self.runtime_state.interaction.focused;
1266 if Some(nid) != old_focused_id {
1267 self.clear_text_pending_on_blur(old_focused_id, Some(nid));
1268 self.dispatch_custom_blur_actions(ir, old_focused_id)?;
1269 }
1270 self.runtime_state.interaction.set_focused(Some(nid));
1271 if let Some(ime_handler) = &self.ime_handler {
1272 let accepts_text = render_obj.accepts_text_input();
1273 ime_handler.set_ime_allowed(accepts_text);
1274 if accepts_text {
1275 if let Some(rect) =
1276 render_obj.ime_cursor_area(node_rect)
1277 {
1278 ime_handler.set_ime_cursor_area(rect);
1279 }
1280 }
1281 }
1282 }
1283 for (target, envelope) in result.actions {
1285 self.dispatch_node(envelope, target)?;
1286 }
1287 self.update_focused_ime_state(ir, layout);
1288 return Ok(());
1289 }
1290 }
1291 }
1292 }
1293 }
1294 }
1295
1296 if matches!(event, InputEvent::Keyboard(_) | InputEvent::Ime(_)) {
1302 if let Some(focused_id) = self.runtime_state.interaction.focused {
1303 let mut walk_id = Some(focused_id);
1304 while let Some(nid) = walk_id {
1305 if let Some(any_ro) = ir.custom_render_objects.get(&nid) {
1306 if let Some(render_obj) = downcast_render_object(any_ro) {
1307 let node_rect = layout
1308 .get_node_rect(nid)
1309 .unwrap_or(LayoutRect::new(0.0, 0.0, 0.0, 0.0));
1310 let result = render_obj.handle_event(nid, &event, node_rect);
1311 if result.handled {
1312 for (target, envelope) in result.actions {
1313 self.dispatch_node(envelope, target)?;
1314 }
1315 self.update_focused_ime_state(ir, layout);
1316 return Ok(());
1317 }
1318 }
1319 }
1320 walk_id = ir.nodes.get(&nid).and_then(|n| n.parent);
1321 }
1322 }
1323 }
1324
1325 let (handled, dispatched_actions) = {
1326 let mut ctx = ControllerContext {
1327 ir,
1328 layout,
1329 text_edit: &mut self.runtime_state.text_edit,
1330 selectable_text: &mut self.runtime_state.selectable_text,
1331 context_menu: &mut self.runtime_state.context_menu,
1332 interaction: &mut self.runtime_state.interaction,
1333 scroll: &mut self.runtime_state.scroll,
1334 gesture: &mut self.runtime_state.gesture,
1335 clipboard: self.clipboard_backend.as_ref(),
1336 measurer: self.measurer.as_ref(),
1337 dispatched_actions: Vec::new(),
1338 };
1339
1340 let mut hover_controller = HoverController;
1341 let _ = hover_controller.handle_event(&mut ctx, &event);
1342
1343 let mut selectable_text_controller = SelectableTextController;
1344 let handled = if selectable_text_controller.handle_event(&mut ctx, &event) {
1345 true
1346 } else {
1347 let mut gesture_controller = GestureController;
1348 if gesture_controller.handle_event(&mut ctx, &event) {
1349 true
1350 } else {
1351 let mut text_controller = TextInputController;
1352 if text_controller.handle_event(&mut ctx, &event) {
1353 true
1354 } else {
1355 let mut slider_controller = SliderController;
1356 slider_controller.handle_event(&mut ctx, &event)
1357 }
1358 }
1359 };
1360 (handled, ctx.dispatched_actions)
1361 };
1362
1363 self.dispatch_input_actions(dispatched_actions)?;
1364
1365 if handled {
1366 if matches!(event, InputEvent::Pointer(PointerEvent::Up { .. })) {
1367 self.runtime_state.interaction.pressed.clear();
1368 self.runtime_state.interaction.last_down_point = None;
1369 }
1370 self.update_focused_ime_state(ir, layout);
1371 return Ok(());
1372 }
1373
1374 match event {
1375 InputEvent::Pointer(PointerEvent::Scroll { point, delta, .. }) => {
1376 let trace_scroll =
1377 std::env::var("FISSION_SCROLL_TRACE").ok().as_deref() == Some("1");
1378 if trace_scroll {
1379 eprintln!(
1380 "[scroll-trace] event point=({:.1},{:.1}) delta=({:.1},{:.1})",
1381 point.x, point.y, delta.x, delta.y
1382 );
1383 }
1384 let hit_node_id = scrollbar_hit_test(ir, layout, &self.runtime_state.scroll, point)
1385 .map(|hit| hit.geometry.node_id)
1386 .or_else(|| {
1387 hit_test_with_scroll(ir, layout, &self.runtime_state.scroll, point)
1388 });
1389 if let Some(hit_node_id) = hit_node_id {
1390 if trace_scroll {
1391 eprintln!("[scroll-trace] hit_node={}", hit_node_id.as_u128());
1392 }
1393 let mut current_id = Some(hit_node_id);
1394 while let Some(node_id) = current_id {
1395 if let Some(node) = ir.nodes.get(&node_id) {
1396 if let Op::Layout(LayoutOp::Scroll { direction, .. }) = &node.op {
1397 let current_offset = self.runtime_state.scroll.get_offset(node_id);
1398 let delta_val = match direction {
1399 FlexDirection::Row => delta.x,
1400 FlexDirection::Column => delta.y,
1401 };
1402 let mut new_offset = current_offset + delta_val;
1403
1404 let mut max_offset = 0.0f32;
1405 let mut viewport_w = 0.0f32;
1406 let mut viewport_h = 0.0f32;
1407 let mut content_w = 0.0f32;
1408 let mut content_h = 0.0f32;
1409 if let Some(geom) = layout.get_node_geometry(node_id) {
1410 viewport_w = geom.rect.width();
1411 viewport_h = geom.rect.height();
1412 content_w = geom.content_size.width;
1413 content_h = geom.content_size.height;
1414 max_offset = if matches!(direction, FlexDirection::Row) {
1415 (geom.content_size.width - geom.rect.width()).max(0.0)
1416 } else {
1417 (geom.content_size.height - geom.rect.height()).max(0.0)
1418 };
1419 new_offset = new_offset.clamp(0.0, max_offset);
1420 }
1421
1422 if trace_scroll {
1423 eprintln!(
1424 "[scroll-trace] scroll_node={} axis={} offset={:.1}->{:.1} max={:.1} viewport=({:.1},{:.1}) content=({:.1},{:.1})",
1425 node_id.as_u128(),
1426 match direction { FlexDirection::Row => "x", FlexDirection::Column => "y" },
1427 current_offset,
1428 new_offset,
1429 max_offset,
1430 viewport_w,
1431 viewport_h,
1432 content_w,
1433 content_h
1434 );
1435 }
1436
1437 {
1438 use fission_diagnostics::prelude as diag;
1439 diag::emit(
1440 diag::DiagCategory::Input,
1441 diag::DiagLevel::Debug,
1442 diag::DiagEventKind::ScrollUpdate {
1443 node: node_id.as_u128(),
1444 axis: match direction {
1445 FlexDirection::Row => "x".into(),
1446 FlexDirection::Column => "y".into(),
1447 },
1448 point_x: point.x,
1449 point_y: point.y,
1450 delta: delta_val,
1451 old_offset: current_offset,
1452 new_offset,
1453 max_offset,
1454 viewport_w,
1455 viewport_h,
1456 content_w,
1457 content_h,
1458 },
1459 );
1460 }
1461
1462 self.runtime_state.scroll.set_offset(node_id, new_offset);
1463 if (new_offset - current_offset).abs() > 0.001 {
1467 break;
1468 }
1469 }
1471 current_id = node.parent;
1472 } else {
1473 break;
1474 }
1475 }
1476 } else if trace_scroll {
1477 eprintln!("[scroll-trace] hit_test: no node");
1478 }
1479 }
1480 InputEvent::Keyboard(KeyEvent::Down {
1481 key_code,
1482 modifiers,
1483 }) => match key_code {
1484 KeyCode::Tab => {
1485 let reverse = (modifiers & 1) != 0;
1486 let old_focus = self.runtime_state.interaction.focused;
1487 let next =
1488 find_next_focus_node(ir, self.runtime_state.interaction.focused, reverse);
1489 if next != old_focus {
1490 self.clear_text_pending_on_blur(old_focus, next);
1491 self.dispatch_custom_blur_actions(ir, old_focus)?;
1492 }
1493 self.runtime_state.interaction.set_focused(next);
1494 }
1495 KeyCode::Up | KeyCode::Down | KeyCode::Left | KeyCode::Right => {
1496 let reverse = matches!(key_code, KeyCode::Up | KeyCode::Left);
1497 let old_focus = self.runtime_state.interaction.focused;
1498 let next = if let Some(focused) = old_focus {
1499 let dir = match key_code {
1500 KeyCode::Up => FocusDirection::Up,
1501 KeyCode::Down => FocusDirection::Down,
1502 KeyCode::Left => FocusDirection::Left,
1503 KeyCode::Right => FocusDirection::Right,
1504 _ => unreachable!(),
1505 };
1506 find_neighbor_focus_node(ir, layout, focused, dir)
1507 .or_else(|| find_next_focus_node(ir, Some(focused), reverse))
1508 } else {
1509 find_next_focus_node(ir, None, reverse)
1510 };
1511 if next != old_focus {
1512 self.clear_text_pending_on_blur(old_focus, next);
1513 self.dispatch_custom_blur_actions(ir, old_focus)?;
1514 self.runtime_state.interaction.set_focused(next);
1515 }
1516 }
1517 KeyCode::Enter | KeyCode::Space => {
1518 if let Some(focused_id) = self.runtime_state.interaction.focused {
1519 let mut current_id = Some(focused_id);
1520 while let Some(node_id) = current_id {
1521 if let Some(node) = ir.nodes.get(&node_id) {
1522 if let Op::Semantics(semantics) = &node.op {
1523 if let Some(action_entry) =
1524 semantics.actions.entries.iter().find(|entry| {
1525 entry.trigger
1526 == fission_ir::semantics::ActionTrigger::Default
1527 })
1528 {
1529 if let Some(payload) = &action_entry.payload_data {
1530 let envelope = ActionEnvelope {
1531 id: ActionId::from_u128(action_entry.action_id),
1532 payload: payload.clone(),
1533 };
1534 let input = crate::input::scoped_action_input(
1535 ir,
1536 node_id,
1537 ActionInput::None,
1538 );
1539 return self.dispatch_node_with_input(
1540 envelope, node_id, &input,
1541 );
1542 }
1543 }
1544 }
1545 current_id = node.parent;
1546 } else {
1547 break;
1548 }
1549 }
1550 }
1551 }
1552 _ => {}
1553 },
1554 InputEvent::Pointer(PointerEvent::Down { point, .. }) => {
1555 if let Some(hit_node_id) =
1556 hit_test_with_scroll(ir, layout, &self.runtime_state.scroll, point)
1557 {
1558 diag::emit(
1559 diag::DiagCategory::Input,
1560 diag::DiagLevel::Debug,
1561 diag::DiagEventKind::InputEvent {
1562 kind: "pointer_down_hit".into(),
1563 target: Some(hit_node_id.as_u128()),
1564 position: Some((point.x, point.y)),
1565 },
1566 );
1567 let mut focus_candidate = Some(hit_node_id);
1568 while let Some(node_id) = focus_candidate {
1569 if let Some(node) = ir.nodes.get(&node_id) {
1570 if let Op::Semantics(s) = &node.op {
1571 if s.focusable {
1572 if s.focus_policy == FocusPolicy::PreserveCurrentOnPointer {
1573 break;
1574 }
1575 let old_focused_id = self.runtime_state.interaction.focused;
1576 if Some(node_id) != old_focused_id {
1577 self.clear_text_pending_on_blur(
1578 old_focused_id,
1579 Some(node_id),
1580 );
1581 self.dispatch_custom_blur_actions(ir, old_focused_id)?;
1582
1583 if s.role == fission_ir::semantics::Role::TextInput {
1584 if let Some(ime_handler) = &self.ime_handler {
1585 ime_handler.set_ime_allowed(true);
1586 }
1587 } else if let Some(ime_handler) = &self.ime_handler {
1588 ime_handler.set_ime_allowed(false);
1589 }
1590 }
1591 self.runtime_state.interaction.set_focused(Some(node_id));
1592 break;
1593 }
1594 }
1595 focus_candidate = node.parent;
1596 } else {
1597 break;
1598 }
1599 }
1600 if focus_candidate.is_none() {
1601 let old_focused_id = self.runtime_state.interaction.focused;
1602 if let Some(old_focused_id) = self.runtime_state.interaction.focused {
1603 if let Some(old_node) = ir.nodes.get(&old_focused_id) {
1604 if let Op::Semantics(s) = &old_node.op {
1605 if s.role == fission_ir::semantics::Role::TextInput {
1606 if let Some(ime_handler) = &self.ime_handler {
1607 ime_handler.set_ime_allowed(false);
1608 }
1609 }
1610 }
1611 }
1612 }
1613 self.clear_text_pending_on_blur(old_focused_id, None);
1614 self.dispatch_custom_blur_actions(ir, old_focused_id)?;
1615 self.runtime_state.interaction.set_focused(None);
1616 }
1617
1618 let mut current_pressed_id = Some(hit_node_id);
1619 while let Some(node_id) = current_pressed_id {
1620 self.runtime_state.interaction.set_pressed(node_id, true);
1621 if let Some(node) = ir.nodes.get(&node_id) {
1622 current_pressed_id = node.parent;
1623 } else {
1624 break;
1625 }
1626 }
1627 self.runtime_state.interaction.last_down_point = Some(point);
1628
1629 if let Some(focused_id) = self.runtime_state.interaction.focused {
1630 if let Some(node) = ir.nodes.get(&focused_id) {
1631 if let Op::Semantics(s) = &node.op {
1632 if s.role == fission_ir::semantics::Role::TextInput {
1633 if let Some(ime_handler) = &self.ime_handler {
1634 ime_handler.set_ime_cursor_area(LayoutRect::new(
1635 point.x, point.y, 2.0, 16.0,
1636 ));
1637 }
1638 }
1639 }
1640 }
1641 }
1642 } else {
1643 let old_focused_id = self.runtime_state.interaction.focused;
1644 if let Some(old_focused_id) = self.runtime_state.interaction.focused {
1645 if let Some(old_node) = ir.nodes.get(&old_focused_id) {
1646 if let Op::Semantics(s) = &old_node.op {
1647 if s.role == fission_ir::semantics::Role::TextInput {
1648 if let Some(ime_handler) = &self.ime_handler {
1649 ime_handler.set_ime_allowed(false);
1650 }
1651 }
1652 }
1653 }
1654 }
1655 self.clear_text_pending_on_blur(old_focused_id, None);
1656 self.dispatch_custom_blur_actions(ir, old_focused_id)?;
1657 self.runtime_state.interaction.set_focused(None);
1658 }
1659 }
1660 InputEvent::Pointer(PointerEvent::Up { point, .. }) => {
1661 self.runtime_state.interaction.pressed.clear();
1662 self.runtime_state.interaction.last_down_point = None;
1663 if let Some(hit_node_id) =
1664 hit_test_with_scroll(ir, layout, &self.runtime_state.scroll, point)
1665 {
1666 let mut current_id = Some(hit_node_id);
1667 while let Some(node_id) = current_id {
1668 if let Some(node) = ir.nodes.get(&node_id) {
1669 if let Op::Semantics(semantics) = &node.op {
1670 if semantics.role == fission_ir::semantics::Role::TextInput {
1671 } else if let Some(action_entry) =
1673 semantics.actions.entries.iter().find(|entry| {
1674 entry.trigger
1675 == fission_ir::semantics::ActionTrigger::Default
1676 })
1677 {
1678 if let Some(payload) = &action_entry.payload_data {
1679 let envelope = ActionEnvelope {
1680 id: ActionId::from_u128(action_entry.action_id),
1681 payload: payload.clone(),
1682 };
1683 diag::emit(
1684 diag::DiagCategory::Input,
1685 diag::DiagLevel::Debug,
1686 diag::DiagEventKind::InputEvent {
1687 kind: "pointer_up_dispatch".into(),
1688 target: Some(node_id.as_u128()),
1689 position: Some((point.x, point.y)),
1690 },
1691 );
1692 let input = crate::input::scoped_action_input(
1693 ir,
1694 node_id,
1695 ActionInput::None,
1696 );
1697 return self
1698 .dispatch_node_with_input(envelope, node_id, &input);
1699 }
1700 }
1701 }
1702 current_id = node.parent;
1703 } else {
1704 break;
1705 }
1706 }
1707 }
1708 }
1709 _ => {}
1710 }
1711 self.update_focused_ime_state(ir, layout);
1712 Ok(())
1713 }
1714
1715 pub fn clear_hover_state(&mut self, ir: &CoreIR, point: Option<LayoutPoint>) -> Result<bool> {
1716 use crate::input::hover::HoverController;
1717 use crate::input::ControllerContext;
1718
1719 let dispatched_actions = {
1720 let layout = &LayoutSnapshot::new(LayoutSize::ZERO);
1721 let mut ctx = ControllerContext {
1722 ir,
1723 layout,
1724 text_edit: &mut self.runtime_state.text_edit,
1725 selectable_text: &mut self.runtime_state.selectable_text,
1726 context_menu: &mut self.runtime_state.context_menu,
1727 interaction: &mut self.runtime_state.interaction,
1728 scroll: &mut self.runtime_state.scroll,
1729 gesture: &mut self.runtime_state.gesture,
1730 clipboard: self.clipboard_backend.as_ref(),
1731 measurer: self.measurer.as_ref(),
1732 dispatched_actions: Vec::new(),
1733 };
1734 let changed = HoverController::clear(&mut ctx, point);
1735 (changed, ctx.dispatched_actions)
1736 };
1737 self.dispatch_input_actions(dispatched_actions.1)?;
1738 Ok(dispatched_actions.0)
1739 }
1740
1741 fn dispatch_input_actions(
1742 &mut self,
1743 dispatched_actions: Vec<(WidgetId, ActionEnvelope, ActionInput)>,
1744 ) -> Result<()> {
1745 for (target, action, input) in dispatched_actions {
1746 self.dispatch_node_with_input(action, target, &input)?;
1747 }
1748 Ok(())
1749 }
1750
1751 fn update_focused_ime_state(&mut self, ir: &CoreIR, layout: &LayoutSnapshot) {
1752 let Some(ime_handler) = self.ime_handler.clone() else {
1753 return;
1754 };
1755 let Some(focused_id) = self.runtime_state.interaction.focused else {
1756 ime_handler.set_ime_allowed(false);
1757 return;
1758 };
1759
1760 let mut walk = Some(focused_id);
1761 while let Some(node_id) = walk {
1762 if let Some(any_ro) = ir.custom_render_objects.get(&node_id) {
1763 if let Some(render_obj) = crate::ui::custom_render::downcast_render_object(any_ro) {
1764 let accepts_text = render_obj.accepts_text_input();
1765 ime_handler.set_ime_allowed(accepts_text);
1766 if accepts_text {
1767 let rect =
1768 Self::visual_node_rect(ir, layout, &self.runtime_state.scroll, node_id)
1769 .unwrap_or(LayoutRect::new(0.0, 0.0, 0.0, 0.0));
1770 if let Some(cursor_area) = render_obj.ime_cursor_area(rect) {
1771 ime_handler.set_ime_cursor_area(cursor_area);
1772 }
1773 }
1774 return;
1775 }
1776 }
1777 walk = ir.nodes.get(&node_id).and_then(|node| node.parent);
1778 }
1779
1780 let accepts_text = ir
1781 .nodes
1782 .get(&focused_id)
1783 .and_then(|node| match &node.op {
1784 Op::Semantics(semantics) => {
1785 Some(semantics.role == fission_ir::semantics::Role::TextInput)
1786 }
1787 _ => None,
1788 })
1789 .unwrap_or(false);
1790 ime_handler.set_ime_allowed(accepts_text);
1791
1792 if accepts_text {
1793 let cursor_area = {
1794 let mut ctx = crate::input::ControllerContext {
1795 ir,
1796 layout,
1797 text_edit: &mut self.runtime_state.text_edit,
1798 selectable_text: &mut self.runtime_state.selectable_text,
1799 context_menu: &mut self.runtime_state.context_menu,
1800 interaction: &mut self.runtime_state.interaction,
1801 scroll: &mut self.runtime_state.scroll,
1802 gesture: &mut self.runtime_state.gesture,
1803 clipboard: self.clipboard_backend.as_ref(),
1804 measurer: self.measurer.as_ref(),
1805 dispatched_actions: Vec::new(),
1806 };
1807 crate::input::text::TextInputController::ime_cursor_area(&mut ctx, focused_id)
1808 };
1809 if let Some(cursor_area) = cursor_area {
1810 ime_handler.set_ime_cursor_area(cursor_area);
1811 }
1812 }
1813 }
1814
1815 fn visual_node_rect(
1816 ir: &CoreIR,
1817 layout: &LayoutSnapshot,
1818 scroll: &crate::env::ScrollStateMap,
1819 node_id: WidgetId,
1820 ) -> Option<LayoutRect> {
1821 let mut rect = layout.get_node_rect(node_id)?;
1822 let mut walk = ir.nodes.get(&node_id).and_then(|node| node.parent);
1823 while let Some(parent_id) = walk {
1824 let Some(parent) = ir.nodes.get(&parent_id) else {
1825 break;
1826 };
1827 if let Op::Layout(LayoutOp::Scroll { direction, .. }) = &parent.op {
1828 let offset = scroll.get_offset(parent_id);
1829 match direction {
1830 FlexDirection::Row => rect.origin.x -= offset,
1831 FlexDirection::Column => rect.origin.y -= offset,
1832 }
1833 }
1834 walk = parent.parent;
1835 }
1836 Some(rect)
1837 }
1838
1839 fn clear_text_pending_on_blur(
1840 &mut self,
1841 old_focus: Option<WidgetId>,
1842 new_focus: Option<WidgetId>,
1843 ) {
1844 if old_focus == new_focus {
1845 return;
1846 }
1847 if let Some(old_id) = old_focus {
1848 if let Some(st) = self.runtime_state.text_edit.states.get_mut(&old_id) {
1849 st.pending_model_sync = false;
1850 st.clear_preedit();
1851 }
1852 }
1853 }
1854
1855 fn dispatch_custom_blur_actions(
1856 &mut self,
1857 ir: &CoreIR,
1858 old_focus: Option<WidgetId>,
1859 ) -> Result<()> {
1860 if let Some(old_id) = old_focus {
1861 if let Some(any_ro) = ir.custom_render_objects.get(&old_id) {
1862 if let Some(render_obj) = crate::ui::custom_render::downcast_render_object(any_ro) {
1863 if render_obj.accepts_text_input() {
1864 if let Some(ime_handler) = &self.ime_handler {
1865 ime_handler.set_ime_allowed(false);
1866 }
1867 }
1868 for (target, envelope) in render_obj.blur_actions(old_id) {
1869 self.dispatch_node(envelope, target)?;
1870 }
1871 }
1872 }
1873 }
1874 Ok(())
1875 }
1876
1877 pub fn hit_test(
1878 &self,
1879 point: LayoutPoint,
1880 ir: &CoreIR,
1881 snapshot: &LayoutSnapshot,
1882 ) -> Option<WidgetId> {
1883 if let Some(root) = ir.root {
1884 return self.hit_test_recursive(root, point, ir, snapshot);
1885 }
1886 None
1887 }
1888
1889 fn hit_test_recursive(
1890 &self,
1891 node_id: WidgetId,
1892 point: LayoutPoint,
1893 ir: &CoreIR,
1894 snapshot: &LayoutSnapshot,
1895 ) -> Option<WidgetId> {
1896 if let Some(geom) = snapshot.nodes.get(&node_id) {
1897 if geom.rect.contains(point) {
1898 if let Some(node) = ir.nodes.get(&node_id) {
1899 for child in node.children.iter().rev() {
1900 let mut child_point = point;
1901
1902 if let Op::Layout(LayoutOp::Scroll { direction, .. }) = &node.op {
1903 if !geom.rect.contains(point) {
1904 continue;
1905 }
1906 let offset = self.runtime_state.scroll.get_offset(node_id);
1907 match direction {
1908 FlexDirection::Row => child_point.x += offset,
1909 FlexDirection::Column => child_point.y += offset,
1910 }
1911 }
1912
1913 if let Op::Layout(LayoutOp::Transform { transform }) = &node.op {
1914 let mat = Mat4::from_cols_array(transform);
1915 let local_x = point.x - geom.rect.origin.x;
1952 let local_y = point.y - geom.rect.origin.y;
1953
1954 let p = Vec4::new(local_x, local_y, 0.0, 1.0);
1955 let inv = mat.inverse();
1956 let transformed = inv * p;
1957
1958 child_point = LayoutPoint::new(
1959 transformed.x + geom.rect.origin.x,
1960 transformed.y + geom.rect.origin.y,
1961 );
1962 }
1963
1964 if let Some(hit) =
1965 self.hit_test_recursive(*child, child_point, ir, snapshot)
1966 {
1967 return Some(hit);
1968 }
1969 }
1970
1971 match &node.op {
1972 Op::Paint(_)
1973 | Op::Layout(LayoutOp::Scroll { .. })
1974 | Op::Layout(LayoutOp::Embed { .. }) => return Some(node_id),
1975 _ => return None,
1976 }
1977 }
1978 return None;
1979 }
1980 }
1981 None
1982 }
1983
1984 fn event_point(event: &InputEvent) -> Option<LayoutPoint> {
1990 match event {
1991 InputEvent::Pointer(PointerEvent::Down { point, .. })
1992 | InputEvent::Pointer(PointerEvent::Up { point, .. })
1993 | InputEvent::Pointer(PointerEvent::Move { point, .. })
1994 | InputEvent::Pointer(PointerEvent::Scroll { point, .. }) => Some(*point),
1995 _ => None,
1996 }
1997 }
1998
1999 fn find_autofocus_node(ir: &CoreIR) -> Option<WidgetId> {
2000 fn walk(ir: &CoreIR, node_id: WidgetId) -> Option<WidgetId> {
2001 let node = ir.nodes.get(&node_id)?;
2002 if let Op::Semantics(semantics) = &node.op {
2003 if semantics.autofocus && semantics.focusable && !semantics.disabled {
2004 return Some(node_id);
2005 }
2006 }
2007 for child_id in &node.children {
2008 if let Some(found) = walk(ir, *child_id) {
2009 return Some(found);
2010 }
2011 }
2012 None
2013 }
2014
2015 ir.root.and_then(|root| walk(ir, root))
2016 }
2017
2018 pub fn reconcile_resources(
2019 &mut self,
2020 declarations: Vec<RuntimeResourceDeclaration>,
2021 ) -> Result<()> {
2022 let now = self.clock().current_time();
2023 let mut existing = std::mem::take(&mut self.active_resources);
2024 let mut next = HashMap::new();
2025
2026 for declaration in declarations {
2027 let key = declaration.key.clone();
2028 match existing.remove(&key) {
2029 Some(current)
2030 if current.policy == declaration.policy
2031 && current.deps == declaration.deps
2032 && current.matches_kind(&declaration.kind) =>
2033 {
2034 next.insert(key, current);
2035 }
2036 Some(current) if declaration.policy == ResourcePolicy::PreserveOnChange => {
2037 next.insert(key, current);
2038 }
2039 Some(current) => {
2040 self.stop_resource(&key, ¤t);
2041 let replacement = self.start_resource(declaration, now);
2042 next.insert(key, replacement);
2043 }
2044 None => {
2045 let resource = self.start_resource(declaration, now);
2046 next.insert(key, resource);
2047 }
2048 }
2049 }
2050
2051 for (key, resource) in existing {
2052 self.stop_resource(&key, &resource);
2053 }
2054
2055 self.active_resources = next;
2056 Ok(())
2057 }
2058
2059 pub fn resource_generation(&self, key: &str) -> Option<u64> {
2060 self.active_resources
2061 .get(key)
2062 .map(|resource| resource.generation)
2063 }
2064
2065 pub fn is_resource_current(&self, resource: &ResourceExecutionContext) -> bool {
2066 self.resource_generation(&resource.key) == Some(resource.generation)
2067 }
2068
2069 fn start_resource(
2070 &mut self,
2071 declaration: RuntimeResourceDeclaration,
2072 now: CurrentTime,
2073 ) -> ActiveResource {
2074 let generation = self.next_resource_generation;
2075 self.next_resource_generation += 1;
2076
2077 let context = ResourceExecutionContext {
2078 key: declaration.key.clone(),
2079 generation,
2080 };
2081
2082 let kind = match declaration.kind {
2083 RuntimeResourceKind::Job(mut job) => {
2084 job.effect.resource = Some(context);
2085 self.enqueue_effect(job.effect);
2086 ActiveResourceKind::Job
2087 }
2088 RuntimeResourceKind::Service(mut service) => {
2089 service.effect.resource = Some(context);
2090 let (service_name, slot_key) = match &service.effect.effect {
2091 crate::Effect::StartService(payload) => {
2092 (payload.service_name.clone(), payload.slot_key.clone())
2093 }
2094 _ => unreachable!("service resource must lower to StartService"),
2095 };
2096 self.enqueue_effect(service.effect);
2097 ActiveResourceKind::Service {
2098 service_name,
2099 slot_key,
2100 }
2101 }
2102 RuntimeResourceKind::Timer(timer) => self.start_timer_resource(timer, now),
2103 };
2104
2105 ActiveResource {
2106 generation,
2107 deps: declaration.deps,
2108 policy: declaration.policy,
2109 kind,
2110 }
2111 }
2112
2113 fn start_timer_resource(&self, timer: TimerResource, now: CurrentTime) -> ActiveResourceKind {
2114 let interval_ms = timer.interval_ms.max(1);
2115 ActiveResourceKind::Timer {
2116 interval_ms,
2117 payload: timer.payload,
2118 on_tick: timer.on_tick,
2119 next_fire_at: if timer.immediate {
2120 now
2121 } else {
2122 now.saturating_add(interval_ms)
2123 },
2124 }
2125 }
2126
2127 fn stop_resource(&mut self, key: &str, resource: &ActiveResource) {
2128 if let ActiveResourceKind::Service {
2129 service_name,
2130 slot_key,
2131 } = &resource.kind
2132 {
2133 self.enqueue_effect(EffectEnvelope {
2134 req_id: 0,
2135 effect: crate::Effect::StopService(ServiceStopPayload {
2136 service_name: service_name.clone(),
2137 slot_key: slot_key.clone(),
2138 }),
2139 on_ok: None,
2140 on_err: None,
2141 service_bindings: None,
2142 resource: Some(ResourceExecutionContext {
2143 key: key.to_string(),
2144 generation: resource.generation,
2145 }),
2146 });
2147 }
2148 }
2149}
2150
2151impl ActiveResource {
2152 fn matches_kind(&self, kind: &RuntimeResourceKind) -> bool {
2153 matches!(
2154 (&self.kind, kind),
2155 (ActiveResourceKind::Job, RuntimeResourceKind::Job(_))
2156 | (
2157 ActiveResourceKind::Timer { .. },
2158 RuntimeResourceKind::Timer(_)
2159 )
2160 | (
2161 ActiveResourceKind::Service { .. },
2162 RuntimeResourceKind::Service(_)
2163 )
2164 )
2165 }
2166}