1use crate::action::{Action, ActionEnvelope, ActionId, AppState};
2use crate::effect::{ActionInput, EffectEnvelope};
3use crate::env::{
4 ActiveAnimation, AnimationStateMap, Env, InteractionStateMap, RuntimeState, ScrollStateMap,
5 VideoStateMap, VideoStatus,
6};
7use crate::registry::{ActionRegistry, AnimationRequest, AnimationStartValue, VideoRegistration};
8use crate::BoxedReducer;
9use crate::{
10 Clipboard, Clock, CurrentTime, ImeHandler, InputEvent, KeyCode, KeyEvent, PointerButton,
11 PointerEvent,
12};
13use anyhow::{anyhow, Result};
14use fission_diagnostics::prelude as diag;
15use fission_ir::{CoreIR, FlexDirection, LayoutOp, NodeId, Op, WidgetNodeId};
16use fission_layout::{LayoutPoint, LayoutRect, LayoutSnapshot, LayoutUnit, TextMeasurer};
17use glam::{Mat4, Vec4};
18use serde_json;
19use std::any::{Any, TypeId};
20use std::collections::{HashMap, HashSet};
21use std::sync::Arc;
22
23pub struct Runtime {
56 pub reducers: HashMap<ActionId, Vec<BoxedReducer>>,
59 pub persistent_reducers: HashMap<ActionId, Vec<BoxedReducer>>,
62 pub app_states: HashMap<TypeId, Box<dyn AppState>>,
64 pub runtime_state: RuntimeState,
66 pub measurer: Option<Arc<dyn TextMeasurer>>,
68 pub clipboard_backend: Option<Arc<dyn Clipboard>>,
70 pub ime_handler: Option<Arc<dyn ImeHandler>>,
72 pub pending_effects: Vec<EffectEnvelope>,
74 pub next_req_id: u64,
76}
77
78impl Default for Runtime {
79 fn default() -> Self {
80 let mut runtime = Self {
81 reducers: HashMap::new(),
82 persistent_reducers: HashMap::new(),
83 app_states: HashMap::new(),
84 runtime_state: RuntimeState::default(),
85 measurer: None,
86 clipboard_backend: None,
87 ime_handler: None,
88 pending_effects: Vec::new(),
89 next_req_id: 0,
90 };
91
92 runtime
93 .add_app_state(Box::new(Clock::default()))
94 .expect("Failed to add Clock state");
95
96 runtime.register_base_reducers();
97
98 runtime
99 }
100}
101
102impl Runtime {
103 pub fn with_measurer(mut self, measurer: Arc<dyn TextMeasurer>) -> Self {
104 self.measurer = Some(measurer);
105 self
106 }
107
108 pub fn with_clipboard(mut self, backend: Arc<dyn Clipboard>) -> Self {
109 self.clipboard_backend = Some(backend);
110 self
111 }
112
113 pub fn with_ime_handler(mut self, handler: Arc<dyn ImeHandler>) -> Self {
114 self.ime_handler = Some(handler);
115 self
116 }
117
118 pub fn caret_from_point_in_text(
119 &self,
120 value: &str,
121 font_size: f32,
122 viewport_x: f32,
123 viewport_w: f32,
124 content_w: f32,
125 scroll_offset: f32,
126 point_x: f32,
127 ) -> usize {
128 crate::input::text::caret_from_point_in_text(
129 self.measurer.as_ref(),
130 value,
131 font_size,
132 viewport_x,
133 viewport_w,
134 content_w,
135 scroll_offset,
136 point_x,
137 )
138 }
139
140 pub fn register_reducer<S: AppState + 'static>(
142 &mut self,
143 action_id: ActionId,
144 reducer_fn: fn(&mut S, &ActionEnvelope, NodeId) -> Result<()>,
145 ) -> Result<()> {
146 let state_type_id = TypeId::of::<S>();
147
148 let boxed_reducer: BoxedReducer = Box::new(
150 move |app_states: &mut HashMap<TypeId, Box<dyn AppState>>,
151 action: &ActionEnvelope,
152 target: NodeId,
153 _effects: &mut Vec<EffectEnvelope>,
154 _input: &ActionInput|
155 -> Result<()> {
156 if let Some(state_box) = app_states.get_mut(&state_type_id) {
157 let concrete_state = state_box.downcast_mut::<S>().ok_or_else(|| {
158 anyhow!("Failed to downcast AppState to concrete type for reducer")
159 })?;
160 reducer_fn(concrete_state, action, target)
161 } else {
162 anyhow::bail!("Target AppState for reducer not found in runtime.");
163 }
164 },
165 );
166
167 self.reducers
168 .entry(action_id)
169 .or_default()
170 .push(boxed_reducer);
171 Ok(())
172 }
173
174 pub fn register_base_reducers(&mut self) {
175 use crate::{AdvanceTo, Tick, ADVANCE_TO_ACTION_ID, TICK_ACTION_ID};
176
177 self.register_reducer::<Clock>(
178 *TICK_ACTION_ID,
179 |state: &mut Clock, action: &ActionEnvelope, _target| {
180 let tick_action: Tick = serde_json::from_slice(&action.payload)
181 .map_err(|e| anyhow!("Failed to deserialize Tick: {}", e))?;
182 state.advance_by(tick_action.dt)
183 },
184 )
185 .expect("Failed to register Tick reducer");
186
187 self.register_reducer::<Clock>(
188 *ADVANCE_TO_ACTION_ID,
189 |state: &mut Clock, action: &ActionEnvelope, _target| {
190 let advance_action: AdvanceTo = serde_json::from_slice(&action.payload)
191 .map_err(|e| anyhow!("Failed to deserialize AdvanceTo: {}", e))?;
192 state.set_to(advance_action.time)
193 },
194 )
195 .expect("Failed to register AdvanceTo reducer");
196 }
197
198 pub fn clear_reducers(&mut self) {
199 self.reducers.clear();
200 self.register_base_reducers();
201 }
202
203 pub fn absorb_registry<S: AppState>(&mut self, registry: ActionRegistry<S>) {
204 let new_reducers = registry.into_runtime_reducers();
205 for (id, mut list) in new_reducers {
206 self.reducers.entry(id).or_default().append(&mut list);
207 }
208 }
209
210 pub fn absorb_persistent_registry<S: AppState>(&mut self, registry: ActionRegistry<S>) {
216 let new_reducers = registry.into_runtime_reducers();
217 for (id, mut list) in new_reducers {
218 self.persistent_reducers
219 .entry(id)
220 .or_default()
221 .append(&mut list);
222 }
223 }
224
225 pub fn clock(&self) -> &Clock {
226 self.get_app_state::<Clock>()
227 .expect("Clock state must always be present")
228 }
229
230 pub fn get_app_state<S: AppState + 'static>(&self) -> Option<&S> {
231 self.app_states
232 .get(&TypeId::of::<S>())
233 .and_then(|s_box| s_box.downcast_ref::<S>())
234 }
235
236 pub fn get_app_state_mut<S: AppState + 'static>(&mut self) -> Option<&mut S> {
237 self.app_states
238 .get_mut(&TypeId::of::<S>())
239 .and_then(|s_box| s_box.downcast_mut::<S>())
240 }
241
242 pub fn add_app_state<S: AppState + 'static>(&mut self, state: Box<S>) -> Result<()> {
243 let type_id = TypeId::of::<S>();
244 if self.app_states.insert(type_id, state).is_some() {
245 anyhow::bail!("App state of this type already registered.");
246 }
247 Ok(())
248 }
249
250 pub fn dispatch(&mut self, action: ActionEnvelope, target: NodeId) -> Result<()> {
251 self.dispatch_with_input(action, target, &ActionInput::None)
252 }
253
254 pub fn dispatch_with_input(
255 &mut self,
256 action: ActionEnvelope,
257 target: NodeId,
258 input: &ActionInput,
259 ) -> Result<()> {
260 diag::emit(
261 diag::DiagCategory::Input,
262 diag::DiagLevel::Debug,
263 diag::DiagEventKind::InputEvent {
264 kind: "dispatch_start".into(),
265 target: Some(target.as_u128()),
266 position: None,
267 },
268 );
269
270 if crate::media::handle_video_action(&mut self.runtime_state.video, &action)? {
272 return Ok(());
273 }
274
275 let action_id = action.id;
276
277 let mut effects = Vec::new();
279
280 if let Some(reducers) = self.persistent_reducers.get_mut(&action_id) {
281 diag::emit(
282 diag::DiagCategory::Input,
283 diag::DiagLevel::Debug,
284 diag::DiagEventKind::InputEvent {
285 kind: format!("persistent_reducers:{}", reducers.len()),
286 target: Some(target.as_u128()),
287 position: None,
288 },
289 );
290
291 let mut temp_reducers: Vec<BoxedReducer> = reducers.drain(..).collect();
292 for reducer_wrapper in temp_reducers.iter_mut() {
293 reducer_wrapper(&mut self.app_states, &action, target, &mut effects, input)?;
294 }
295 reducers.extend(temp_reducers);
296 }
297
298 if let Some(reducers) = self.reducers.get_mut(&action_id) {
299 diag::emit(
300 diag::DiagCategory::Input,
301 diag::DiagLevel::Debug,
302 diag::DiagEventKind::InputEvent {
303 kind: format!("reducers:{}", reducers.len()),
304 target: Some(target.as_u128()),
305 position: None,
306 },
307 );
308
309 let mut temp_reducers: Vec<BoxedReducer> = reducers.drain(..).collect();
310 for reducer_wrapper in temp_reducers.iter_mut() {
311 reducer_wrapper(&mut self.app_states, &action, target, &mut effects, input)?;
312 }
313 reducers.extend(temp_reducers);
314 }
315
316 for mut envelope in effects {
318 envelope.req_id = self.next_req_id;
320 self.next_req_id += 1;
321 self.pending_effects.push(envelope);
322 }
323
324 diag::emit(
325 diag::DiagCategory::Input,
326 diag::DiagLevel::Debug,
327 diag::DiagEventKind::InputEvent {
328 kind: "dispatch_end".into(),
329 target: Some(target.as_u128()),
330 position: None,
331 },
332 );
333 Ok(())
334 }
335
336 pub fn tick(&mut self, dt: CurrentTime) -> Result<()> {
337 use crate::Tick;
338 let action = Tick { dt };
339 let envelope: ActionEnvelope = action.into();
340 self.dispatch(envelope, NodeId::derived(0, &[0]))?;
341
342 let current_time = self.clock().current_time();
343
344 let mut finished = Vec::new();
345 let mut has_animation_changes = false;
346 for ((target, property), anim) in self.runtime_state.animation.active.iter_mut() {
347 let elapsed = current_time.saturating_sub(anim.start_time);
348 let mut progress = if anim.duration == 0 {
349 1.0
350 } else {
351 (elapsed as f32 / anim.duration as f32)
352 };
353
354 if anim.repeat && progress >= 1.0 {
355 progress = progress % 1.0;
356 } else {
357 progress = progress.clamp(0.0, 1.0);
358 }
359
360 if !anim.repeat && (elapsed >= anim.duration || anim.duration == 0) {
361 finished.push((*target, property.clone()));
362 }
363
364 let value = anim.start_value + (anim.end_value - anim.start_value) * progress;
365
366 let current_val = self.runtime_state.animation.values.get(&(*target, property.clone())).copied();
368 if current_val != Some(value) {
369 self.runtime_state
370 .animation
371 .values
372 .insert((*target, property.clone()), value);
373 has_animation_changes = true;
374 }
375 }
376
377 for key in finished {
378 self.runtime_state.animation.active.remove(&key);
379 has_animation_changes = true;
380 }
381
382 let _ = has_animation_changes;
383
384 Ok(())
385 }
386
387 pub fn enqueue_animation(&mut self, target: WidgetNodeId, request: AnimationRequest) {
388 let key = (target, request.property.clone());
389
390 if let Some(active) = self.runtime_state.animation.active.get(&key) {
392 if (active.end_value - request.to).abs() < 0.001
394 && active.duration == request.duration_ms
395 && active.repeat == request.repeat
396 {
397 return;
399 }
400 }
401
402 let current_value = self
403 .runtime_state
404 .animation
405 .values
406 .get(&key)
407 .copied()
408 .unwrap_or_else(|| request.property.default_value());
409
410 let start_value = match request.from {
418 AnimationStartValue::Explicit(v) => v,
419 AnimationStartValue::Current => current_value,
420 };
421
422 let anim = ActiveAnimation {
423 target,
424 property: request.property.clone(),
425 start_value,
426 end_value: request.to,
427 start_time: self.clock().current_time() + request.delay_ms,
428 duration: request.duration_ms,
429 repeat: request.repeat,
430 };
431
432 self.runtime_state
433 .animation
434 .values
435 .insert(key.clone(), start_value);
436 self.runtime_state.animation.active.insert(key, anim);
437 }
438
439 pub fn sync_video_nodes(&mut self, registrations: &[VideoRegistration]) {
440 let mut seen: HashSet<WidgetNodeId> = HashSet::new();
441
442 for reg in registrations {
443 seen.insert(reg.node_id);
444 let entry = self
445 .runtime_state
446 .video
447 .states
448 .entry(reg.node_id)
449 .or_insert_with(crate::env::VideoState::default);
450 entry.asset_source = reg.source.clone();
451 entry.looped = reg.loop_playback;
452 if reg.autoplay && entry.status == VideoStatus::Stopped {
453 entry.status = VideoStatus::Playing;
454 }
455 }
456
457 self.runtime_state
458 .video
459 .states
460 .retain(|node_id, _| seen.contains(node_id));
461 }
462
463 pub fn sync_web_nodes(&mut self, registrations: &[crate::registry::WebRegistration]) {
464 let mut seen: HashSet<WidgetNodeId> = HashSet::new();
465
466 for reg in registrations {
467 seen.insert(reg.node_id);
468 let entry = self
469 .runtime_state
470 .web
471 .states
472 .entry(reg.node_id)
473 .or_insert_with(crate::env::WebState::default);
474
475 if entry.url != reg.url {
477 entry.url = reg.url.clone();
478 entry.loading = true; }
480 entry.user_agent = reg.user_agent.clone();
481 }
482
483 self.runtime_state
484 .web
485 .states
486 .retain(|node_id, _| seen.contains(node_id));
487 }
488
489 pub fn post_layout_hook(&mut self, ir: &CoreIR, layout: &LayoutSnapshot) {
490 let mut current_heroes = HashMap::new();
491
492 for (id, node) in &ir.nodes {
493 if let Op::Semantics(s) = &node.op {
494 if let Some(tag) = &s.hero_tag {
495 if let Some(geom) = layout.get_node_geometry(*id) {
496 current_heroes.insert(tag.clone(), (*id, geom.rect));
497 }
498 }
499 }
500 }
501
502 for (tag, (_new_id, new_rect)) in ¤t_heroes {
504 if let Some((_old_id, old_rect)) = self.runtime_state.hero.positions.get(tag) {
505 if *new_rect != *old_rect {
506 diag::emit(
508 diag::DiagCategory::Layout,
509 diag::DiagLevel::Debug,
510 diag::DiagEventKind::AnchorPlacement {
511 widget: 0,
512 node: 0,
513 rect_x: old_rect.origin.x,
514 rect_y: old_rect.origin.y,
515 rect_w: old_rect.size.width,
516 rect_h: old_rect.size.height,
517 place_left: new_rect.origin.x,
518 place_top: new_rect.origin.y,
519 note: Some(format!("Hero flight: {}", tag)),
520 },
521 );
522 }
523 }
524 }
525
526 self.runtime_state.hero.positions = current_heroes;
527 }
528
529 pub fn handle_input(
530 &mut self,
531 event: InputEvent,
532 ir: &CoreIR,
533 layout: &LayoutSnapshot,
534 ) -> Result<()> {
535 use crate::hit_test::{
536 find_neighbor_focus_node, find_next_focus_node, hit_test_with_scroll, FocusDirection,
537 };
538 use crate::input::gesture::GestureController;
539 use crate::input::slider::SliderController;
540 use crate::input::text::TextInputController;
541 use crate::input::{ControllerContext, InputController};
542
543 let mut dispatched_actions = Vec::new();
544 let mut handled = false;
545
546 {
547 let mut ctx = ControllerContext {
548 ir,
549 layout,
550 text_edit: &mut self.runtime_state.text_edit,
551 interaction: &mut self.runtime_state.interaction,
552 scroll: &mut self.runtime_state.scroll,
553 ime_preedit: &mut self.runtime_state.ime_preedit,
554 gesture: &mut self.runtime_state.gesture,
555 clipboard: self.clipboard_backend.as_ref(),
556 measurer: self.measurer.as_ref(),
557 dispatched_actions: Vec::new(),
558 };
559
560 let mut gesture_controller = GestureController;
561 if gesture_controller.handle_event(&mut ctx, &event) {
562 handled = true;
563 } else {
564 let mut text_controller = TextInputController;
565 if text_controller.handle_event(&mut ctx, &event) {
566 handled = true;
567 } else {
568 let mut slider_controller = SliderController;
569 if slider_controller.handle_event(&mut ctx, &event) {
570 handled = true;
571 }
572 }
573 }
574 dispatched_actions = ctx.dispatched_actions;
575 }
576
577 for (target, action, input) in dispatched_actions {
578 self.dispatch_with_input(action, target, &input)?;
579 }
580
581 if handled {
582 if matches!(event, InputEvent::Pointer(PointerEvent::Up { .. })) {
583 self.runtime_state.interaction.pressed.clear();
584 self.runtime_state.interaction.last_down_point = None;
585 }
586 return Ok(());
587 }
588
589 match event {
590 InputEvent::Pointer(PointerEvent::Scroll { point, delta }) => {
591 let trace_scroll =
592 std::env::var("FISSION_SCROLL_TRACE").ok().as_deref() == Some("1");
593 if trace_scroll {
594 eprintln!(
595 "[scroll-trace] event point=({:.1},{:.1}) delta=({:.1},{:.1})",
596 point.x, point.y, delta.x, delta.y
597 );
598 }
599 if let Some(hit_node_id) =
600 hit_test_with_scroll(ir, layout, &self.runtime_state.scroll, point)
601 {
602 if trace_scroll {
603 eprintln!("[scroll-trace] hit_node={}", hit_node_id.as_u128());
604 }
605 let mut current_id = Some(hit_node_id);
606 while let Some(node_id) = current_id {
607 if let Some(node) = ir.nodes.get(&node_id) {
608 if let Op::Layout(LayoutOp::Scroll { direction, .. }) = &node.op {
609 let current_offset = self.runtime_state.scroll.get_offset(node_id);
610 let delta_val = match direction {
611 FlexDirection::Row => delta.x,
612 FlexDirection::Column => delta.y,
613 };
614 let mut new_offset = current_offset + delta_val;
615
616 let mut max_offset = 0.0f32;
617 let mut viewport_w = 0.0f32;
618 let mut viewport_h = 0.0f32;
619 let mut content_w = 0.0f32;
620 let mut content_h = 0.0f32;
621 if let Some(geom) = layout.get_node_geometry(node_id) {
622 viewport_w = geom.rect.width();
623 viewport_h = geom.rect.height();
624 content_w = geom.content_size.width;
625 content_h = geom.content_size.height;
626 max_offset = if matches!(direction, FlexDirection::Row) {
627 (geom.content_size.width - geom.rect.width()).max(0.0)
628 } else {
629 (geom.content_size.height - geom.rect.height()).max(0.0)
630 };
631 new_offset = new_offset.clamp(0.0, max_offset);
632 }
633
634 if trace_scroll {
635 eprintln!(
636 "[scroll-trace] scroll_node={} axis={} offset={:.1}->{:.1} max={:.1} viewport=({:.1},{:.1}) content=({:.1},{:.1})",
637 node_id.as_u128(),
638 match direction { FlexDirection::Row => "x", FlexDirection::Column => "y" },
639 current_offset,
640 new_offset,
641 max_offset,
642 viewport_w,
643 viewport_h,
644 content_w,
645 content_h
646 );
647 }
648
649 {
650 use fission_diagnostics::prelude as diag;
651 diag::emit(
652 diag::DiagCategory::Input,
653 diag::DiagLevel::Debug,
654 diag::DiagEventKind::ScrollUpdate {
655 node: node_id.as_u128(),
656 axis: match direction {
657 FlexDirection::Row => "x".into(),
658 FlexDirection::Column => "y".into(),
659 },
660 point_x: point.x,
661 point_y: point.y,
662 delta: delta_val,
663 old_offset: current_offset,
664 new_offset,
665 max_offset,
666 viewport_w,
667 viewport_h,
668 content_w,
669 content_h,
670 },
671 );
672 }
673
674 self.runtime_state.scroll.set_offset(node_id, new_offset);
675 break;
676 }
677 current_id = node.parent;
678 } else {
679 break;
680 }
681 }
682 } else if trace_scroll {
683 eprintln!("[scroll-trace] hit_test: no node");
684 }
685 }
686 InputEvent::Keyboard(KeyEvent::Down {
687 key_code,
688 modifiers,
689 }) => match key_code {
690 KeyCode::Tab => {
691 let reverse = (modifiers & 1) != 0;
692 let old_focus = self.runtime_state.interaction.focused;
693 let next =
694 find_next_focus_node(ir, self.runtime_state.interaction.focused, reverse);
695 if next != old_focus {
696 self.runtime_state.ime_preedit = None;
697 self.clear_text_pending_on_blur(old_focus, next);
698 }
699 self.runtime_state.interaction.set_focused(next);
700 }
701 KeyCode::Up | KeyCode::Down | KeyCode::Left | KeyCode::Right => {
702 if let Some(focused) = self.runtime_state.interaction.focused {
703 let dir = match key_code {
704 KeyCode::Up => FocusDirection::Up,
705 KeyCode::Down => FocusDirection::Down,
706 KeyCode::Left => FocusDirection::Left,
707 KeyCode::Right => FocusDirection::Right,
708 _ => unreachable!(),
709 };
710 if let Some(next) = find_neighbor_focus_node(ir, layout, focused, dir) {
711 self.runtime_state.ime_preedit = None;
712 self.clear_text_pending_on_blur(Some(focused), Some(next));
713 self.runtime_state.interaction.set_focused(Some(next));
714 }
715 }
716 }
717 KeyCode::Enter | KeyCode::Space => {
718 if let Some(focused_id) = self.runtime_state.interaction.focused {
719 let mut current_id = Some(focused_id);
720 while let Some(node_id) = current_id {
721 if let Some(node) = ir.nodes.get(&node_id) {
722 if let Op::Semantics(semantics) = &node.op {
723 if let Some(action_entry) = semantics.actions.entries.first() {
724 if let Some(payload) = &action_entry.payload_data {
725 let envelope = ActionEnvelope {
726 id: ActionId::from_u128(action_entry.action_id),
727 payload: payload.clone(),
728 };
729 return self.dispatch(envelope, node_id);
730 }
731 }
732 }
733 current_id = node.parent;
734 } else {
735 break;
736 }
737 }
738 }
739 }
740 _ => {}
741 },
742 InputEvent::Pointer(PointerEvent::Down { point, .. }) => {
743 if let Some(hit_node_id) =
744 hit_test_with_scroll(ir, layout, &self.runtime_state.scroll, point)
745 {
746 diag::emit(
747 diag::DiagCategory::Input,
748 diag::DiagLevel::Debug,
749 diag::DiagEventKind::InputEvent {
750 kind: "pointer_down_hit".into(),
751 target: Some(hit_node_id.as_u128()),
752 position: Some((point.x, point.y)),
753 },
754 );
755 let mut focus_candidate = Some(hit_node_id);
756 while let Some(node_id) = focus_candidate {
757 if let Some(node) = ir.nodes.get(&node_id) {
758 if let Op::Semantics(s) = &node.op {
759 if s.focusable {
760 let old_focused_id = self.runtime_state.interaction.focused;
761 if Some(node_id) != old_focused_id {
762 self.runtime_state.ime_preedit = None;
763 self.clear_text_pending_on_blur(
764 old_focused_id,
765 Some(node_id),
766 );
767
768 if s.role == fission_ir::semantics::Role::TextInput {
769 if let Some(ime_handler) = &self.ime_handler {
770 ime_handler.set_ime_allowed(true);
771 }
772 } else if let Some(ime_handler) = &self.ime_handler {
773 ime_handler.set_ime_allowed(false);
774 }
775 }
776 self.runtime_state.interaction.set_focused(Some(node_id));
777 break;
778 }
779 }
780 focus_candidate = node.parent;
781 } else {
782 break;
783 }
784 }
785 if focus_candidate.is_none() {
786 let old_focused_id = self.runtime_state.interaction.focused;
787 if let Some(old_focused_id) = self.runtime_state.interaction.focused {
788 if let Some(old_node) = ir.nodes.get(&old_focused_id) {
789 if let Op::Semantics(s) = &old_node.op {
790 if s.role == fission_ir::semantics::Role::TextInput {
791 if let Some(ime_handler) = &self.ime_handler {
792 ime_handler.set_ime_allowed(false);
793 }
794 }
795 }
796 }
797 }
798 self.clear_text_pending_on_blur(old_focused_id, None);
799 self.runtime_state.interaction.set_focused(None);
800 }
801
802 let mut current_pressed_id = Some(hit_node_id);
803 while let Some(node_id) = current_pressed_id {
804 self.runtime_state.interaction.set_pressed(node_id, true);
805 if let Some(node) = ir.nodes.get(&node_id) {
806 current_pressed_id = node.parent;
807 } else {
808 break;
809 }
810 }
811 self.runtime_state.interaction.last_down_point = Some(point);
812
813 if let Some(focused_id) = self.runtime_state.interaction.focused {
814 if let Some(node) = ir.nodes.get(&focused_id) {
815 if let Op::Semantics(s) = &node.op {
816 if s.role == fission_ir::semantics::Role::TextInput {
817 if let Some(ime_handler) = &self.ime_handler {
818 ime_handler.set_ime_cursor_area(LayoutRect::new(
819 point.x, point.y, 2.0, 16.0,
820 ));
821 }
822 }
823 }
824 }
825 }
826 } else {
827 let old_focused_id = self.runtime_state.interaction.focused;
828 if let Some(old_focused_id) = self.runtime_state.interaction.focused {
829 if let Some(old_node) = ir.nodes.get(&old_focused_id) {
830 if let Op::Semantics(s) = &old_node.op {
831 if s.role == fission_ir::semantics::Role::TextInput {
832 if let Some(ime_handler) = &self.ime_handler {
833 ime_handler.set_ime_allowed(false);
834 }
835 }
836 }
837 }
838 }
839 self.clear_text_pending_on_blur(old_focused_id, None);
840 self.runtime_state.interaction.set_focused(None);
841 }
842 }
843 InputEvent::Pointer(PointerEvent::Up { point, .. }) => {
844 self.runtime_state.interaction.pressed.clear();
845 self.runtime_state.interaction.last_down_point = None;
846 if let Some(hit_node_id) =
847 hit_test_with_scroll(ir, layout, &self.runtime_state.scroll, point)
848 {
849 let mut current_id = Some(hit_node_id);
850 while let Some(node_id) = current_id {
851 if let Some(node) = ir.nodes.get(&node_id) {
852 if let Op::Semantics(semantics) = &node.op {
853 if semantics.role == fission_ir::semantics::Role::TextInput {
854 } else if let Some(action_entry) = semantics.actions.entries.first()
856 {
857 if let Some(payload) = &action_entry.payload_data {
858 let envelope = ActionEnvelope {
859 id: ActionId::from_u128(action_entry.action_id),
860 payload: payload.clone(),
861 };
862 diag::emit(
863 diag::DiagCategory::Input,
864 diag::DiagLevel::Debug,
865 diag::DiagEventKind::InputEvent {
866 kind: "pointer_up_dispatch".into(),
867 target: Some(node_id.as_u128()),
868 position: Some((point.x, point.y)),
869 },
870 );
871 return self.dispatch(envelope, node_id);
872 }
873 }
874 }
875 current_id = node.parent;
876 } else {
877 break;
878 }
879 }
880 }
881 }
882 _ => {}
883 }
884 Ok(())
885 }
886
887 fn clear_text_pending_on_blur(&mut self, old_focus: Option<NodeId>, new_focus: Option<NodeId>) {
888 if old_focus == new_focus {
889 return;
890 }
891 if let Some(old_id) = old_focus {
892 if let Some(st) = self.runtime_state.text_edit.states.get_mut(&old_id) {
893 st.pending_model_sync = false;
894 }
895 }
896 }
897
898 pub fn hit_test(
899 &self,
900 point: LayoutPoint,
901 ir: &CoreIR,
902 snapshot: &LayoutSnapshot,
903 ) -> Option<NodeId> {
904 if let Some(root) = ir.root {
905 return self.hit_test_recursive(root, point, ir, snapshot);
906 }
907 None
908 }
909
910 fn hit_test_recursive(
911 &self,
912 node_id: NodeId,
913 point: LayoutPoint,
914 ir: &CoreIR,
915 snapshot: &LayoutSnapshot,
916 ) -> Option<NodeId> {
917 if let Some(geom) = snapshot.nodes.get(&node_id) {
918 if geom.rect.contains(point) {
919 if let Some(node) = ir.nodes.get(&node_id) {
920 for child in node.children.iter().rev() {
921 let mut child_point = point;
922
923 if let Op::Layout(LayoutOp::Scroll { direction, .. }) = &node.op {
924 if !geom.rect.contains(point) {
925 continue;
926 }
927 let offset = self.runtime_state.scroll.get_offset(node_id);
928 match direction {
929 FlexDirection::Row => child_point.x += offset,
930 FlexDirection::Column => child_point.y += offset,
931 }
932 }
933
934 if let Op::Layout(LayoutOp::Transform { transform }) = &node.op {
935 let mat = Mat4::from_cols_array(transform);
936 let local_x = point.x - geom.rect.origin.x;
973 let local_y = point.y - geom.rect.origin.y;
974
975 let p = Vec4::new(local_x, local_y, 0.0, 1.0);
976 let inv = mat.inverse();
977 let transformed = inv * p;
978
979 child_point = LayoutPoint::new(
980 transformed.x + geom.rect.origin.x,
981 transformed.y + geom.rect.origin.y,
982 );
983 }
984
985 if let Some(hit) =
986 self.hit_test_recursive(*child, child_point, ir, snapshot)
987 {
988 return Some(hit);
989 }
990 }
991
992 match &node.op {
993 Op::Paint(_)
994 | Op::Layout(LayoutOp::Scroll { .. })
995 | Op::Layout(LayoutOp::Embed { .. }) => return Some(node_id),
996 _ => return None,
997 }
998 }
999 return None;
1000 }
1001 }
1002 None
1003 }
1004}