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