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