1use crate::action::{ActionEnvelope, ActionId, GlobalState};
2use crate::async_runtime::ServiceStopPayload;
3use crate::effect::{ActionInput, EffectEnvelope};
4use crate::env::{ActiveAnimation, RuntimeState, VideoStatus};
5use crate::registry::{
6 ActionRegistry, AnimationPropertyId, AnimationRequest, AnimationStartValue, ResourcePolicy,
7 RuntimeResourceDeclaration, RuntimeResourceKind, TimerResource, VideoRegistration,
8};
9use crate::BoxedReducer;
10use crate::{
11 Clipboard, Clock, CurrentTime, ImeHandler, InputEvent, KeyCode, KeyEvent, PointerButton,
12 PointerEvent, ResourceExecutionContext,
13};
14use anyhow::{anyhow, Result};
15use fission_diagnostics::prelude as diag;
16use fission_ir::{CoreIR, FlexDirection, LayoutOp, Op, WidgetId};
17use fission_layout::{LayoutPoint, LayoutRect, LayoutSize, LayoutSnapshot, TextMeasurer};
18use glam::{Mat4, Vec4};
19use serde_json;
20use std::any::TypeId;
21use std::collections::{HashMap, HashSet};
22use std::sync::Arc;
23
24#[derive(Debug, Default, Clone)]
25pub struct TickResult {
26 pub changed_animations: Vec<(WidgetId, AnimationPropertyId)>,
27}
28
29#[derive(Debug, Clone)]
30enum ActiveResourceKind {
31 Job,
32 Service {
33 service_name: String,
34 slot_key: String,
35 },
36 Timer {
37 interval_ms: u64,
38 payload: Vec<u8>,
39 on_tick: Option<ActionEnvelope>,
40 next_fire_at: CurrentTime,
41 },
42}
43
44#[derive(Debug, Clone)]
45struct ActiveResource {
46 generation: u64,
47 deps: Option<Vec<u8>>,
48 policy: ResourcePolicy,
49 kind: ActiveResourceKind,
50}
51
52pub struct Runtime {
85 pub(crate) reducers: HashMap<ActionId, Vec<BoxedReducer>>,
88 pub(crate) persistent_reducers: HashMap<ActionId, Vec<BoxedReducer>>,
91 pub app_states: HashMap<TypeId, Box<dyn GlobalState>>,
93 pub runtime_state: RuntimeState,
95 pub measurer: Option<Arc<dyn TextMeasurer>>,
97 pub clipboard_backend: Option<Arc<dyn Clipboard>>,
99 pub ime_handler: Option<Arc<dyn ImeHandler>>,
101 pub pending_effects: Vec<EffectEnvelope>,
103 pub next_req_id: u64,
105 active_resources: HashMap<String, ActiveResource>,
107 next_resource_generation: u64,
109}
110
111impl Default for Runtime {
112 fn default() -> Self {
113 let mut runtime = Self {
114 reducers: HashMap::new(),
115 persistent_reducers: HashMap::new(),
116 app_states: HashMap::new(),
117 runtime_state: RuntimeState::default(),
118 measurer: None,
119 clipboard_backend: None,
120 ime_handler: None,
121 pending_effects: Vec::new(),
122 next_req_id: 0,
123 active_resources: HashMap::new(),
124 next_resource_generation: 1,
125 };
126
127 runtime
128 .add_global_state(Box::new(runtime.runtime_state.local_widget_state.clone()))
129 .expect("Failed to add local widget state store");
130
131 runtime
132 .add_global_state(Box::new(Clock::default()))
133 .expect("Failed to add Clock state");
134
135 runtime.register_base_reducers();
136
137 runtime
138 }
139}
140
141impl Runtime {
142 pub fn with_measurer(mut self, measurer: Arc<dyn TextMeasurer>) -> Self {
143 self.measurer = Some(measurer);
144 self
145 }
146
147 pub fn with_clipboard(mut self, backend: Arc<dyn Clipboard>) -> Self {
148 self.clipboard_backend = Some(backend);
149 self
150 }
151
152 pub fn with_ime_handler(mut self, handler: Arc<dyn ImeHandler>) -> Self {
153 self.ime_handler = Some(handler);
154 self
155 }
156
157 pub fn caret_from_point_in_text(
158 &self,
159 value: &str,
160 font_size: f32,
161 viewport_x: f32,
162 viewport_w: f32,
163 content_w: f32,
164 scroll_offset: f32,
165 point_x: f32,
166 ) -> usize {
167 crate::input::text::caret_from_point_in_text(
168 self.measurer.as_ref(),
169 value,
170 font_size,
171 viewport_x,
172 viewport_w,
173 content_w,
174 scroll_offset,
175 point_x,
176 )
177 }
178
179 pub fn register_reducer<S: GlobalState + 'static>(
181 &mut self,
182 action_id: ActionId,
183 reducer_fn: crate::action::Reducer<S>,
184 ) -> Result<()> {
185 let state_type_id = TypeId::of::<S>();
186
187 let boxed_reducer: BoxedReducer = Box::new(
189 move |app_states: &mut HashMap<TypeId, Box<dyn GlobalState>>,
190 action: &ActionEnvelope,
191 target: WidgetId,
192 _effects: &mut Vec<EffectEnvelope>,
193 _input: &ActionInput|
194 -> Result<()> {
195 if let Some(state_box) = app_states.get_mut(&state_type_id) {
196 let concrete_state = state_box.downcast_mut::<S>().ok_or_else(|| {
197 anyhow!("Failed to downcast GlobalState to concrete type for reducer")
198 })?;
199 reducer_fn(concrete_state, action, target.into())
200 } else {
201 anyhow::bail!("Target GlobalState for reducer not found in runtime.");
202 }
203 },
204 );
205
206 self.reducers
207 .entry(action_id)
208 .or_default()
209 .push(boxed_reducer);
210 Ok(())
211 }
212
213 pub fn register_base_reducers(&mut self) {
214 use crate::{AdvanceTo, Tick, ADVANCE_TO_ACTION_ID, TICK_ACTION_ID};
215
216 self.register_reducer::<Clock>(
217 *TICK_ACTION_ID,
218 |state: &mut Clock, action: &ActionEnvelope, _target| {
219 let tick_action: Tick = serde_json::from_slice(&action.payload)
220 .map_err(|e| anyhow!("Failed to deserialize Tick: {}", e))?;
221 state.advance_by(tick_action.dt)
222 },
223 )
224 .expect("Failed to register Tick reducer");
225
226 self.register_reducer::<Clock>(
227 *ADVANCE_TO_ACTION_ID,
228 |state: &mut Clock, action: &ActionEnvelope, _target| {
229 let advance_action: AdvanceTo = serde_json::from_slice(&action.payload)
230 .map_err(|e| anyhow!("Failed to deserialize AdvanceTo: {}", e))?;
231 state.set_to(advance_action.time)
232 },
233 )
234 .expect("Failed to register AdvanceTo reducer");
235 }
236
237 pub fn clear_reducers(&mut self) {
238 self.reducers.clear();
239 self.register_base_reducers();
240 }
241
242 pub fn absorb_registry<S: GlobalState>(&mut self, registry: ActionRegistry<S>) {
243 let new_reducers = registry.into_runtime_reducers();
244 for (id, mut list) in new_reducers {
245 self.reducers.entry(id).or_default().append(&mut list);
246 }
247 }
248
249 pub fn absorb_persistent_registry<S: GlobalState>(&mut self, registry: ActionRegistry<S>) {
255 let new_reducers = registry.into_runtime_reducers();
256 for (id, mut list) in new_reducers {
257 self.persistent_reducers
258 .entry(id)
259 .or_default()
260 .append(&mut list);
261 }
262 }
263
264 pub fn clock(&self) -> &Clock {
265 self.get_global_state::<Clock>()
266 .expect("Clock state must always be present")
267 }
268
269 pub fn get_global_state<S: GlobalState + 'static>(&self) -> Option<&S> {
270 self.app_states
271 .get(&TypeId::of::<S>())
272 .and_then(|s_box| s_box.downcast_ref::<S>())
273 }
274
275 pub fn get_global_state_mut<S: GlobalState + 'static>(&mut self) -> Option<&mut S> {
276 self.app_states
277 .get_mut(&TypeId::of::<S>())
278 .and_then(|s_box| s_box.downcast_mut::<S>())
279 }
280
281 pub fn add_global_state<S: GlobalState + 'static>(&mut self, state: Box<S>) -> Result<()> {
282 let type_id = TypeId::of::<S>();
283 if self.app_states.insert(type_id, state).is_some() {
284 anyhow::bail!("Global state of this type already registered.");
285 }
286 Ok(())
287 }
288
289 pub fn with_global_state<S: GlobalState + 'static>(mut self, state: S) -> Self {
290 self.app_states.insert(TypeId::of::<S>(), Box::new(state));
291 self
292 }
293
294 #[doc(hidden)]
295 pub fn get_app_state<S: GlobalState + 'static>(&self) -> Option<&S> {
296 self.get_global_state::<S>()
297 }
298
299 #[doc(hidden)]
300 pub fn get_app_state_mut<S: GlobalState + 'static>(&mut self) -> Option<&mut S> {
301 self.get_global_state_mut::<S>()
302 }
303
304 #[doc(hidden)]
305 pub fn add_app_state<S: GlobalState + 'static>(&mut self, state: Box<S>) -> Result<()> {
306 self.add_global_state(state)
307 }
308
309 pub fn dispatch(&mut self, action: ActionEnvelope, target: WidgetId) -> Result<()> {
310 self.dispatch_with_input(action, target, &ActionInput::None)
311 }
312
313 fn enqueue_effect(&mut self, mut envelope: EffectEnvelope) {
314 envelope.req_id = self.next_req_id;
315 self.next_req_id += 1;
316 self.pending_effects.push(envelope);
317 }
318
319 pub fn dispatch_with_input(
320 &mut self,
321 action: ActionEnvelope,
322 target: WidgetId,
323 input: &ActionInput,
324 ) -> Result<()> {
325 self.dispatch_node_with_input(action, target.into(), input)
326 }
327
328 fn dispatch_node(&mut self, action: ActionEnvelope, target: WidgetId) -> Result<()> {
329 self.dispatch_node_with_input(action, target, &ActionInput::None)
330 }
331
332 fn dispatch_node_with_input(
333 &mut self,
334 action: ActionEnvelope,
335 target: WidgetId,
336 input: &ActionInput,
337 ) -> Result<()> {
338 diag::emit(
339 diag::DiagCategory::Input,
340 diag::DiagLevel::Debug,
341 diag::DiagEventKind::InputEvent {
342 kind: "dispatch_start".into(),
343 target: Some(target.as_u128()),
344 position: None,
345 },
346 );
347
348 if crate::media::handle_video_action(&mut self.runtime_state.video, &action)? {
350 return Ok(());
351 }
352
353 let action_id = action.id;
354
355 let mut effects = Vec::new();
357
358 if let Some(reducers) = self.persistent_reducers.get_mut(&action_id) {
359 diag::emit(
360 diag::DiagCategory::Input,
361 diag::DiagLevel::Debug,
362 diag::DiagEventKind::InputEvent {
363 kind: format!("persistent_reducers:{}", reducers.len()),
364 target: Some(target.as_u128()),
365 position: None,
366 },
367 );
368
369 let mut temp_reducers: Vec<BoxedReducer> = reducers.drain(..).collect();
370 for reducer_wrapper in temp_reducers.iter_mut() {
371 reducer_wrapper(&mut self.app_states, &action, target, &mut effects, input)?;
372 }
373 reducers.extend(temp_reducers);
374 }
375
376 if let Some(reducers) = self.reducers.get_mut(&action_id) {
377 diag::emit(
378 diag::DiagCategory::Input,
379 diag::DiagLevel::Debug,
380 diag::DiagEventKind::InputEvent {
381 kind: format!("reducers:{}", reducers.len()),
382 target: Some(target.as_u128()),
383 position: None,
384 },
385 );
386
387 let mut temp_reducers: Vec<BoxedReducer> = reducers.drain(..).collect();
388 for reducer_wrapper in temp_reducers.iter_mut() {
389 reducer_wrapper(&mut self.app_states, &action, target, &mut effects, input)?;
390 }
391 reducers.extend(temp_reducers);
392 }
393
394 for envelope in effects {
395 self.enqueue_effect(envelope);
396 }
397
398 diag::emit(
399 diag::DiagCategory::Input,
400 diag::DiagLevel::Debug,
401 diag::DiagEventKind::InputEvent {
402 kind: "dispatch_end".into(),
403 target: Some(target.as_u128()),
404 position: None,
405 },
406 );
407 Ok(())
408 }
409
410 pub fn tick(&mut self, dt: CurrentTime) -> Result<TickResult> {
411 use crate::Tick;
412 let action = Tick { dt };
413 let envelope: ActionEnvelope = action.into();
414 self.dispatch_node(envelope, WidgetId::derived(0, &[0]))?;
415
416 self.tick_resource_timers()?;
417
418 let current_time = self.clock().current_time();
419
420 let mut finished = Vec::new();
421 let mut result = TickResult::default();
422 for ((target, property), anim) in self.runtime_state.animation.active.iter_mut() {
423 let elapsed = current_time.saturating_sub(anim.start_time);
424 let mut progress = if anim.duration == 0 {
425 1.0
426 } else {
427 elapsed as f32 / anim.duration as f32
428 };
429
430 if anim.repeat && progress >= 1.0 {
431 progress = progress % 1.0;
432 } else {
433 progress = progress.clamp(0.0, 1.0);
434 }
435
436 if !anim.repeat && (elapsed >= anim.duration || anim.duration == 0) {
437 finished.push((*target, property.clone()));
438 }
439
440 let eased_progress = anim.easing.apply(progress);
441 let value = anim.start_value + (anim.end_value - anim.start_value) * eased_progress;
442 let current_val = self
444 .runtime_state
445 .animation
446 .values
447 .get(&(*target, property.clone()))
448 .copied();
449 if current_val != Some(value) {
450 self.runtime_state
451 .animation
452 .values
453 .insert((*target, property.clone()), value);
454 result.changed_animations.push((*target, property.clone()));
455 }
456 }
457
458 for key in finished {
459 self.runtime_state.animation.active.remove(&key);
460 }
461
462 Ok(result)
463 }
464
465 fn tick_resource_timers(&mut self) -> Result<()> {
466 let now = self.clock().current_time();
467 let mut ticks = Vec::new();
468
469 for resource in self.active_resources.values_mut() {
470 if let ActiveResourceKind::Timer {
471 interval_ms,
472 payload,
473 on_tick,
474 next_fire_at,
475 } = &mut resource.kind
476 {
477 let Some(action) = on_tick.clone() else {
478 continue;
479 };
480
481 let interval_ms = (*interval_ms).max(1);
482 while now >= *next_fire_at {
483 ticks.push((action.clone(), payload.clone()));
484 *next_fire_at = next_fire_at.saturating_add(interval_ms);
485 }
486 }
487 }
488
489 for (action, payload) in ticks {
490 self.dispatch_node_with_input(
491 action,
492 WidgetId::derived(0, &[0]),
493 &ActionInput::TimerTick { payload },
494 )?;
495 }
496
497 Ok(())
498 }
499
500 pub fn enqueue_animation(&mut self, target: WidgetId, request: AnimationRequest) {
501 let key = (target, request.property.clone());
502
503 if let Some(active) = self.runtime_state.animation.active.get(&key) {
505 if (active.end_value - request.to).abs() < 0.001
507 && active.duration == request.duration_ms
508 && active.repeat == request.repeat
509 && active.frame_interval_ms == request.frame_interval_ms
510 && active.easing == request.easing
511 {
512 return;
514 }
515 }
516
517 let current_value = self.runtime_state.animation.values.get(&key).copied();
518 let current_value = current_value.unwrap_or_else(|| request.property.default_value());
519
520 if !request.repeat
525 && self.runtime_state.animation.values.contains_key(&key)
526 && (current_value - request.to).abs() < 0.001
527 {
528 self.runtime_state.animation.values.insert(key, request.to);
529 return;
530 }
531
532 let start_value = match request.from {
533 AnimationStartValue::Explicit(v) => v,
534 AnimationStartValue::Current => current_value,
535 };
536
537 let anim = ActiveAnimation {
538 target,
539 property: request.property.clone(),
540 start_value,
541 end_value: request.to,
542 start_time: self.clock().current_time() + request.delay_ms,
543 duration: request.duration_ms,
544 repeat: request.repeat,
545 frame_interval_ms: request.frame_interval_ms.filter(|ms| *ms > 0),
546 easing: request.easing.clone(),
547 };
548
549 self.runtime_state
550 .animation
551 .values
552 .insert(key.clone(), start_value);
553 self.runtime_state.animation.active.insert(key, anim);
554 }
555
556 pub fn sync_animation_requests(&mut self, requests: &[(WidgetId, AnimationRequest)]) {
557 let requested: HashSet<(WidgetId, AnimationPropertyId)> = requests
558 .iter()
559 .map(|(target, request)| (*target, request.property.clone()))
560 .collect();
561
562 self.runtime_state
563 .animation
564 .active
565 .retain(|key, _| requested.contains(key));
566 self.runtime_state
567 .animation
568 .values
569 .retain(|key, _| requested.contains(key));
570 }
571
572 pub fn sync_video_nodes(&mut self, registrations: &[VideoRegistration]) {
573 let mut seen: HashSet<WidgetId> = HashSet::new();
574
575 for reg in registrations {
576 seen.insert(reg.node_id);
577 let entry = self
578 .runtime_state
579 .video
580 .states
581 .entry(reg.node_id)
582 .or_insert_with(crate::env::VideoState::default);
583 entry.asset_source = reg.source.clone();
584 entry.looped = reg.loop_playback;
585 if reg.autoplay && entry.status == VideoStatus::Stopped {
586 entry.status = VideoStatus::Playing;
587 }
588 }
589
590 self.runtime_state
591 .video
592 .states
593 .retain(|node_id, _| seen.contains(node_id));
594 }
595
596 pub fn sync_web_nodes(&mut self, registrations: &[crate::registry::WebRegistration]) {
597 let mut seen: HashSet<WidgetId> = HashSet::new();
598
599 for reg in registrations {
600 seen.insert(reg.node_id);
601 let entry = self
602 .runtime_state
603 .web
604 .states
605 .entry(reg.node_id)
606 .or_insert_with(crate::env::WebState::default);
607
608 if entry.url != reg.url {
610 entry.url = reg.url.clone();
611 entry.loading = true; }
613 entry.user_agent = reg.user_agent.clone();
614 }
615
616 self.runtime_state
617 .web
618 .states
619 .retain(|node_id, _| seen.contains(node_id));
620 }
621
622 pub fn post_layout_hook(&mut self, ir: &CoreIR, layout: &LayoutSnapshot) {
623 let mut current_heroes = HashMap::new();
624
625 for (id, node) in &ir.nodes {
626 if let Op::Semantics(s) = &node.op {
627 if let Some(tag) = &s.hero_tag {
628 if let Some(geom) = layout.get_node_geometry(*id) {
629 current_heroes.insert(tag.clone(), (*id, geom.rect));
630 }
631 }
632 }
633 }
634
635 for (tag, (_new_id, new_rect)) in ¤t_heroes {
637 if let Some((_old_id, old_rect)) = self.runtime_state.hero.positions.get(tag) {
638 if *new_rect != *old_rect {
639 diag::emit(
641 diag::DiagCategory::Layout,
642 diag::DiagLevel::Debug,
643 diag::DiagEventKind::AnchorPlacement {
644 widget: 0,
645 node: 0,
646 rect_x: old_rect.origin.x,
647 rect_y: old_rect.origin.y,
648 rect_w: old_rect.size.width,
649 rect_h: old_rect.size.height,
650 place_left: new_rect.origin.x,
651 place_top: new_rect.origin.y,
652 note: Some(format!("Hero flight: {}", tag)),
653 },
654 );
655 }
656 }
657 }
658
659 self.runtime_state.hero.positions = current_heroes;
660 }
661
662 pub fn handle_input(
663 &mut self,
664 event: InputEvent,
665 ir: &CoreIR,
666 layout: &LayoutSnapshot,
667 ) -> Result<()> {
668 use crate::hit_test::{
669 find_neighbor_focus_node, find_next_focus_node, hit_test_with_scroll, FocusDirection,
670 };
671 use crate::input::gesture::GestureController;
672 use crate::input::hover::HoverController;
673 use crate::input::slider::SliderController;
674 use crate::input::text::TextInputController;
675 use crate::input::{ControllerContext, InputController};
676 use crate::scrollbar::scrollbar_hit_test;
677 use crate::ui::custom_render::downcast_render_object;
678
679 if self.runtime_state.interaction.focused.is_none() {
680 if let Some(autofocus_id) = Self::find_autofocus_node(ir) {
681 self.runtime_state
682 .interaction
683 .set_focused(Some(autofocus_id));
684 if let Some(ime_handler) = &self.ime_handler {
685 let accepts_text = ir
686 .nodes
687 .get(&autofocus_id)
688 .and_then(|node| match &node.op {
689 Op::Semantics(semantics) => {
690 Some(semantics.role == fission_ir::semantics::Role::TextInput)
691 }
692 _ => None,
693 })
694 .unwrap_or(false);
695 ime_handler.set_ime_allowed(accepts_text);
696 }
697 }
698 }
699
700 if matches!(event, InputEvent::Pointer(_)) {
701 let dispatched_actions = {
702 let mut ctx = ControllerContext {
703 ir,
704 layout,
705 text_edit: &mut self.runtime_state.text_edit,
706 interaction: &mut self.runtime_state.interaction,
707 scroll: &mut self.runtime_state.scroll,
708 gesture: &mut self.runtime_state.gesture,
709 clipboard: self.clipboard_backend.as_ref(),
710 measurer: self.measurer.as_ref(),
711 dispatched_actions: Vec::new(),
712 };
713 let mut hover_controller = HoverController;
714 let _ = hover_controller.handle_event(&mut ctx, &event);
715 ctx.dispatched_actions
716 };
717 self.dispatch_input_actions(dispatched_actions)?;
718 }
719
720 let pointer_targets_scrollbar = match &event {
726 InputEvent::Pointer(PointerEvent::Down { point, button, .. })
727 if matches!(button, PointerButton::Primary) =>
728 {
729 scrollbar_hit_test(ir, layout, &self.runtime_state.scroll, *point).is_some()
730 }
731 InputEvent::Pointer(PointerEvent::Move { .. })
732 | InputEvent::Pointer(PointerEvent::Up { .. }) => {
733 self.runtime_state.gesture.scrollbar_drag.is_some()
734 }
735 InputEvent::Pointer(PointerEvent::Scroll { point, .. }) => {
736 scrollbar_hit_test(ir, layout, &self.runtime_state.scroll, *point).is_some()
737 }
738 _ => false,
739 };
740
741 if !pointer_targets_scrollbar {
742 if let Some(point) = Self::event_point(&event) {
743 if let Some(hit_node_id) =
744 hit_test_with_scroll(ir, layout, &self.runtime_state.scroll, point)
745 {
746 let mut target_ro: Option<(WidgetId, &fission_ir::AnyRenderObject)> = None;
751 {
752 let mut walk = Some(hit_node_id);
753 while let Some(nid) = walk {
754 if let Some(ro) = ir.custom_render_objects.get(&nid) {
755 target_ro = Some((nid, ro));
756 break;
757 }
758 walk = ir.nodes.get(&nid).and_then(|n| n.parent);
759 }
760 }
761 if target_ro.is_none() {
762 for (ro_nid, ro) in &ir.custom_render_objects {
763 if let Some(rect) = layout.get_node_rect(*ro_nid) {
764 if rect.contains(point) {
765 target_ro = Some((*ro_nid, ro));
766 break;
767 }
768 }
769 }
770 }
771
772 if let Some((nid, any_ro)) = target_ro {
773 if let Some(render_obj) = downcast_render_object(any_ro) {
774 let mut node_rect = layout
775 .get_node_rect(nid)
776 .unwrap_or(LayoutRect::new(0.0, 0.0, 0.0, 0.0));
777 {
780 let mut walk = ir.nodes.get(&nid).and_then(|n| n.parent);
781 while let Some(pid) = walk {
782 if let Some(pnode) = ir.nodes.get(&pid) {
783 if let fission_ir::Op::Layout(
784 fission_ir::LayoutOp::Scroll { direction, .. },
785 ) = &pnode.op
786 {
787 let off = self.runtime_state.scroll.get_offset(pid);
788 match direction {
789 fission_ir::FlexDirection::Row => {
790 node_rect.origin.x -= off
791 }
792 fission_ir::FlexDirection::Column => {
793 node_rect.origin.y -= off
794 }
795 }
796 }
797 walk = pnode.parent;
798 } else {
799 break;
800 }
801 }
802 }
803 let result = render_obj.handle_event(nid, &event, node_rect);
804 if result.handled {
805 if matches!(event, InputEvent::Pointer(PointerEvent::Down { .. })) {
807 let old_focused_id = self.runtime_state.interaction.focused;
808 if Some(nid) != old_focused_id {
809 self.clear_text_pending_on_blur(old_focused_id, Some(nid));
810 self.dispatch_custom_blur_actions(ir, old_focused_id)?;
811 }
812 self.runtime_state.interaction.set_focused(Some(nid));
813 if let Some(ime_handler) = &self.ime_handler {
814 let accepts_text = render_obj.accepts_text_input();
815 ime_handler.set_ime_allowed(accepts_text);
816 if accepts_text {
817 if let Some(rect) =
818 render_obj.ime_cursor_area(node_rect)
819 {
820 ime_handler.set_ime_cursor_area(rect);
821 }
822 }
823 }
824 }
825 for (target, envelope) in result.actions {
827 self.dispatch_node(envelope, target)?;
828 }
829 return Ok(());
830 }
831 }
832 }
833 }
834 }
835 }
836
837 if matches!(event, InputEvent::Keyboard(_) | InputEvent::Ime(_)) {
843 if let Some(focused_id) = self.runtime_state.interaction.focused {
844 let mut walk_id = Some(focused_id);
845 while let Some(nid) = walk_id {
846 if let Some(any_ro) = ir.custom_render_objects.get(&nid) {
847 if let Some(render_obj) = downcast_render_object(any_ro) {
848 let node_rect = layout
849 .get_node_rect(nid)
850 .unwrap_or(LayoutRect::new(0.0, 0.0, 0.0, 0.0));
851 let result = render_obj.handle_event(nid, &event, node_rect);
852 if result.handled {
853 for (target, envelope) in result.actions {
854 self.dispatch_node(envelope, target)?;
855 }
856 return Ok(());
857 }
858 }
859 }
860 walk_id = ir.nodes.get(&nid).and_then(|n| n.parent);
861 }
862 }
863 }
864
865 let (handled, dispatched_actions) = {
866 let mut ctx = ControllerContext {
867 ir,
868 layout,
869 text_edit: &mut self.runtime_state.text_edit,
870 interaction: &mut self.runtime_state.interaction,
871 scroll: &mut self.runtime_state.scroll,
872 gesture: &mut self.runtime_state.gesture,
873 clipboard: self.clipboard_backend.as_ref(),
874 measurer: self.measurer.as_ref(),
875 dispatched_actions: Vec::new(),
876 };
877
878 let mut hover_controller = HoverController;
879 let _ = hover_controller.handle_event(&mut ctx, &event);
880
881 let mut gesture_controller = GestureController;
882 let handled = if gesture_controller.handle_event(&mut ctx, &event) {
883 true
884 } else {
885 let mut text_controller = TextInputController;
886 if text_controller.handle_event(&mut ctx, &event) {
887 true
888 } else {
889 let mut slider_controller = SliderController;
890 slider_controller.handle_event(&mut ctx, &event)
891 }
892 };
893 (handled, ctx.dispatched_actions)
894 };
895
896 self.dispatch_input_actions(dispatched_actions)?;
897
898 if handled {
899 if matches!(event, InputEvent::Pointer(PointerEvent::Up { .. })) {
900 self.runtime_state.interaction.pressed.clear();
901 self.runtime_state.interaction.last_down_point = None;
902 }
903 return Ok(());
904 }
905
906 match event {
907 InputEvent::Pointer(PointerEvent::Scroll { point, delta, .. }) => {
908 let trace_scroll =
909 std::env::var("FISSION_SCROLL_TRACE").ok().as_deref() == Some("1");
910 if trace_scroll {
911 eprintln!(
912 "[scroll-trace] event point=({:.1},{:.1}) delta=({:.1},{:.1})",
913 point.x, point.y, delta.x, delta.y
914 );
915 }
916 let hit_node_id = scrollbar_hit_test(ir, layout, &self.runtime_state.scroll, point)
917 .map(|hit| hit.geometry.node_id)
918 .or_else(|| {
919 hit_test_with_scroll(ir, layout, &self.runtime_state.scroll, point)
920 });
921 if let Some(hit_node_id) = hit_node_id {
922 if trace_scroll {
923 eprintln!("[scroll-trace] hit_node={}", hit_node_id.as_u128());
924 }
925 let mut current_id = Some(hit_node_id);
926 while let Some(node_id) = current_id {
927 if let Some(node) = ir.nodes.get(&node_id) {
928 if let Op::Layout(LayoutOp::Scroll { direction, .. }) = &node.op {
929 let current_offset = self.runtime_state.scroll.get_offset(node_id);
930 let delta_val = match direction {
931 FlexDirection::Row => delta.x,
932 FlexDirection::Column => delta.y,
933 };
934 let mut new_offset = current_offset + delta_val;
935
936 let mut max_offset = 0.0f32;
937 let mut viewport_w = 0.0f32;
938 let mut viewport_h = 0.0f32;
939 let mut content_w = 0.0f32;
940 let mut content_h = 0.0f32;
941 if let Some(geom) = layout.get_node_geometry(node_id) {
942 viewport_w = geom.rect.width();
943 viewport_h = geom.rect.height();
944 content_w = geom.content_size.width;
945 content_h = geom.content_size.height;
946 max_offset = if matches!(direction, FlexDirection::Row) {
947 (geom.content_size.width - geom.rect.width()).max(0.0)
948 } else {
949 (geom.content_size.height - geom.rect.height()).max(0.0)
950 };
951 new_offset = new_offset.clamp(0.0, max_offset);
952 }
953
954 if trace_scroll {
955 eprintln!(
956 "[scroll-trace] scroll_node={} axis={} offset={:.1}->{:.1} max={:.1} viewport=({:.1},{:.1}) content=({:.1},{:.1})",
957 node_id.as_u128(),
958 match direction { FlexDirection::Row => "x", FlexDirection::Column => "y" },
959 current_offset,
960 new_offset,
961 max_offset,
962 viewport_w,
963 viewport_h,
964 content_w,
965 content_h
966 );
967 }
968
969 {
970 use fission_diagnostics::prelude as diag;
971 diag::emit(
972 diag::DiagCategory::Input,
973 diag::DiagLevel::Debug,
974 diag::DiagEventKind::ScrollUpdate {
975 node: node_id.as_u128(),
976 axis: match direction {
977 FlexDirection::Row => "x".into(),
978 FlexDirection::Column => "y".into(),
979 },
980 point_x: point.x,
981 point_y: point.y,
982 delta: delta_val,
983 old_offset: current_offset,
984 new_offset,
985 max_offset,
986 viewport_w,
987 viewport_h,
988 content_w,
989 content_h,
990 },
991 );
992 }
993
994 self.runtime_state.scroll.set_offset(node_id, new_offset);
995 if (new_offset - current_offset).abs() > 0.001 {
999 break;
1000 }
1001 }
1003 current_id = node.parent;
1004 } else {
1005 break;
1006 }
1007 }
1008 } else if trace_scroll {
1009 eprintln!("[scroll-trace] hit_test: no node");
1010 }
1011 }
1012 InputEvent::Keyboard(KeyEvent::Down {
1013 key_code,
1014 modifiers,
1015 }) => match key_code {
1016 KeyCode::Tab => {
1017 let reverse = (modifiers & 1) != 0;
1018 let old_focus = self.runtime_state.interaction.focused;
1019 let next =
1020 find_next_focus_node(ir, self.runtime_state.interaction.focused, reverse);
1021 if next != old_focus {
1022 self.clear_text_pending_on_blur(old_focus, next);
1023 self.dispatch_custom_blur_actions(ir, old_focus)?;
1024 }
1025 self.runtime_state.interaction.set_focused(next);
1026 }
1027 KeyCode::Up | KeyCode::Down | KeyCode::Left | KeyCode::Right => {
1028 if let Some(focused) = self.runtime_state.interaction.focused {
1029 let dir = match key_code {
1030 KeyCode::Up => FocusDirection::Up,
1031 KeyCode::Down => FocusDirection::Down,
1032 KeyCode::Left => FocusDirection::Left,
1033 KeyCode::Right => FocusDirection::Right,
1034 _ => unreachable!(),
1035 };
1036 if let Some(next) = find_neighbor_focus_node(ir, layout, focused, dir) {
1037 self.clear_text_pending_on_blur(Some(focused), Some(next));
1038 self.dispatch_custom_blur_actions(ir, Some(focused))?;
1039 self.runtime_state.interaction.set_focused(Some(next));
1040 }
1041 }
1042 }
1043 KeyCode::Enter | KeyCode::Space => {
1044 if let Some(focused_id) = self.runtime_state.interaction.focused {
1045 let mut current_id = Some(focused_id);
1046 while let Some(node_id) = current_id {
1047 if let Some(node) = ir.nodes.get(&node_id) {
1048 if let Op::Semantics(semantics) = &node.op {
1049 if let Some(action_entry) = semantics.actions.entries.first() {
1050 if let Some(payload) = &action_entry.payload_data {
1051 let envelope = ActionEnvelope {
1052 id: ActionId::from_u128(action_entry.action_id),
1053 payload: payload.clone(),
1054 };
1055 let input = crate::input::scoped_action_input(
1056 ir,
1057 node_id,
1058 ActionInput::None,
1059 );
1060 return self.dispatch_node_with_input(
1061 envelope, node_id, &input,
1062 );
1063 }
1064 }
1065 }
1066 current_id = node.parent;
1067 } else {
1068 break;
1069 }
1070 }
1071 }
1072 }
1073 _ => {}
1074 },
1075 InputEvent::Pointer(PointerEvent::Down { point, .. }) => {
1076 if let Some(hit_node_id) =
1077 hit_test_with_scroll(ir, layout, &self.runtime_state.scroll, point)
1078 {
1079 diag::emit(
1080 diag::DiagCategory::Input,
1081 diag::DiagLevel::Debug,
1082 diag::DiagEventKind::InputEvent {
1083 kind: "pointer_down_hit".into(),
1084 target: Some(hit_node_id.as_u128()),
1085 position: Some((point.x, point.y)),
1086 },
1087 );
1088 let mut focus_candidate = Some(hit_node_id);
1089 while let Some(node_id) = focus_candidate {
1090 if let Some(node) = ir.nodes.get(&node_id) {
1091 if let Op::Semantics(s) = &node.op {
1092 if s.focusable {
1093 let old_focused_id = self.runtime_state.interaction.focused;
1094 if Some(node_id) != old_focused_id {
1095 self.clear_text_pending_on_blur(
1096 old_focused_id,
1097 Some(node_id),
1098 );
1099 self.dispatch_custom_blur_actions(ir, old_focused_id)?;
1100
1101 if s.role == fission_ir::semantics::Role::TextInput {
1102 if let Some(ime_handler) = &self.ime_handler {
1103 ime_handler.set_ime_allowed(true);
1104 }
1105 } else if let Some(ime_handler) = &self.ime_handler {
1106 ime_handler.set_ime_allowed(false);
1107 }
1108 }
1109 self.runtime_state.interaction.set_focused(Some(node_id));
1110 break;
1111 }
1112 }
1113 focus_candidate = node.parent;
1114 } else {
1115 break;
1116 }
1117 }
1118 if focus_candidate.is_none() {
1119 let old_focused_id = self.runtime_state.interaction.focused;
1120 if let Some(old_focused_id) = self.runtime_state.interaction.focused {
1121 if let Some(old_node) = ir.nodes.get(&old_focused_id) {
1122 if let Op::Semantics(s) = &old_node.op {
1123 if s.role == fission_ir::semantics::Role::TextInput {
1124 if let Some(ime_handler) = &self.ime_handler {
1125 ime_handler.set_ime_allowed(false);
1126 }
1127 }
1128 }
1129 }
1130 }
1131 self.clear_text_pending_on_blur(old_focused_id, None);
1132 self.dispatch_custom_blur_actions(ir, old_focused_id)?;
1133 self.runtime_state.interaction.set_focused(None);
1134 }
1135
1136 let mut current_pressed_id = Some(hit_node_id);
1137 while let Some(node_id) = current_pressed_id {
1138 self.runtime_state.interaction.set_pressed(node_id, true);
1139 if let Some(node) = ir.nodes.get(&node_id) {
1140 current_pressed_id = node.parent;
1141 } else {
1142 break;
1143 }
1144 }
1145 self.runtime_state.interaction.last_down_point = Some(point);
1146
1147 if let Some(focused_id) = self.runtime_state.interaction.focused {
1148 if let Some(node) = ir.nodes.get(&focused_id) {
1149 if let Op::Semantics(s) = &node.op {
1150 if s.role == fission_ir::semantics::Role::TextInput {
1151 if let Some(ime_handler) = &self.ime_handler {
1152 ime_handler.set_ime_cursor_area(LayoutRect::new(
1153 point.x, point.y, 2.0, 16.0,
1154 ));
1155 }
1156 }
1157 }
1158 }
1159 }
1160 } else {
1161 let old_focused_id = self.runtime_state.interaction.focused;
1162 if let Some(old_focused_id) = self.runtime_state.interaction.focused {
1163 if let Some(old_node) = ir.nodes.get(&old_focused_id) {
1164 if let Op::Semantics(s) = &old_node.op {
1165 if s.role == fission_ir::semantics::Role::TextInput {
1166 if let Some(ime_handler) = &self.ime_handler {
1167 ime_handler.set_ime_allowed(false);
1168 }
1169 }
1170 }
1171 }
1172 }
1173 self.clear_text_pending_on_blur(old_focused_id, None);
1174 self.dispatch_custom_blur_actions(ir, old_focused_id)?;
1175 self.runtime_state.interaction.set_focused(None);
1176 }
1177 }
1178 InputEvent::Pointer(PointerEvent::Up { point, .. }) => {
1179 self.runtime_state.interaction.pressed.clear();
1180 self.runtime_state.interaction.last_down_point = None;
1181 if let Some(hit_node_id) =
1182 hit_test_with_scroll(ir, layout, &self.runtime_state.scroll, point)
1183 {
1184 let mut current_id = Some(hit_node_id);
1185 while let Some(node_id) = current_id {
1186 if let Some(node) = ir.nodes.get(&node_id) {
1187 if let Op::Semantics(semantics) = &node.op {
1188 if semantics.role == fission_ir::semantics::Role::TextInput {
1189 } else if let Some(action_entry) = semantics.actions.entries.first()
1191 {
1192 if let Some(payload) = &action_entry.payload_data {
1193 let envelope = ActionEnvelope {
1194 id: ActionId::from_u128(action_entry.action_id),
1195 payload: payload.clone(),
1196 };
1197 diag::emit(
1198 diag::DiagCategory::Input,
1199 diag::DiagLevel::Debug,
1200 diag::DiagEventKind::InputEvent {
1201 kind: "pointer_up_dispatch".into(),
1202 target: Some(node_id.as_u128()),
1203 position: Some((point.x, point.y)),
1204 },
1205 );
1206 let input = crate::input::scoped_action_input(
1207 ir,
1208 node_id,
1209 ActionInput::None,
1210 );
1211 return self
1212 .dispatch_node_with_input(envelope, node_id, &input);
1213 }
1214 }
1215 }
1216 current_id = node.parent;
1217 } else {
1218 break;
1219 }
1220 }
1221 }
1222 }
1223 _ => {}
1224 }
1225 Ok(())
1226 }
1227
1228 pub fn clear_hover_state(&mut self, ir: &CoreIR, point: Option<LayoutPoint>) -> Result<bool> {
1229 use crate::input::hover::HoverController;
1230 use crate::input::ControllerContext;
1231
1232 let dispatched_actions = {
1233 let layout = &LayoutSnapshot::new(LayoutSize::ZERO);
1234 let mut ctx = ControllerContext {
1235 ir,
1236 layout,
1237 text_edit: &mut self.runtime_state.text_edit,
1238 interaction: &mut self.runtime_state.interaction,
1239 scroll: &mut self.runtime_state.scroll,
1240 gesture: &mut self.runtime_state.gesture,
1241 clipboard: self.clipboard_backend.as_ref(),
1242 measurer: self.measurer.as_ref(),
1243 dispatched_actions: Vec::new(),
1244 };
1245 let changed = HoverController::clear(&mut ctx, point);
1246 (changed, ctx.dispatched_actions)
1247 };
1248 self.dispatch_input_actions(dispatched_actions.1)?;
1249 Ok(dispatched_actions.0)
1250 }
1251
1252 fn dispatch_input_actions(
1253 &mut self,
1254 dispatched_actions: Vec<(WidgetId, ActionEnvelope, ActionInput)>,
1255 ) -> Result<()> {
1256 for (target, action, input) in dispatched_actions {
1257 self.dispatch_node_with_input(action, target, &input)?;
1258 }
1259 Ok(())
1260 }
1261
1262 fn clear_text_pending_on_blur(
1263 &mut self,
1264 old_focus: Option<WidgetId>,
1265 new_focus: Option<WidgetId>,
1266 ) {
1267 if old_focus == new_focus {
1268 return;
1269 }
1270 if let Some(old_id) = old_focus {
1271 if let Some(st) = self.runtime_state.text_edit.states.get_mut(&old_id) {
1272 st.pending_model_sync = false;
1273 st.clear_preedit();
1274 }
1275 }
1276 }
1277
1278 fn dispatch_custom_blur_actions(
1279 &mut self,
1280 ir: &CoreIR,
1281 old_focus: Option<WidgetId>,
1282 ) -> Result<()> {
1283 if let Some(old_id) = old_focus {
1284 if let Some(any_ro) = ir.custom_render_objects.get(&old_id) {
1285 if let Some(render_obj) = crate::ui::custom_render::downcast_render_object(any_ro) {
1286 if render_obj.accepts_text_input() {
1287 if let Some(ime_handler) = &self.ime_handler {
1288 ime_handler.set_ime_allowed(false);
1289 }
1290 }
1291 for (target, envelope) in render_obj.blur_actions(old_id) {
1292 self.dispatch_node(envelope, target)?;
1293 }
1294 }
1295 }
1296 }
1297 Ok(())
1298 }
1299
1300 pub fn hit_test(
1301 &self,
1302 point: LayoutPoint,
1303 ir: &CoreIR,
1304 snapshot: &LayoutSnapshot,
1305 ) -> Option<WidgetId> {
1306 if let Some(root) = ir.root {
1307 return self.hit_test_recursive(root, point, ir, snapshot);
1308 }
1309 None
1310 }
1311
1312 fn hit_test_recursive(
1313 &self,
1314 node_id: WidgetId,
1315 point: LayoutPoint,
1316 ir: &CoreIR,
1317 snapshot: &LayoutSnapshot,
1318 ) -> Option<WidgetId> {
1319 if let Some(geom) = snapshot.nodes.get(&node_id) {
1320 if geom.rect.contains(point) {
1321 if let Some(node) = ir.nodes.get(&node_id) {
1322 for child in node.children.iter().rev() {
1323 let mut child_point = point;
1324
1325 if let Op::Layout(LayoutOp::Scroll { direction, .. }) = &node.op {
1326 if !geom.rect.contains(point) {
1327 continue;
1328 }
1329 let offset = self.runtime_state.scroll.get_offset(node_id);
1330 match direction {
1331 FlexDirection::Row => child_point.x += offset,
1332 FlexDirection::Column => child_point.y += offset,
1333 }
1334 }
1335
1336 if let Op::Layout(LayoutOp::Transform { transform }) = &node.op {
1337 let mat = Mat4::from_cols_array(transform);
1338 let local_x = point.x - geom.rect.origin.x;
1375 let local_y = point.y - geom.rect.origin.y;
1376
1377 let p = Vec4::new(local_x, local_y, 0.0, 1.0);
1378 let inv = mat.inverse();
1379 let transformed = inv * p;
1380
1381 child_point = LayoutPoint::new(
1382 transformed.x + geom.rect.origin.x,
1383 transformed.y + geom.rect.origin.y,
1384 );
1385 }
1386
1387 if let Some(hit) =
1388 self.hit_test_recursive(*child, child_point, ir, snapshot)
1389 {
1390 return Some(hit);
1391 }
1392 }
1393
1394 match &node.op {
1395 Op::Paint(_)
1396 | Op::Layout(LayoutOp::Scroll { .. })
1397 | Op::Layout(LayoutOp::Embed { .. }) => return Some(node_id),
1398 _ => return None,
1399 }
1400 }
1401 return None;
1402 }
1403 }
1404 None
1405 }
1406
1407 fn event_point(event: &InputEvent) -> Option<LayoutPoint> {
1413 match event {
1414 InputEvent::Pointer(PointerEvent::Down { point, .. })
1415 | InputEvent::Pointer(PointerEvent::Up { point, .. })
1416 | InputEvent::Pointer(PointerEvent::Move { point, .. })
1417 | InputEvent::Pointer(PointerEvent::Scroll { point, .. }) => Some(*point),
1418 _ => None,
1419 }
1420 }
1421
1422 fn find_autofocus_node(ir: &CoreIR) -> Option<WidgetId> {
1423 fn walk(ir: &CoreIR, node_id: WidgetId) -> Option<WidgetId> {
1424 let node = ir.nodes.get(&node_id)?;
1425 if let Op::Semantics(semantics) = &node.op {
1426 if semantics.autofocus && semantics.focusable && !semantics.disabled {
1427 return Some(node_id);
1428 }
1429 }
1430 for child_id in &node.children {
1431 if let Some(found) = walk(ir, *child_id) {
1432 return Some(found);
1433 }
1434 }
1435 None
1436 }
1437
1438 ir.root.and_then(|root| walk(ir, root))
1439 }
1440
1441 pub fn reconcile_resources(
1442 &mut self,
1443 declarations: Vec<RuntimeResourceDeclaration>,
1444 ) -> Result<()> {
1445 let now = self.clock().current_time();
1446 let mut existing = std::mem::take(&mut self.active_resources);
1447 let mut next = HashMap::new();
1448
1449 for declaration in declarations {
1450 let key = declaration.key.clone();
1451 match existing.remove(&key) {
1452 Some(current)
1453 if current.policy == declaration.policy
1454 && current.deps == declaration.deps
1455 && current.matches_kind(&declaration.kind) =>
1456 {
1457 next.insert(key, current);
1458 }
1459 Some(current) if declaration.policy == ResourcePolicy::PreserveOnChange => {
1460 next.insert(key, current);
1461 }
1462 Some(current) => {
1463 self.stop_resource(&key, ¤t);
1464 let replacement = self.start_resource(declaration, now);
1465 next.insert(key, replacement);
1466 }
1467 None => {
1468 let resource = self.start_resource(declaration, now);
1469 next.insert(key, resource);
1470 }
1471 }
1472 }
1473
1474 for (key, resource) in existing {
1475 self.stop_resource(&key, &resource);
1476 }
1477
1478 self.active_resources = next;
1479 Ok(())
1480 }
1481
1482 pub fn resource_generation(&self, key: &str) -> Option<u64> {
1483 self.active_resources
1484 .get(key)
1485 .map(|resource| resource.generation)
1486 }
1487
1488 pub fn is_resource_current(&self, resource: &ResourceExecutionContext) -> bool {
1489 self.resource_generation(&resource.key) == Some(resource.generation)
1490 }
1491
1492 fn start_resource(
1493 &mut self,
1494 declaration: RuntimeResourceDeclaration,
1495 now: CurrentTime,
1496 ) -> ActiveResource {
1497 let generation = self.next_resource_generation;
1498 self.next_resource_generation += 1;
1499
1500 let context = ResourceExecutionContext {
1501 key: declaration.key.clone(),
1502 generation,
1503 };
1504
1505 let kind = match declaration.kind {
1506 RuntimeResourceKind::Job(mut job) => {
1507 job.effect.resource = Some(context);
1508 self.enqueue_effect(job.effect);
1509 ActiveResourceKind::Job
1510 }
1511 RuntimeResourceKind::Service(mut service) => {
1512 service.effect.resource = Some(context);
1513 let (service_name, slot_key) = match &service.effect.effect {
1514 crate::Effect::StartService(payload) => {
1515 (payload.service_name.clone(), payload.slot_key.clone())
1516 }
1517 _ => unreachable!("service resource must lower to StartService"),
1518 };
1519 self.enqueue_effect(service.effect);
1520 ActiveResourceKind::Service {
1521 service_name,
1522 slot_key,
1523 }
1524 }
1525 RuntimeResourceKind::Timer(timer) => self.start_timer_resource(timer, now),
1526 };
1527
1528 ActiveResource {
1529 generation,
1530 deps: declaration.deps,
1531 policy: declaration.policy,
1532 kind,
1533 }
1534 }
1535
1536 fn start_timer_resource(&self, timer: TimerResource, now: CurrentTime) -> ActiveResourceKind {
1537 let interval_ms = timer.interval_ms.max(1);
1538 ActiveResourceKind::Timer {
1539 interval_ms,
1540 payload: timer.payload,
1541 on_tick: timer.on_tick,
1542 next_fire_at: if timer.immediate {
1543 now
1544 } else {
1545 now.saturating_add(interval_ms)
1546 },
1547 }
1548 }
1549
1550 fn stop_resource(&mut self, key: &str, resource: &ActiveResource) {
1551 if let ActiveResourceKind::Service {
1552 service_name,
1553 slot_key,
1554 } = &resource.kind
1555 {
1556 self.enqueue_effect(EffectEnvelope {
1557 req_id: 0,
1558 effect: crate::Effect::StopService(ServiceStopPayload {
1559 service_name: service_name.clone(),
1560 slot_key: slot_key.clone(),
1561 }),
1562 on_ok: None,
1563 on_err: None,
1564 service_bindings: None,
1565 resource: Some(ResourceExecutionContext {
1566 key: key.to_string(),
1567 generation: resource.generation,
1568 }),
1569 });
1570 }
1571 }
1572}
1573
1574impl ActiveResource {
1575 fn matches_kind(&self, kind: &RuntimeResourceKind) -> bool {
1576 matches!(
1577 (&self.kind, kind),
1578 (ActiveResourceKind::Job, RuntimeResourceKind::Job(_))
1579 | (
1580 ActiveResourceKind::Timer { .. },
1581 RuntimeResourceKind::Timer(_)
1582 )
1583 | (
1584 ActiveResourceKind::Service { .. },
1585 RuntimeResourceKind::Service(_)
1586 )
1587 )
1588 }
1589}