Skip to main content

ansiq_runtime/
engine.rs

1use std::{
2    collections::{BTreeMap, BTreeSet, HashMap},
3    future::Future,
4};
5
6use ansiq_core::{
7    ComponentRenderer, Element, ElementKind, HistoryBlock, HistoryEntry, HookStore, Node, Rect,
8    RuntimeRequest, ScopeId, ViewCtx, dispose_component_scope, flush_reactivity,
9    render_in_component_scope, take_dirty_component_scopes,
10};
11use ansiq_layout::{layout_tree_with_ids, measure_node_height, relayout_tree_along_paths};
12use ansiq_surface::Key;
13use tokio::sync::mpsc::{self, UnboundedReceiver, UnboundedSender, error::SendError};
14
15use crate::{FocusState, routing};
16
17pub trait App {
18    type Message: Send + 'static;
19
20    fn mount(&mut self, _handle: &RuntimeHandle<Self::Message>) {}
21
22    fn render(&mut self, cx: &mut ViewCtx<'_, Self::Message>) -> Element<Self::Message>;
23
24    fn update(&mut self, message: Self::Message, handle: &RuntimeHandle<Self::Message>);
25
26    fn on_unhandled_key(&mut self, _key: Key, _handle: &RuntimeHandle<Self::Message>) -> bool {
27        false
28    }
29}
30
31pub struct RuntimeHandle<Message> {
32    sender: UnboundedSender<RuntimeRequest<Message>>,
33}
34
35impl<Message> Clone for RuntimeHandle<Message> {
36    fn clone(&self) -> Self {
37        Self {
38            sender: self.sender.clone(),
39        }
40    }
41}
42
43impl<Message: Send + 'static> RuntimeHandle<Message> {
44    pub fn emit(&self, message: Message) -> Result<(), SendError<RuntimeRequest<Message>>> {
45        self.sender.send(RuntimeRequest::EmitMessage(message))
46    }
47
48    pub fn quit(&self) -> Result<(), SendError<RuntimeRequest<Message>>> {
49        self.sender.send(RuntimeRequest::Quit)
50    }
51
52    pub fn trap_focus_in(
53        &self,
54        scope_key: impl Into<String>,
55    ) -> Result<(), SendError<RuntimeRequest<Message>>> {
56        self.sender
57            .send(RuntimeRequest::SetFocusScope(Some(scope_key.into())))
58    }
59
60    pub fn clear_focus_scope(&self) -> Result<(), SendError<RuntimeRequest<Message>>> {
61        self.sender.send(RuntimeRequest::SetFocusScope(None))
62    }
63
64    pub fn commit_history(
65        &self,
66        content: String,
67    ) -> Result<(), SendError<RuntimeRequest<Message>>> {
68        self.sender.send(RuntimeRequest::commit_text(content))
69    }
70
71    pub fn commit_history_block(
72        &self,
73        block: HistoryBlock,
74    ) -> Result<(), SendError<RuntimeRequest<Message>>> {
75        self.sender.send(RuntimeRequest::commit_block(block))
76    }
77
78    pub fn spawn<F>(&self, future: F) -> tokio::task::JoinHandle<()>
79    where
80        F: Future<Output = ()> + Send + 'static,
81    {
82        tokio::spawn(future)
83    }
84}
85
86pub struct Engine<A: App> {
87    app: A,
88    hooks: HookStore,
89    component_hooks: HashMap<ScopeId, HookStore>,
90    sender: UnboundedSender<RuntimeRequest<A::Message>>,
91    receiver: UnboundedReceiver<RuntimeRequest<A::Message>>,
92    focus: FocusState,
93    pending_history: Vec<HistoryEntry>,
94    tree: Option<Node<A::Message>>,
95    redraw_regions: Option<Vec<Rect>>,
96    root_scope: Option<ScopeId>,
97    component_scopes: BTreeSet<ScopeId>,
98    pending_component_scopes: Vec<ScopeId>,
99    bounds: Rect,
100    required_height: u16,
101    next_node_id: usize,
102    needs_rerender: bool,
103    dirty: bool,
104    mounted: bool,
105}
106
107impl<A: App> Engine<A> {
108    pub fn new(app: A) -> Self {
109        let (sender, receiver) = mpsc::unbounded_channel();
110
111        Self {
112            app,
113            hooks: HookStore::default(),
114            component_hooks: HashMap::new(),
115            sender,
116            receiver,
117            focus: FocusState::default(),
118            pending_history: Vec::new(),
119            tree: None,
120            redraw_regions: None,
121            root_scope: None,
122            component_scopes: BTreeSet::new(),
123            pending_component_scopes: Vec::new(),
124            bounds: Rect::new(0, 0, 80, 24),
125            required_height: 1,
126            next_node_id: 0,
127            needs_rerender: true,
128            dirty: true,
129            mounted: false,
130        }
131    }
132
133    pub fn app(&self) -> &A {
134        &self.app
135    }
136
137    pub fn app_mut(&mut self) -> &mut A {
138        &mut self.app
139    }
140
141    pub fn handle(&self) -> RuntimeHandle<A::Message> {
142        RuntimeHandle {
143            sender: self.sender.clone(),
144        }
145    }
146
147    pub fn mount(&mut self) {
148        if self.mounted {
149            return;
150        }
151
152        let handle = self.handle();
153        self.app.mount(&handle);
154        self.mounted = true;
155        self.needs_rerender = true;
156        self.dirty = true;
157    }
158
159    pub fn tree(&self) -> Option<&Node<A::Message>> {
160        self.tree.as_ref()
161    }
162
163    pub fn redraw_regions(&self) -> Option<&[Rect]> {
164        self.redraw_regions.as_deref()
165    }
166
167    pub fn is_dirty(&self) -> bool {
168        self.dirty || self.needs_rerender
169    }
170
171    pub fn focused(&self) -> Option<usize> {
172        self.focus.current()
173    }
174
175    pub fn take_pending_history(&mut self) -> Vec<HistoryEntry> {
176        std::mem::take(&mut self.pending_history)
177    }
178
179    pub fn required_height(&self) -> u16 {
180        self.required_height
181    }
182
183    pub fn set_bounds(&mut self, bounds: Rect) {
184        self.bounds = bounds;
185        self.needs_rerender = true;
186        self.dirty = true;
187    }
188
189    pub fn render_tree(&mut self) {
190        self.sync_reactivity();
191
192        if !self.needs_rerender && self.pending_component_scopes.is_empty() && self.tree.is_some() {
193            if self.dirty {
194                self.redraw_regions = None;
195                self.dirty = false;
196            }
197            return;
198        }
199
200        let root_scope_dirty = self
201            .root_scope
202            .is_some_and(|scope| self.pending_component_scopes.contains(&scope));
203
204        if self.tree.is_none()
205            || self.root_scope.is_none()
206            || self.needs_rerender
207            || root_scope_dirty
208        {
209            self.render_root_tree();
210            return;
211        }
212
213        let dirty_scopes: BTreeSet<_> = std::mem::take(&mut self.pending_component_scopes)
214            .into_iter()
215            .collect();
216        if dirty_scopes.is_empty() {
217            return;
218        }
219
220        if let Some(mut tree) = self.tree.take() {
221            let focused_before = self.focus.current();
222            let focus_rect_before =
223                focused_before.and_then(|focused| find_node_rect(&tree, focused));
224            let dirty_paths =
225                self.rerender_dirty_component_nodes(&mut tree, &dirty_scopes, &mut Vec::new());
226            let mut relayout = relayout_tree_along_paths(&mut tree, self.bounds, &dirty_paths);
227            self.required_height = tree.measured_height;
228            self.focus.sync_from_tree(&tree);
229            let focused_after = self.focus.current();
230            let focus_rect_after = focused_after.and_then(|focused| find_node_rect(&tree, focused));
231            append_focus_transition_regions(
232                &mut relayout.invalidated_regions,
233                focus_rect_before,
234                focus_rect_after,
235            );
236            self.redraw_regions = Some(relayout.invalidated_regions);
237            self.refresh_component_scope_registry(&tree);
238            self.tree = Some(tree);
239        }
240
241        self.dirty = false;
242        self.needs_rerender = false;
243    }
244
245    pub fn handle_input(&mut self, key: Key) -> bool {
246        let Some(tree) = self.tree.as_mut() else {
247            return false;
248        };
249
250        let focused_before = self.focus.current();
251        let effect = routing::handle_key(tree, &mut self.focus, key);
252        if effect.dirty {
253            self.dirty = true;
254        }
255        if self.focus.current() != focused_before {
256            self.dirty = true;
257        }
258        if let Some(message) = effect.message {
259            let handle = self.handle();
260            self.app.update(message, &handle);
261            self.needs_rerender = true;
262            self.dirty = true;
263        }
264        if !effect.handled && !effect.quit {
265            let handle = self.handle();
266            if self.app.on_unhandled_key(key, &handle) {
267                self.needs_rerender = true;
268                self.dirty = true;
269            }
270        }
271
272        effect.quit
273    }
274
275    pub fn drain_requests(&mut self) -> bool {
276        let mut should_quit = false;
277
278        while let Ok(request) = self.receiver.try_recv() {
279            match request {
280                RuntimeRequest::EmitMessage(message) => {
281                    let handle = self.handle();
282                    self.app.update(message, &handle);
283                    self.needs_rerender = true;
284                    self.dirty = true;
285                }
286                RuntimeRequest::CommitHistory(entry) => {
287                    self.pending_history.push(entry);
288                    self.dirty = true;
289                }
290                RuntimeRequest::SetFocusScope(scope_key) => {
291                    self.focus.set_scope_key(scope_key);
292                    if let Some(tree) = self.tree.as_ref() {
293                        self.focus.sync_from_tree(tree);
294                    }
295                    self.dirty = true;
296                }
297                RuntimeRequest::Quit => should_quit = true,
298            }
299        }
300
301        should_quit
302    }
303
304    fn sync_reactivity(&mut self) {
305        flush_reactivity();
306        let dirty = take_dirty_component_scopes();
307        if !dirty.is_empty() {
308            self.pending_component_scopes.extend(dirty);
309        }
310    }
311
312    fn render_root_tree(&mut self) {
313        let continuity_restore = self
314            .tree
315            .as_ref()
316            .map(capture_widget_runtime_state_in_subtree)
317            .unwrap_or_default();
318        let focus_restore = self.focus.current().and_then(|current| {
319            self.tree
320                .as_ref()
321                .map(|tree| capture_focus_continuity_in_subtree(tree, current))
322        });
323
324        self.hooks.begin_render();
325        let (scope, element) = render_in_component_scope(self.root_scope, |_| {
326            let element = {
327                let mut cx = ViewCtx::new(&mut self.hooks);
328                self.app.render(&mut cx)
329            };
330            self.hooks.finish_render();
331            element
332        });
333        self.root_scope = Some(scope);
334
335        let element = self.resolve_component_elements(element);
336        self.next_node_id = 0;
337        let mut tree = layout_tree_with_ids(element, self.bounds, &mut self.next_node_id);
338        self.required_height = tree.measured_height;
339        self.redraw_regions = None;
340        restore_widget_runtime_state_in_subtree(&mut tree, &continuity_restore);
341        if let Some(focus_target) = focus_restore
342            && let Some(focus_id) = restore_focus_continuity_in_subtree(&tree, &focus_target)
343        {
344            self.focus.set_current(Some(focus_id));
345        }
346        self.focus.sync_from_tree(&tree);
347        self.refresh_component_scope_registry(&tree);
348        self.tree = Some(tree);
349        self.pending_component_scopes.clear();
350        self.dirty = false;
351        self.needs_rerender = false;
352    }
353
354    fn resolve_component_elements(&mut self, element: Element<A::Message>) -> Element<A::Message> {
355        let Element {
356            kind,
357            layout,
358            style,
359            focusable,
360            continuity_key,
361            children,
362        } = element;
363
364        match kind {
365            ElementKind::Component(mut props) => {
366                let sender = self.sender.clone();
367                let renderer = props.renderer.clone();
368                let (scope, child) = render_in_component_scope(props.scope, |scope| {
369                    self.render_component(renderer, scope, sender)
370                });
371                props.scope = Some(scope);
372                let child = self.resolve_component_elements(child);
373
374                let mut element = Element::new(ElementKind::Component(props))
375                    .with_layout(layout)
376                    .with_style(style)
377                    .with_focusable(focusable)
378                    .with_children(vec![child]);
379                if let Some(key) = continuity_key {
380                    element = element.with_continuity_key(key);
381                }
382                element
383            }
384            kind => {
385                let children = children
386                    .into_iter()
387                    .map(|child| self.resolve_component_elements(child))
388                    .collect();
389                let mut element = Element::new(kind)
390                    .with_layout(layout)
391                    .with_style(style)
392                    .with_focusable(focusable)
393                    .with_children(children);
394                if let Some(key) = continuity_key {
395                    element = element.with_continuity_key(key);
396                }
397                element
398            }
399        }
400    }
401
402    fn rerender_dirty_component_nodes(
403        &mut self,
404        node: &mut Node<A::Message>,
405        dirty_scopes: &BTreeSet<ScopeId>,
406        path: &mut Vec<usize>,
407    ) -> Vec<Vec<usize>> {
408        if let ElementKind::Component(props) = &mut node.element.kind
409            && let Some(scope) = props.scope
410            && dirty_scopes.contains(&scope)
411        {
412            let focus_restore = self.focus.current().and_then(|current| {
413                node.children
414                    .first()
415                    .map(|child| capture_focus_continuity_in_subtree(child, current))
416            });
417            let widget_state_restore = node
418                .children
419                .first()
420                .map(capture_widget_runtime_state_in_subtree)
421                .unwrap_or_default();
422            let sender = self.sender.clone();
423            let renderer = props.renderer.clone();
424            let (_, child) = render_in_component_scope(Some(scope), |scope| {
425                self.render_component(renderer, scope, sender)
426            });
427            let child = self.resolve_component_elements(child);
428            let mut child = layout_tree_with_ids(child, node.rect, &mut self.next_node_id);
429            restore_widget_runtime_state_in_subtree(&mut child, &widget_state_restore);
430            if let Some(focus_target) = focus_restore
431                && let Some(focus_id) = restore_focus_continuity_in_subtree(&child, &focus_target)
432            {
433                self.focus.set_current(Some(focus_id));
434            }
435            node.children = vec![child];
436            node.measured_height = 0;
437            node.measured_height = measure_node_height(node, node.rect.width);
438            return vec![path.clone()];
439        }
440
441        let mut dirty_paths = Vec::new();
442        for (index, child) in node.children.iter_mut().enumerate() {
443            path.push(index);
444            dirty_paths.extend(self.rerender_dirty_component_nodes(child, dirty_scopes, path));
445            path.pop();
446        }
447        dirty_paths
448    }
449
450    fn render_component(
451        &mut self,
452        renderer: ComponentRenderer<A::Message>,
453        scope: ScopeId,
454        _sender: UnboundedSender<RuntimeRequest<A::Message>>,
455    ) -> Element<A::Message> {
456        match renderer {
457            ComponentRenderer::Static(renderer) => renderer(),
458            ComponentRenderer::WithCx(renderer) => {
459                let hooks = self.component_hooks.entry(scope).or_default();
460                hooks.begin_render();
461                let element = {
462                    let mut cx = ViewCtx::new(hooks);
463                    renderer(&mut cx)
464                };
465                hooks.finish_render();
466                element
467            }
468        }
469    }
470
471    fn refresh_component_scope_registry(&mut self, tree: &Node<A::Message>) {
472        let mut live = BTreeSet::new();
473        collect_component_scopes(tree, &mut live);
474
475        // Component scopes are long-lived reactive identities. When a subtree
476        // disappears, drop its hook store and unregister the scope so stale
477        // signals/watchers cannot keep waking removed UI.
478        let stale: Vec<_> = self.component_scopes.difference(&live).copied().collect();
479        for scope in stale {
480            self.component_hooks.remove(&scope);
481            self.pending_component_scopes
482                .retain(|pending| *pending != scope);
483            dispose_component_scope(scope);
484        }
485
486        self.component_scopes = live;
487    }
488}
489
490fn collect_component_scopes<Message>(node: &Node<Message>, scopes: &mut BTreeSet<ScopeId>) {
491    if let ElementKind::Component(props) = &node.element.kind
492        && let Some(scope) = props.scope
493    {
494        scopes.insert(scope);
495    }
496
497    for child in &node.children {
498        collect_component_scopes(child, scopes);
499    }
500}
501
502fn find_node_rect<Message>(node: &Node<Message>, target_id: usize) -> Option<Rect> {
503    if node.id == target_id {
504        return Some(node.rect);
505    }
506
507    for child in &node.children {
508        if let Some(rect) = find_node_rect(child, target_id) {
509            return Some(rect);
510        }
511    }
512
513    None
514}
515
516fn append_focus_transition_regions(
517    regions: &mut Vec<Rect>,
518    before: Option<Rect>,
519    after: Option<Rect>,
520) {
521    if before == after {
522        return;
523    }
524
525    if let Some(rect) = before {
526        push_region_if_missing(regions, rect);
527    }
528    if let Some(rect) = after {
529        push_region_if_missing(regions, rect);
530    }
531}
532
533fn push_region_if_missing(regions: &mut Vec<Rect>, rect: Rect) {
534    if regions.iter().any(|existing| *existing == rect) {
535        return;
536    }
537    regions.push(rect);
538}
539
540#[derive(Debug, Clone, Default)]
541struct FocusContinuityTarget {
542    continuity_key: Option<String>,
543    fallback_index: usize,
544}
545
546#[derive(Debug, Clone, Default)]
547struct WidgetRuntimeStateSnapshot {
548    keyed: BTreeMap<String, ansiq_core::RuntimeWidgetState>,
549    ordered: Vec<ansiq_core::RuntimeWidgetState>,
550}
551
552fn focusable_index_in_subtree<Message>(node: &Node<Message>, target_id: usize) -> Option<usize> {
553    let mut index = 0usize;
554    focusable_index_in_subtree_inner(node, target_id, &mut index)
555}
556
557fn focusable_index_in_subtree_inner<Message>(
558    node: &Node<Message>,
559    target_id: usize,
560    index: &mut usize,
561) -> Option<usize> {
562    if node.element.focusable {
563        if node.id == target_id {
564            return Some(*index);
565        }
566        *index = index.saturating_add(1);
567    }
568
569    for child in &node.children {
570        if let Some(found) = focusable_index_in_subtree_inner(child, target_id, index) {
571            return Some(found);
572        }
573    }
574
575    None
576}
577
578fn focusable_id_at_index<Message>(node: &Node<Message>, target_index: usize) -> Option<usize> {
579    let mut index = 0usize;
580    focusable_id_at_index_inner(node, target_index, &mut index)
581}
582
583fn focusable_id_at_index_inner<Message>(
584    node: &Node<Message>,
585    target_index: usize,
586    index: &mut usize,
587) -> Option<usize> {
588    if node.element.focusable {
589        if *index == target_index {
590            return Some(node.id);
591        }
592        *index = index.saturating_add(1);
593    }
594
595    for child in &node.children {
596        if let Some(found) = focusable_id_at_index_inner(child, target_index, index) {
597            return Some(found);
598        }
599    }
600
601    None
602}
603
604fn capture_focus_continuity_in_subtree<Message>(
605    node: &Node<Message>,
606    target_id: usize,
607) -> FocusContinuityTarget {
608    FocusContinuityTarget {
609        continuity_key: continuity_key_for_node_id(node, target_id),
610        fallback_index: focusable_index_in_subtree(node, target_id).unwrap_or(0),
611    }
612}
613
614fn continuity_key_for_node_id<Message>(node: &Node<Message>, target_id: usize) -> Option<String> {
615    if node.id == target_id {
616        return node.element.continuity_key.clone();
617    }
618
619    for child in &node.children {
620        if let Some(key) = continuity_key_for_node_id(child, target_id) {
621            return Some(key);
622        }
623    }
624
625    None
626}
627
628fn focusable_id_for_continuity_key<Message>(
629    node: &Node<Message>,
630    target_key: &str,
631) -> Option<usize> {
632    if node.element.focusable && node.element.continuity_key() == Some(target_key) {
633        return Some(node.id);
634    }
635
636    for child in &node.children {
637        if let Some(id) = focusable_id_for_continuity_key(child, target_key) {
638            return Some(id);
639        }
640    }
641
642    None
643}
644
645fn restore_focus_continuity_in_subtree<Message>(
646    node: &Node<Message>,
647    target: &FocusContinuityTarget,
648) -> Option<usize> {
649    if let Some(key) = target.continuity_key.as_deref()
650        && let Some(id) = focusable_id_for_continuity_key(node, key)
651    {
652        return Some(id);
653    }
654
655    focusable_id_at_index(node, target.fallback_index)
656}
657
658fn capture_widget_runtime_state_in_subtree<Message>(
659    node: &Node<Message>,
660) -> WidgetRuntimeStateSnapshot {
661    let mut snapshot = WidgetRuntimeStateSnapshot::default();
662    capture_widget_runtime_state_in_subtree_inner(node, &mut snapshot);
663    snapshot
664}
665
666fn capture_widget_runtime_state_in_subtree_inner<Message>(
667    node: &Node<Message>,
668    output: &mut WidgetRuntimeStateSnapshot,
669) {
670    if let Some(state) = node.element.kind.capture_runtime_state() {
671        if let Some(key) = node.element.continuity_key() {
672            output.keyed.insert(key.to_string(), state);
673        } else {
674            output.ordered.push(state);
675        }
676    }
677
678    for child in &node.children {
679        capture_widget_runtime_state_in_subtree_inner(child, output);
680    }
681}
682
683fn restore_widget_runtime_state_in_subtree<Message>(
684    node: &mut Node<Message>,
685    snapshot: &WidgetRuntimeStateSnapshot,
686) {
687    let mut index = 0usize;
688    restore_widget_runtime_state_in_subtree_inner(node, snapshot, &mut index);
689}
690
691fn restore_widget_runtime_state_in_subtree_inner<Message>(
692    node: &mut Node<Message>,
693    snapshot: &WidgetRuntimeStateSnapshot,
694    index: &mut usize,
695) {
696    let expects_runtime_state = node.element.kind.capture_runtime_state().is_some();
697    if expects_runtime_state {
698        let keyed_state = node
699            .element
700            .continuity_key()
701            .and_then(|key| snapshot.keyed.get(key));
702        let positional_state = snapshot.ordered.get(*index);
703        if let Some(state) = keyed_state.or(positional_state) {
704            node.element.kind.restore_runtime_state(state);
705        } else {
706            node.element.kind.initialize_runtime_state();
707        }
708        if keyed_state.is_none() {
709            *index = index.saturating_add(1);
710        }
711    }
712
713    for child in &mut node.children {
714        restore_widget_runtime_state_in_subtree_inner(child, snapshot, index);
715    }
716}