Skip to main content

tui_lipan/core/
component.rs

1use rustc_hash::{FxHashMap, FxHashSet};
2use std::any::TypeId;
3use std::cell::{Cell, RefCell};
4use std::marker::PhantomData;
5use std::sync::Arc;
6use web_time::Instant;
7
8use crate::app::context::SurfaceMode;
9use crate::app::input::command_registry::CommandEntry;
10use crate::app::input::command_registry::CommandRegistry;
11use crate::callback::{CancellationToken, CommandLink, CommandTx, Dispatcher, Link, ScopeId};
12use crate::core::context_value::ContextValue;
13use crate::core::element::{Element, Key};
14use crate::core::event::KeyEvent;
15use crate::core::node::{NodeId, NodeKind, NodeTree};
16use crate::core::runtime_env::{
17    CopyFeedbackRequest, DevToolsRequest, MemoDependency, MemoDependencySnapshot, RuntimeEnv,
18    ScrollDependency, ScrollDependencyKind, ScrollIdentity, TranscriptEntry,
19};
20use crate::runtime::FocusRequest;
21use crate::style::{HostTerminalColors, Rect, RichText, Theme, ThemeExtension};
22use crate::utils::GridSelection;
23
24/// Side-effect command returned from `Component::update`.
25///
26/// A `Command` represents work that should happen *outside* the synchronous `update` call
27/// (HTTP/IO/timers), typically executed by the runtime and eventually producing more messages.
28#[non_exhaustive]
29pub struct Command {
30    action: Box<dyn CommandAction>,
31}
32
33/// Coalescing behavior for keyed background tasks.
34#[derive(Clone, Copy, Debug, PartialEq, Eq)]
35pub enum TaskPolicy {
36    /// Always enqueue every task.
37    QueueAll,
38    /// If a task with the same key is already active, drop the new one.
39    DropIfRunning,
40    /// Keep only the latest pending task for the key.
41    LatestOnly,
42}
43
44mod task_policy;
45
46use task_policy::Task;
47
48#[cfg(not(target_arch = "wasm32"))]
49mod executor_native;
50#[cfg(target_arch = "wasm32")]
51mod executor_wasm;
52#[cfg(not(target_arch = "wasm32"))]
53mod timer_native;
54#[cfg(target_arch = "wasm32")]
55mod timer_wasm;
56
57#[cfg(not(target_arch = "wasm32"))]
58use executor_native::TaskExecutor;
59#[cfg(target_arch = "wasm32")]
60use executor_wasm::TaskExecutor;
61#[cfg(not(target_arch = "wasm32"))]
62use timer_native::TimerService;
63#[cfg(target_arch = "wasm32")]
64use timer_wasm::TimerService;
65
66/// Skip `dt` of virtual time in the [`Command::after`] scheduler, running whatever becomes due.
67///
68/// Called from [`RuntimeEnv::advance_clock`](crate::core::runtime_env::RuntimeEnv::advance_clock), so
69/// every harness that drives the virtual clock settles deferred commands without asking. Returns how
70/// many tasks ran, which the tests use to assert the plumbing rather than a wall-clock coincidence.
71pub(crate) fn advance_deferred_commands(horizon: Instant, runtime_id: RuntimeId) -> usize {
72    TimerService::global().advance_owned(horizon, runtime_id)
73}
74
75impl std::fmt::Debug for Command {
76    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
77        f.debug_struct("Command").finish_non_exhaustive()
78    }
79}
80
81impl Command {
82    /// Run a one-shot action on the UI thread.
83    pub fn new(action: impl FnOnce() + 'static) -> Self {
84        Self {
85            action: Box::new(RunAction(Some(action))),
86        }
87    }
88
89    /// Spawn a background task that can send messages back.
90    pub fn spawn<Msg, F>(f: F) -> Self
91    where
92        Msg: Send + 'static,
93        F: FnOnce(CommandLink<Msg>) + Send + 'static,
94    {
95        Self {
96            action: Box::new(SpawnAction::<Msg, F> {
97                f: Some(f),
98                _marker: PhantomData,
99            }),
100        }
101    }
102
103    /// Run a background task after `delay`, without holding a worker while it waits.
104    ///
105    /// Use this for debounces, retries, and recurring ticks instead of `Command::spawn` with a
106    /// `thread::sleep` inside. The executor runs on a fixed pool of 2-8 workers, so a sleeping task
107    /// occupies one for the whole delay: two recurring timers can park every worker on a low-core
108    /// machine and stall unrelated background work behind them. A shared timer thread does the
109    /// waiting here, and the task reaches the pool only once it is due.
110    ///
111    /// ```no_run
112    /// # use tui_lipan::prelude::*;
113    /// # use std::time::Duration;
114    /// # #[derive(Clone)] enum Msg { Tick }
115    /// # fn example() -> Command {
116    /// Command::after(Duration::from_secs(1), |link: CommandLink<Msg>| {
117    ///     link.send(Msg::Tick);
118    /// })
119    /// # }
120    /// ```
121    ///
122    /// Re-arming from the handler gives a recurring tick that costs no thread between firings. On
123    /// the web target without the `web` feature there is no timer source, and the task runs
124    /// immediately rather than being dropped.
125    pub fn after<Msg, F>(delay: std::time::Duration, f: F) -> Self
126    where
127        Msg: Send + 'static,
128        F: FnOnce(CommandLink<Msg>) + Send + 'static,
129    {
130        Self {
131            action: Box::new(AfterAction::<Msg, F> {
132                delay,
133                f: Some(f),
134                _marker: PhantomData,
135            }),
136        }
137    }
138
139    /// Spawn a keyed background task with explicit coalescing policy.
140    pub fn spawn_keyed<Msg, F>(key: impl Into<Arc<str>>, policy: TaskPolicy, f: F) -> Self
141    where
142        Msg: Send + 'static,
143        F: FnOnce(CommandLink<Msg>) + Send + 'static,
144    {
145        Self {
146            action: Box::new(SpawnKeyedAction::<Msg, F> {
147                key: key.into(),
148                policy,
149                f: Some(f),
150                _marker: PhantomData,
151            }),
152        }
153    }
154
155    pub(crate) fn run(self, runtime: CommandRuntime) {
156        self.action.run(runtime);
157    }
158}
159
160pub(crate) struct CommandRuntime {
161    pub(crate) scope: ScopeId,
162    pub(crate) tx: CommandTx,
163    /// Which runtime is running this command, so a deferred timer can be attributed to it.
164    pub(crate) runtime_id: RuntimeId,
165}
166
167/// Identity of one runtime within the process.
168///
169/// The delayed-task queue is process-wide while runtimes are not - a test binary runs several in
170/// parallel - so a timer records the runtime that armed it and a virtual advance claims only its own.
171#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
172pub(crate) struct RuntimeId(u64);
173
174impl RuntimeId {
175    #[cfg(test)]
176    pub(crate) fn from_raw_for_tests(raw: u64) -> Self {
177        Self(raw)
178    }
179
180    /// Mint an id no live runtime shares.
181    pub(crate) fn next() -> Self {
182        static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
183        Self(NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed))
184    }
185}
186
187trait CommandAction {
188    fn run(self: Box<Self>, runtime: CommandRuntime);
189}
190
191struct RunAction<F>(Option<F>);
192
193impl<F> CommandAction for RunAction<F>
194where
195    F: FnOnce() + 'static,
196{
197    fn run(mut self: Box<Self>, _runtime: CommandRuntime) {
198        if let Some(f) = self.0.take() {
199            f();
200        }
201    }
202}
203
204struct SpawnAction<Msg, F> {
205    f: Option<F>,
206    _marker: PhantomData<fn(Msg)>,
207}
208
209struct SpawnKeyedAction<Msg, F> {
210    key: Arc<str>,
211    policy: TaskPolicy,
212    f: Option<F>,
213    _marker: PhantomData<fn(Msg)>,
214}
215
216/// Run `f` on the executor after `delay`, for crate code that already holds its own channel and so
217/// has no [`CommandRuntime`] to build a [`Command`] against (see [`CommandLink::send_after`]).
218///
219/// [`CommandLink::send_after`]: crate::callback::CommandLink::send_after
220pub(crate) fn schedule_after(delay: std::time::Duration, f: impl FnOnce() + Send + 'static) {
221    TimerService::global().schedule(delay, Task::with_token(f, CancellationToken::default()));
222}
223
224struct AfterAction<Msg, F> {
225    delay: std::time::Duration,
226    f: Option<F>,
227    _marker: PhantomData<fn(Msg)>,
228}
229
230impl<Msg, F> CommandAction for AfterAction<Msg, F>
231where
232    Msg: Send + 'static,
233    F: FnOnce(CommandLink<Msg>) + Send + 'static,
234{
235    fn run(mut self: Box<Self>, runtime: CommandRuntime) {
236        let Some(f) = self.f.take() else {
237            return;
238        };
239
240        let token = CancellationToken::default();
241        let link = CommandLink::new(runtime.scope, runtime.tx, token.clone());
242        TimerService::global().schedule_owned(
243            self.delay,
244            Task::with_token(move || f(link), token),
245            Some(runtime.runtime_id),
246        );
247    }
248}
249
250impl<Msg, F> CommandAction for SpawnAction<Msg, F>
251where
252    Msg: Send + 'static,
253    F: FnOnce(CommandLink<Msg>) + Send + 'static,
254{
255    fn run(mut self: Box<Self>, runtime: CommandRuntime) {
256        let Some(f) = self.f.take() else {
257            return;
258        };
259
260        let token = CancellationToken::default();
261        let link = CommandLink::new(runtime.scope, runtime.tx, token.clone());
262        TaskExecutor::global().execute(Task::with_token(move || f(link), token));
263    }
264}
265
266impl<Msg, F> CommandAction for SpawnKeyedAction<Msg, F>
267where
268    Msg: Send + 'static,
269    F: FnOnce(CommandLink<Msg>) + Send + 'static,
270{
271    fn run(mut self: Box<Self>, runtime: CommandRuntime) {
272        let Some(f) = self.f.take() else {
273            return;
274        };
275
276        let key = Arc::clone(&self.key);
277        let policy = self.policy;
278        let token = CancellationToken::default();
279        let link = CommandLink::new(runtime.scope, runtime.tx, token.clone());
280        TaskExecutor::global().execute_keyed(key, policy, Task::with_token(move || f(link), token));
281    }
282}
283
284impl<Msg: 'static> Link<Msg> {
285    /// Create a background `Command` that can send messages back to this component.
286    pub fn command<F>(&self, f: F) -> Command
287    where
288        Msg: Send + 'static,
289        F: FnOnce(CommandLink<Msg>) + Send + 'static,
290    {
291        Command::spawn::<Msg, F>(f)
292    }
293
294    /// Create a keyed background `Command` with coalescing policy.
295    pub fn command_keyed<F>(&self, key: impl Into<Arc<str>>, policy: TaskPolicy, f: F) -> Command
296    where
297        Msg: Send + 'static,
298        F: FnOnce(CommandLink<Msg>) + Send + 'static,
299    {
300        Command::spawn_keyed::<Msg, F>(key, policy, f)
301    }
302}
303
304/// How much refresh work an [`Update`] asks the runtime for.
305///
306/// Ordered by cost. Worth asserting on in tests for messages that arrive at high frequency — the
307/// difference between [`Paint`](Self::Paint) and [`Full`](Self::Full) on a message that fires per
308/// keystroke or per chunk of streamed output is the difference between a repaint and rebuilding the
309/// whole window.
310#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
311pub enum UpdateLevel {
312    /// Nothing changed; no frame.
313    #[default]
314    None,
315    /// Redraw from the node tree as it stands: no `view()`, no layout.
316    Paint,
317    /// Re-run `view()` and reconcile layout for the dirty scope.
318    Layout,
319    /// Re-run `view()`, expand, reconcile and repaint everything.
320    Full,
321}
322
323/// Component update result with optional side-effect command.
324pub struct Update {
325    /// Whether the component's view needs re-rendering.
326    pub dirty: bool,
327    /// Granularity of the requested refresh.
328    pub(crate) level: UpdateLevel,
329    /// An optional async command to execute.
330    pub command: Option<Command>,
331}
332
333impl Update {
334    /// Request a paint-only update.
335    pub fn paint() -> Self {
336        Self {
337            dirty: true,
338            level: UpdateLevel::Paint,
339            command: None,
340        }
341    }
342
343    /// Request a layout reconcile update.
344    pub fn layout() -> Self {
345        Self {
346            dirty: true,
347            level: UpdateLevel::Layout,
348            command: None,
349        }
350    }
351
352    /// Request a layout reconcile and optionally run a command.
353    pub fn layout_with_command(command: impl Into<Option<Command>>) -> Self {
354        match command.into() {
355            Some(command) => Self {
356                dirty: true,
357                level: UpdateLevel::Layout,
358                command: Some(command),
359            },
360            None => Self::layout(),
361        }
362    }
363
364    /// Request a full update.
365    pub fn full() -> Self {
366        Self {
367            dirty: true,
368            level: UpdateLevel::Full,
369            command: None,
370        }
371    }
372
373    /// Run a command without marking the component dirty (e.g. background fetch with a follow-up message).
374    pub fn command_only(command: Command) -> Self {
375        Self {
376            dirty: false,
377            level: UpdateLevel::None,
378            command: Some(command),
379        }
380    }
381
382    /// Nothing changed, no command.
383    pub fn none() -> Self {
384        Self {
385            dirty: false,
386            level: UpdateLevel::None,
387            command: None,
388        }
389    }
390
391    /// Full refresh, and optionally run a command when [`Some`]; when [`None`], same as [`Self::full`].
392    pub fn with_command(command: impl Into<Option<Command>>) -> Self {
393        match command.into() {
394            Some(cmd) => Self {
395                dirty: true,
396                level: UpdateLevel::Full,
397                command: Some(cmd),
398            },
399            None => Self::full(),
400        }
401    }
402
403    /// The refresh level this update asks for.
404    pub fn level(&self) -> UpdateLevel {
405        self.level
406    }
407}
408
409/// The return type of `Component::on_key`.
410///
411/// `handled` is tracked separately from `dirty` and `command`.
412pub struct KeyUpdate {
413    /// Whether the key event was handled.
414    pub handled: bool,
415    /// State change and side-effect work, same as `Update`.
416    pub update: Update,
417}
418
419impl KeyUpdate {
420    /// Mark the key event as handled.
421    pub fn handled(update: Update) -> Self {
422        Self {
423            handled: true,
424            update,
425        }
426    }
427
428    /// Mark the key event as unhandled.
429    pub fn unhandled(update: Update) -> Self {
430        Self {
431            handled: false,
432            update,
433        }
434    }
435}
436
437/// A stateful, reusable UI component.
438///
439/// Components own their dependencies via the struct instance (dependency injection),
440/// while all UI state mutations happen through `update()` via `Context`.
441pub trait Component: Sized + 'static {
442    /// Messages (events) that can be sent to this component.
443    type Message: 'static;
444
445    /// Properties passed from the parent.
446    type Properties: Clone + PartialEq + 'static;
447
448    /// Local state owned by the runtime.
449    type State: 'static;
450
451    /// Create the initial state for this component.
452    fn create_state(&self, props: &Self::Properties) -> Self::State;
453
454    /// Stable memo key used to retain this component's previously expanded subtree.
455    ///
456    /// Return `Some(key)` to opt into retained subtree reuse. When the key is unchanged,
457    /// the runtime may skip `view()` and reuse the prior expanded subtree until local state,
458    /// props, or observed context dependencies require a refresh.
459    fn memo_key(&self, _props: &Self::Properties, _ctx: &Context<Self>) -> Option<u64> {
460        None
461    }
462
463    /// Called once when the component is first mounted.
464    ///
465    /// This is the right place to kick off background work (HTTP/IO) by returning a `Command`.
466    fn init(&mut self, _ctx: &mut Context<Self>) -> Option<Command> {
467        None
468    }
469
470    /// Declarative UI definition.
471    fn view(&self, ctx: &Context<Self>) -> Element;
472
473    /// Handle a keyboard event that was not handled by the focused node.
474    ///
475    /// This enables global shortcuts (e.g. `Ctrl+S`) without attaching handlers to every widget.
476    /// Return `KeyUpdate::handled` to stop bubbling.
477    fn on_key(&mut self, _key: KeyEvent, _ctx: &mut Context<Self>) -> KeyUpdate {
478        KeyUpdate::unhandled(Update::none())
479    }
480
481    /// Called on the root component when the host terminal/window gains or loses focus.
482    ///
483    /// This is distinct from widget focus and is never delivered to nested components.
484    fn on_window_focus_changed(&mut self, _focused: bool, _ctx: &mut Context<Self>) -> Update {
485        Update::none()
486    }
487
488    /// Update state in response to a message.
489    ///
490    /// Returns `(dirty, command)`.
491    fn update(&mut self, msg: Self::Message, ctx: &mut Context<Self>) -> Update;
492
493    /// Called when properties have changed.
494    ///
495    /// Returns `(dirty, command)`.
496    fn on_props_changed(
497        &mut self,
498        _old_props: &Self::Properties,
499        _ctx: &mut Context<Self>,
500    ) -> Update {
501        Update::none()
502    }
503
504    /// Called once when the component is being unmounted.
505    fn unmount(&mut self, _ctx: &mut Context<Self>) {}
506}
507
508/// Simple width breakpoint derived from the current viewport.
509#[derive(Clone, Copy, Debug, PartialEq, Eq)]
510pub enum Breakpoint {
511    /// Narrow terminal / compact layout.
512    Small,
513    /// Medium-width terminal.
514    Medium,
515    /// Wide terminal.
516    Large,
517}
518
519/// Resolved scrollbar visibility for a keyed [`TextArea`](crate::widgets::TextArea).
520#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
521pub struct ScrollbarVisibility {
522    /// Whether the vertical scrollbar is visible.
523    pub v: bool,
524    /// Whether the horizontal scrollbar is visible.
525    pub h: bool,
526}
527
528/// Shared ancestor-walk state used by both [`FocusContext`] and [`HoverContext`].
529#[derive(Default)]
530struct NodeChainState {
531    current_node: Cell<Option<NodeId>>,
532    chain_scopes: RefCell<Vec<ScopeId>>,
533    chain_keys: RefCell<Vec<Key>>,
534    generation: Cell<u64>,
535}
536
537impl NodeChainState {
538    fn node_id(&self) -> Option<NodeId> {
539        self.current_node.get()
540    }
541
542    fn has_within_scope(&self, scope: ScopeId) -> bool {
543        self.chain_scopes.borrow().contains(&scope)
544    }
545
546    fn has_within_key(&self, key: &Key) -> bool {
547        self.chain_keys
548            .borrow()
549            .iter()
550            .any(|candidate| candidate == key)
551    }
552
553    fn push_scope_if_missing(scopes: &mut Vec<ScopeId>, scope: ScopeId) {
554        if !scopes.contains(&scope) {
555            scopes.push(scope);
556        }
557    }
558
559    fn push_key_if_missing(keys: &mut Vec<Key>, key: &Key) {
560        if !keys.iter().any(|candidate| candidate == key) {
561            keys.push(key.clone());
562        }
563    }
564
565    fn replace_snapshot(&self, node: Option<NodeId>, scopes: Vec<ScopeId>, keys: Vec<Key>) {
566        let mut scope_ref = self.chain_scopes.borrow_mut();
567        let mut key_ref = self.chain_keys.borrow_mut();
568        let changed = self.current_node.get() != node || *scope_ref != scopes || *key_ref != keys;
569
570        self.current_node.set(node);
571        *scope_ref = scopes;
572        *key_ref = keys;
573
574        if changed {
575            self.generation
576                .set(self.generation.get().wrapping_add(1).max(1));
577        }
578    }
579
580    /// Walk the ancestor chain from `node` upward, collecting the scopes and keys that contain it.
581    fn chain_for(tree: &NodeTree, node: Option<NodeId>) -> (Vec<ScopeId>, Vec<Key>) {
582        let mut scopes = Vec::new();
583        let mut keys = Vec::new();
584
585        if let Some(mut cur) = node {
586            Self::push_scope_if_missing(&mut scopes, ScopeId(1));
587
588            loop {
589                if !tree.is_valid(cur) {
590                    break;
591                }
592
593                let node_ref = tree.node(cur);
594
595                if let Some(k) = &node_ref.key {
596                    Self::push_key_if_missing(&mut keys, k);
597                }
598
599                if let NodeKind::Group(group) = &node_ref.kind {
600                    Self::push_scope_if_missing(&mut scopes, group.scope);
601                }
602
603                let Some(parent) = node_ref.parent else {
604                    break;
605                };
606                cur = parent;
607            }
608        }
609
610        (scopes, keys)
611    }
612
613    /// Walk the ancestor chain from `node` upward, populating scopes and keys.
614    fn update_chain(&self, tree: &NodeTree, node: Option<NodeId>) {
615        let (scopes, keys) = Self::chain_for(tree, node);
616        self.replace_snapshot(node, scopes, keys);
617    }
618
619    fn generation(&self) -> u64 {
620        self.generation.get()
621    }
622}
623
624/// Shared render-time focus information from the previous frame.
625#[derive(Default)]
626pub(crate) struct FocusContext {
627    inner: NodeChainState,
628}
629
630impl FocusContext {
631    pub(crate) fn update_from_tree(
632        &self,
633        tree: &NodeTree,
634        focused: Option<NodeId>,
635        focused_key: Option<&Key>,
636    ) {
637        let mut cur = focused.filter(|id| tree.is_valid(*id));
638
639        if cur.is_none()
640            && let Some(key) = focused_key
641        {
642            if let Some(id) = tree
643                .iter()
644                .find(|n| n.key.as_ref() == Some(key))
645                .map(|n| n.id)
646            {
647                cur = Some(id);
648            } else {
649                self.inner
650                    .replace_snapshot(None, Vec::new(), vec![key.clone()]);
651                return;
652            }
653        }
654
655        self.inner.update_chain(tree, cur);
656    }
657
658    pub(crate) fn focused_node_id(&self) -> Option<NodeId> {
659        self.inner.node_id()
660    }
661
662    pub(crate) fn has_focus_within_scope(&self, scope: ScopeId) -> bool {
663        self.inner.has_within_scope(scope)
664    }
665
666    pub(crate) fn has_focus_within_key(&self, key: &Key) -> bool {
667        self.inner.has_within_key(key)
668    }
669
670    pub(crate) fn generation(&self) -> u64 {
671        self.inner.generation()
672    }
673}
674
675/// Shared render-time hover information from the previous frame.
676///
677/// Also records *which* hover questions the last view asked, so a hover change can be answered with
678/// a repaint unless one of those questions would now answer differently. Knowing only that some view
679/// reads hover is far too coarse: a single `has_hover_within_key` call anywhere — one keyed close
680/// button in a sidebar row, say — would otherwise promote every pointer crossing anywhere in the
681/// window to a full rebuild.
682#[derive(Default)]
683pub(crate) struct HoverContext {
684    inner: NodeChainState,
685    /// One hover question, and the component scope that asked it. The scope is what makes a changed
686    /// answer refreshable on its own, rather than by rebuilding the whole tree.
687    queries: RefCell<Vec<(ScopeId, HoverQuery)>>,
688}
689
690/// A hover question a view asked, recorded so its answer can be re-evaluated later.
691#[derive(Clone, PartialEq, Eq)]
692enum HoverQuery {
693    /// `has_hover_within`: is hover inside this scope's subtree?
694    WithinScope(ScopeId),
695    /// `has_hover_within_key`: is hover inside the subtree of this key?
696    WithinKey(Key),
697    /// `hovered_node_id`: which node exactly? Any change of node changes this answer.
698    NodeId,
699}
700
701impl HoverContext {
702    pub(crate) fn update_from_tree(&self, tree: &NodeTree, hovered: Option<NodeId>) {
703        // Start of a full view pass: every question asked from here on is this frame's. A paint-only
704        // frame never gets here, so the previous frame's questions stay in force — which is exactly
705        // what they must do, since its element tree is still the one on screen.
706        self.queries.borrow_mut().clear();
707        self.update_chain(tree, hovered);
708    }
709
710    /// Point the chain at `hovered` without disturbing the recorded questions.
711    ///
712    /// A frame that only reconciles an already-built element tree must land here rather than in
713    /// [`Self::update_from_tree`]: no view ran, so the questions still on file are the ones the tree
714    /// on screen depends on, and dropping them would price the next crossing as a repaint and leave
715    /// that tree stale.
716    pub(crate) fn update_chain(&self, tree: &NodeTree, hovered: Option<NodeId>) {
717        let cur = hovered.filter(|id| tree.is_valid(*id));
718        self.inner.update_chain(tree, cur);
719    }
720
721    /// Start of a *partial* view pass: only `scopes` are about to run, so only their questions are
722    /// this frame's to re-ask. Every other scope keeps the tree — and the questions — it already had.
723    ///
724    /// The chain has to move here rather than after the refresh: the views being re-run are being
725    /// re-run precisely because their hover answers changed, so they must see the new hover position
726    /// while they build, not the one that made them dirty.
727    pub(crate) fn begin_scoped_view(
728        &self,
729        tree: &NodeTree,
730        hovered: Option<NodeId>,
731        scopes: &[ScopeId],
732    ) {
733        self.queries
734            .borrow_mut()
735            .retain(|(asker, _)| !scopes.contains(asker));
736        self.update_chain(tree, hovered);
737    }
738
739    fn record(&self, asker: ScopeId, query: HoverQuery) {
740        let mut queries = self.queries.borrow_mut();
741        if !queries
742            .iter()
743            .any(|(scope, recorded)| *scope == asker && *recorded == query)
744        {
745            queries.push((asker, query));
746        }
747    }
748
749    pub(crate) fn hovered_node_id(&self, asker: ScopeId) -> Option<NodeId> {
750        self.record(asker, HoverQuery::NodeId);
751        self.inner.node_id()
752    }
753
754    pub(crate) fn has_hover_within_scope(&self, scope: ScopeId) -> bool {
755        self.record(scope, HoverQuery::WithinScope(scope));
756        self.inner.has_within_scope(scope)
757    }
758
759    pub(crate) fn has_hover_within_key(&self, asker: ScopeId, key: &Key) -> bool {
760        self.record(asker, HoverQuery::WithinKey(key.clone()));
761        self.inner.has_within_key(key)
762    }
763
764    /// The scopes whose recorded hover answers would change if hover moved to `hovered`.
765    ///
766    /// Empty means the element tree on screen is still correct and the frame can be a repaint.
767    /// A non-empty result names exactly the views that have to run, so the frame can be a layout
768    /// pass over those scopes instead of a rebuild of everything. Compares against the chain
769    /// captured at the last view pass; both chains are ancestor walks, so this costs one walk.
770    pub(crate) fn scopes_needing_view(
771        &self,
772        tree: &NodeTree,
773        hovered: Option<NodeId>,
774    ) -> Vec<ScopeId> {
775        let queries = self.queries.borrow();
776        if queries.is_empty() {
777            return Vec::new();
778        }
779        let cur = hovered.filter(|id| tree.is_valid(*id));
780        let (scopes, keys) = NodeChainState::chain_for(tree, cur);
781
782        let mut affected = Vec::new();
783        for (asker, query) in queries.iter() {
784            let changed = match query {
785                HoverQuery::WithinScope(scope) => {
786                    scopes.contains(scope) != self.inner.has_within_scope(*scope)
787                }
788                HoverQuery::WithinKey(key) => {
789                    keys.iter().any(|candidate| candidate == key) != self.inner.has_within_key(key)
790                }
791                HoverQuery::NodeId => self.inner.node_id() != cur,
792            };
793            if changed && !affected.contains(asker) {
794                affected.push(*asker);
795            }
796        }
797        affected
798    }
799
800    pub(crate) fn generation(&self) -> u64 {
801        self.inner.generation()
802    }
803}
804
805/// Shared render-time scrollable information from the previous frame.
806#[derive(Default)]
807pub(crate) struct ScrollContext {
808    by_key: RefCell<FxHashMap<ScrollIdentity, ScrollbarVisibility>>,
809    text_area_metrics_by_key: RefCell<FxHashMap<ScrollIdentity, crate::widgets::TextAreaMetrics>>,
810    metrics_generations: RefCell<FxHashMap<ScrollIdentity, u64>>,
811    scrollbar_generations: RefCell<FxHashMap<ScrollIdentity, u64>>,
812    metrics_view_dependencies: RefCell<FxHashSet<ScrollIdentity>>,
813    scrollbar_view_dependencies: RefCell<FxHashSet<ScrollIdentity>>,
814}
815
816/// Snapshot of [`ScrollContext`] generations taken before a reconcile pass,
817/// used to decide whether cached views became stale.
818#[derive(Clone, Debug, PartialEq, Eq)]
819pub(crate) struct ScrollGenerations {
820    metrics: FxHashMap<ScrollIdentity, u64>,
821    scrollbars: FxHashMap<ScrollIdentity, u64>,
822}
823
824impl ScrollContext {
825    pub(crate) fn update_from_tree(&self, tree: &NodeTree) {
826        let mut map = self.by_key.borrow_mut();
827        let prev = std::mem::take(&mut *map);
828        let mut metrics_map = self.text_area_metrics_by_key.borrow_mut();
829        let prev_metrics = std::mem::take(&mut *metrics_map);
830
831        for node in tree.iter_with_overlays() {
832            if let (Some(key), NodeKind::TextArea(text_area)) = (&node.key, &node.kind) {
833                let identity = ScrollIdentity {
834                    scope: node_scope(tree, node.id),
835                    key: key.clone(),
836                };
837                let metrics = text_area.metrics(node.rect);
838                map.insert(identity.clone(), metrics.scrollbars);
839                metrics_map.insert(identity, metrics);
840            }
841        }
842
843        advance_changed_generations(&prev, &map, &self.scrollbar_generations);
844        advance_changed_generations(&prev_metrics, &metrics_map, &self.metrics_generations);
845    }
846
847    pub(crate) fn get(&self, identity: &ScrollIdentity) -> Option<ScrollbarVisibility> {
848        self.by_key.borrow().get(identity).copied()
849    }
850
851    pub(crate) fn text_area_metrics(
852        &self,
853        identity: &ScrollIdentity,
854    ) -> Option<crate::widgets::TextAreaMetrics> {
855        self.text_area_metrics_by_key
856            .borrow()
857            .get(identity)
858            .cloned()
859    }
860
861    pub(crate) fn begin_view(&self, scope: ScopeId) {
862        self.metrics_view_dependencies
863            .borrow_mut()
864            .retain(|identity| identity.scope != scope);
865        self.scrollbar_view_dependencies
866            .borrow_mut()
867            .retain(|identity| identity.scope != scope);
868    }
869
870    pub(crate) fn remove_scope(&self, scope: ScopeId) {
871        self.begin_view(scope);
872        self.by_key
873            .borrow_mut()
874            .retain(|identity, _| identity.scope != scope);
875        self.text_area_metrics_by_key
876            .borrow_mut()
877            .retain(|identity, _| identity.scope != scope);
878        self.metrics_generations
879            .borrow_mut()
880            .retain(|identity, _| identity.scope != scope);
881        self.scrollbar_generations
882            .borrow_mut()
883            .retain(|identity, _| identity.scope != scope);
884    }
885
886    pub(crate) fn mark_view_dependency(&self, dependency: &ScrollDependency) {
887        match dependency.kind {
888            ScrollDependencyKind::Metrics => {
889                self.metrics_view_dependencies
890                    .borrow_mut()
891                    .insert(dependency.identity.clone());
892            }
893            ScrollDependencyKind::Scrollbars => {
894                self.scrollbar_view_dependencies
895                    .borrow_mut()
896                    .insert(dependency.identity.clone());
897            }
898        }
899    }
900
901    pub(crate) fn dependency_generation(&self, dependency: &ScrollDependency) -> u64 {
902        let generations = match dependency.kind {
903            ScrollDependencyKind::Metrics => &self.metrics_generations,
904            ScrollDependencyKind::Scrollbars => &self.scrollbar_generations,
905        };
906        generations
907            .borrow()
908            .get(&dependency.identity)
909            .copied()
910            .unwrap_or(0)
911    }
912
913    pub(crate) fn view_generations(&self) -> ScrollGenerations {
914        ScrollGenerations {
915            metrics: self.metrics_generations.borrow().clone(),
916            scrollbars: self.scrollbar_generations.borrow().clone(),
917        }
918    }
919
920    /// Whether a change since `prev` can affect the output of any cached
921    /// `view()`, given which accessors views have actually used.
922    pub(crate) fn view_dependencies_stale(&self, prev: &ScrollGenerations) -> bool {
923        self.metrics_view_dependencies
924            .borrow()
925            .iter()
926            .any(|identity| {
927                self.metrics_generations
928                    .borrow()
929                    .get(identity)
930                    .copied()
931                    .unwrap_or(0)
932                    != prev.metrics.get(identity).copied().unwrap_or(0)
933            })
934            || self
935                .scrollbar_view_dependencies
936                .borrow()
937                .iter()
938                .any(|identity| {
939                    self.scrollbar_generations
940                        .borrow()
941                        .get(identity)
942                        .copied()
943                        .unwrap_or(0)
944                        != prev.scrollbars.get(identity).copied().unwrap_or(0)
945                })
946    }
947}
948
949fn node_scope(tree: &NodeTree, mut id: NodeId) -> ScopeId {
950    loop {
951        let node = tree.node(id);
952        if let NodeKind::Group(group) = &node.kind {
953            return group.scope;
954        }
955        let Some(parent) = node.parent.filter(|parent| tree.is_valid(*parent)) else {
956            return ScopeId(1);
957        };
958        id = parent;
959    }
960}
961
962fn advance_changed_generations<T: PartialEq>(
963    previous: &FxHashMap<ScrollIdentity, T>,
964    current: &FxHashMap<ScrollIdentity, T>,
965    generations: &RefCell<FxHashMap<ScrollIdentity, u64>>,
966) {
967    let identities: FxHashSet<_> = previous.keys().chain(current.keys()).cloned().collect();
968    let mut generations = generations.borrow_mut();
969    for identity in identities {
970        if previous.get(&identity) != current.get(&identity) {
971            let generation = generations.entry(identity).or_default();
972            *generation = generation.wrapping_add(1).max(1);
973        }
974    }
975    generations.retain(|identity, _| current.contains_key(identity));
976}
977
978/// Per-component runtime context.
979pub struct Context<C: Component> {
980    /// Component-local state.
981    pub state: C::State,
982
983    /// Component properties.
984    pub props: C::Properties,
985
986    viewport: Rect,
987    link: Link<C::Message>,
988    env: RuntimeEnv,
989    scope: ScopeId,
990}
991
992impl<C: Component> Context<C> {
993    pub(crate) fn new(
994        component: &C,
995        scope: ScopeId,
996        dispatcher: Dispatcher,
997        props: C::Properties,
998        env: RuntimeEnv,
999        viewport: Rect,
1000    ) -> Self {
1001        let state = component.create_state(&props);
1002        Self {
1003            state,
1004            props,
1005            viewport,
1006            link: Link::new(scope, dispatcher),
1007            env,
1008            scope,
1009        }
1010    }
1011
1012    /// Link used to create callbacks.
1013    pub fn link(&self) -> &Link<C::Message> {
1014        &self.link
1015    }
1016
1017    /// Access the toast notification API.
1018    pub fn toast(&self) -> crate::overlay::ToastHandle {
1019        crate::overlay::ToastHandle::new(self.env.overlay_manager.clone())
1020    }
1021
1022    /// Access the clipboard API.
1023    pub fn clipboard(&self) -> crate::clipboard::ClipboardHandle {
1024        crate::clipboard::ClipboardHandle::new(
1025            self.env.clipboard.clone(),
1026            self.env.clipboard_config.clone(),
1027        )
1028    }
1029
1030    /// Access the command registry API.
1031    pub fn command_registry(&self) -> CommandRegistry {
1032        self.env.command_registry.clone()
1033    }
1034
1035    /// Check if an app command chord is currently pending.
1036    ///
1037    /// True from the moment the first chord step is accepted. Use this for chrome that must react
1038    /// instantly - a mode badge, suppressing a caret - and
1039    /// [`command_chord_revealed`](Self::command_chord_revealed) for chrome that should wait out a
1040    /// quickly completed chord.
1041    pub fn command_chord_pending(&self) -> bool {
1042        self.env.command_chord_pending_since.get().is_some()
1043    }
1044
1045    /// When the currently pending command chord started, or `None` when none is pending.
1046    pub fn command_chord_pending_since(&self) -> Option<Instant> {
1047        self.env.command_chord_pending_since.get()
1048    }
1049
1050    /// Change the reveal delay set by
1051    /// [`App::command_chord_reveal_delay`](crate::App::command_chord_reveal_delay) at runtime, for
1052    /// an app whose delay comes from a config file it reloads while running. Takes effect on the
1053    /// next chord; a chord already pending keeps being measured against the new value.
1054    pub fn set_command_chord_reveal_delay(&self, delay: std::time::Duration) {
1055        self.env.command_chord_reveal_delay.set(delay);
1056    }
1057
1058    /// Whether a command chord has been pending for at least
1059    /// [`App::command_chord_reveal_delay`](crate::App::command_chord_reveal_delay).
1060    ///
1061    /// This is the signal for a which-key panel or any other chord affordance that should appear
1062    /// only when the user hesitates: with the default zero delay it matches
1063    /// [`command_chord_pending`](Self::command_chord_pending) exactly, and with a delay set the
1064    /// runtime schedules a frame at the moment it becomes true, so a view reading it does not need
1065    /// a timer of its own.
1066    pub fn command_chord_revealed(&self) -> bool {
1067        self.env
1068            .command_chord_pending_since
1069            .get()
1070            .is_some_and(|since| {
1071                self.env.elapsed(since) >= self.env.command_chord_reveal_delay.get()
1072            })
1073    }
1074
1075    /// Register a command scoped to this component instance.
1076    pub fn register_command(&self, entry: CommandEntry) {
1077        self.env
1078            .command_registry
1079            .register_for_scope(self.scope, entry);
1080    }
1081
1082    /// Current viewport bounds (content area) for this render.
1083    pub fn viewport(&self) -> Rect {
1084        self.env.note_memo_dependency(MemoDependency::Viewport);
1085        self.viewport
1086    }
1087
1088    /// Returns the active theme for this component's subtree.
1089    pub fn theme(&self) -> Theme {
1090        self.env.note_memo_dependency(MemoDependency::Theme);
1091        self.env.active_theme.borrow().clone()
1092    }
1093
1094    /// Returns a cloned typed theme extension from the active theme.
1095    pub fn theme_extension<T>(&self) -> Option<T>
1096    where
1097        T: ThemeExtension,
1098    {
1099        self.env.note_memo_dependency(MemoDependency::Theme);
1100        self.env.active_theme.borrow().extension_cloned::<T>()
1101    }
1102
1103    /// Returns a cloned typed value from the nearest active `ContextProvider<T>`.
1104    pub fn use_context<T>(&self) -> Option<T>
1105    where
1106        T: ContextValue,
1107    {
1108        self.env.note_memo_dependency(MemoDependency::Context {
1109            type_id: TypeId::of::<T>(),
1110            name: std::any::type_name::<T>(),
1111        });
1112        self.env
1113            .contexts
1114            .borrow()
1115            .get(&TypeId::of::<T>())
1116            .and_then(|value| value.as_ref().downcast_ref::<T>())
1117            .cloned()
1118    }
1119
1120    /// Returns a cloned typed value from the nearest active `ContextProvider<T>`.
1121    pub fn context<T>(&self) -> Option<T>
1122    where
1123        T: ContextValue,
1124    {
1125        self.use_context::<T>()
1126    }
1127
1128    /// Returns `true` when the app runs in inline viewport mode.
1129    pub fn is_inline(&self) -> bool {
1130        self.env.surface_mode.is_inline()
1131    }
1132
1133    /// Returns the app surface mode.
1134    pub fn surface_mode(&self) -> SurfaceMode {
1135        self.env.surface_mode
1136    }
1137
1138    /// Returns the current renderer animation phase used by built-in visual effects.
1139    ///
1140    /// Capture this value when starting a one-shot phase-based effect such as
1141    /// [`VisualEffect::centered_burst_ripple`](crate::style::VisualEffect::centered_burst_ripple).
1142    pub fn effect_phase(&self) -> u64 {
1143        self.env.effect_phase.get()
1144    }
1145
1146    /// Return the latest host terminal colors known to the runner.
1147    ///
1148    /// Values are available after `App::system_theme()` or
1149    /// `App::live_host_terminal_colors(true)` is enabled and the runner completes
1150    /// at least one successful OSC 4/10/11 probe. The cache is updated on the UI
1151    /// thread, not by app background tasks.
1152    pub fn host_terminal_colors(&self) -> Option<HostTerminalColors> {
1153        self.env.host_terminal_colors()
1154    }
1155
1156    /// Return the generation for the cached host terminal colors.
1157    ///
1158    /// Starts at `0` and increments whenever the runner observes a different
1159    /// host terminal palette. Components can compare this value to decide when
1160    /// to rebuild app-specific theme tokens.
1161    pub fn host_terminal_color_generation(&self) -> u64 {
1162        self.env.host_terminal_color_generation()
1163    }
1164
1165    /// Ask the runner to refresh host terminal colors on the UI thread.
1166    ///
1167    /// This is a no-op unless `App::system_theme()` or
1168    /// `App::live_host_terminal_colors(true)` was enabled. Those app settings
1169    /// also refresh on focus gained. The actual OSC query is performed later by
1170    /// the runner while coordinating with its input reader, so apps should call
1171    /// this instead of polling
1172    /// [`query_host_colors`](crate::style::query_host_colors) from background
1173    /// threads.
1174    pub fn request_host_terminal_color_refresh(&self) {
1175        self.env.request_host_terminal_color_refresh();
1176    }
1177
1178    /// Last pointer position in terminal content coordinates.
1179    ///
1180    /// `None` until a mouse event has been seen this run. Coordinates match
1181    /// [`MouseEvent`](crate::core::event::MouseEvent): the same space widgets and `MouseRegion`
1182    /// callbacks receive, including after inline-mode content-offset adjustment. Motion that is
1183    /// forwarded to a tracking terminal still updates this value, so a key binding can place UI at
1184    /// the pointer without a move listener.
1185    pub fn last_mouse(&self) -> Option<(u16, u16)> {
1186        self.env.last_mouse.get()
1187    }
1188
1189    /// Returns whether terminal mouse capture is currently enabled.
1190    pub fn mouse_capture_enabled(&self) -> bool {
1191        self.env.note_memo_dependency(MemoDependency::MouseCapture);
1192        self.env.mouse_capture.get()
1193    }
1194
1195    /// Enable or disable terminal mouse capture at runtime.
1196    pub fn set_mouse_capture(&self, enabled: bool) {
1197        if self.env.mouse_capture.get() != enabled {
1198            self.env.mouse_capture.set(enabled);
1199            self.env.mouse_capture_generation.set(
1200                self.env
1201                    .mouse_capture_generation
1202                    .get()
1203                    .wrapping_add(1)
1204                    .max(1),
1205            );
1206        }
1207    }
1208
1209    /// Toggle terminal mouse capture at runtime and return the new state.
1210    pub fn toggle_mouse_capture(&self) -> bool {
1211        let next = !self.env.mouse_capture.get();
1212        self.set_mouse_capture(next);
1213        next
1214    }
1215
1216    /// Stop the app to the shell, the way `ctrl+z` does in an ordinary program.
1217    ///
1218    /// Wire this to whatever key your app uses for suspend: raw mode clears the
1219    /// tty's `ISIG` flag, so the terminal driver never generates `SIGTSTP`
1220    /// while the app runs and nothing happens unless the app asks for it.
1221    ///
1222    /// At the next frame boundary the runner hands the terminal back — raw
1223    /// mode, alternate screen, mouse tracking — stops the process group with
1224    /// `SIGTSTP`, and restores the terminal with a full repaint once the job is
1225    /// foregrounded again. An external `SIGTSTP` (`kill -TSTP`, a parent shell)
1226    /// takes the same path, so the shell never inherits a terminal that is
1227    /// still in raw mode.
1228    ///
1229    /// No-op on targets without POSIX job control (Windows, wasm).
1230    pub fn suspend_to_shell(&self) {
1231        crate::app::job_control::request_suspend();
1232    }
1233
1234    /// Append plain rich-text lines to transcript history above the inline viewport.
1235    ///
1236    /// This is a no-op outside inline transcript mode.
1237    pub fn append_transcript_lines<I, L>(&mut self, lines: I)
1238    where
1239        I: IntoIterator<Item = L>,
1240        L: Into<RichText>,
1241    {
1242        if !matches!(self.env.surface_mode, SurfaceMode::InlineTranscript { .. }) {
1243            return;
1244        }
1245
1246        let lines: Vec<RichText> = lines.into_iter().map(Into::into).collect();
1247        if lines.is_empty() {
1248            return;
1249        }
1250
1251        self.env
1252            .transcript_history
1253            .borrow_mut()
1254            .push(TranscriptEntry::Lines(lines.clone()));
1255        self.env
1256            .pending_transcript_entries
1257            .borrow_mut()
1258            .push_back(TranscriptEntry::Lines(lines));
1259    }
1260
1261    /// Append a rendered element to transcript history above the inline viewport.
1262    ///
1263    /// This is a no-op outside inline transcript mode. The appended subtree must
1264    /// already be expanded: this API accepts widget trees, not `Component` elements.
1265    pub fn append_transcript_element(&mut self, element: impl Into<Element>) {
1266        if !matches!(self.env.surface_mode, SurfaceMode::InlineTranscript { .. }) {
1267            return;
1268        }
1269
1270        let element = element.into();
1271        if element.contains_unexpanded_component() {
1272            crate::debug::internal_log!(
1273                "[tui-lipan] append_transcript_element ignored an element containing Component nodes"
1274            );
1275            return;
1276        }
1277
1278        self.env
1279            .transcript_history
1280            .borrow_mut()
1281            .push(TranscriptEntry::Element(Box::new(element.clone())));
1282        self.env
1283            .pending_transcript_entries
1284            .borrow_mut()
1285            .push_back(TranscriptEntry::Element(Box::new(element)));
1286    }
1287
1288    /// Returns `true` if the currently focused node (from the previous frame) is inside this
1289    /// component's subtree.
1290    pub fn has_focus_within(&self) -> bool {
1291        self.env.note_memo_dependency(MemoDependency::Focus);
1292        self.env.focus.has_focus_within_scope(self.scope)
1293    }
1294
1295    /// Returns `true` if the currently focused node (from the previous frame) is inside the
1296    /// subtree of the element identified by `key`.
1297    pub fn has_focus_within_key(&self, key: impl Into<Key>) -> bool {
1298        let key = key.into();
1299        self.env.note_memo_dependency(MemoDependency::Focus);
1300        self.env.focus.has_focus_within_key(&key)
1301    }
1302
1303    /// Returns resolved scrollbar visibility for the keyed `TextArea` from the previous frame.
1304    ///
1305    /// The `TextArea` element must have an `Element::key`; missing or first-frame entries return
1306    /// `ScrollbarVisibility::default()`.
1307    pub fn text_area_scrollbars(&self, key: impl Into<Key>) -> ScrollbarVisibility {
1308        let dependency = ScrollDependency {
1309            identity: ScrollIdentity {
1310                scope: self.scope,
1311                key: key.into(),
1312            },
1313            kind: ScrollDependencyKind::Scrollbars,
1314        };
1315        self.env
1316            .note_memo_dependency(MemoDependency::Scroll(dependency.clone()));
1317        self.env.scroll.mark_view_dependency(&dependency);
1318        self.env
1319            .scroll
1320            .text_area_metrics(&dependency.identity)
1321            .map(|metrics| metrics.scrollbars)
1322            .or_else(|| self.env.scroll.get(&dependency.identity))
1323            .unwrap_or_default()
1324    }
1325
1326    /// Returns previous-frame metrics for a keyed `TextArea`.
1327    pub fn text_area_metrics(
1328        &self,
1329        key: impl Into<Key>,
1330    ) -> Option<crate::widgets::TextAreaMetrics> {
1331        let dependency = ScrollDependency {
1332            identity: ScrollIdentity {
1333                scope: self.scope,
1334                key: key.into(),
1335            },
1336            kind: ScrollDependencyKind::Metrics,
1337        };
1338        self.env
1339            .note_memo_dependency(MemoDependency::Scroll(dependency.clone()));
1340        self.env.scroll.mark_view_dependency(&dependency);
1341        self.env.scroll.text_area_metrics(&dependency.identity)
1342    }
1343
1344    /// Returns `true` if the hovered node (from the previous frame) is inside this
1345    /// component's subtree.
1346    pub fn has_hover_within(&self) -> bool {
1347        self.env.note_memo_dependency(MemoDependency::Hover);
1348        self.env.hover.has_hover_within_scope(self.scope)
1349    }
1350
1351    /// Returns `true` if the hovered node (from the previous frame) is inside the
1352    /// subtree of the element identified by `key`.
1353    pub fn has_hover_within_key(&self, key: impl Into<Key>) -> bool {
1354        let key = key.into();
1355        self.env.note_memo_dependency(MemoDependency::Hover);
1356        self.env.hover.has_hover_within_key(self.scope, &key)
1357    }
1358
1359    /// Returns the focused node id from the previous frame, if any.
1360    pub fn focused_node_id(&self) -> Option<NodeId> {
1361        self.env.note_memo_dependency(MemoDependency::Focus);
1362        self.env.focus.focused_node_id()
1363    }
1364
1365    /// Returns the hovered node id from the previous frame, if any.
1366    pub fn hovered_node_id(&self) -> Option<NodeId> {
1367        self.env.note_memo_dependency(MemoDependency::Hover);
1368        self.env.hover.hovered_node_id(self.scope)
1369    }
1370
1371    /// Property-scoped transition for a single style value.
1372    ///
1373    /// Pass the desired *final* `target` each frame. The first call for a given
1374    /// `key` records the target as the resting value. When `target` differs
1375    /// from the previously stored target, a transition starts from the current
1376    /// value to the new target using `config`. Returns the current interpolated
1377    /// value for this frame — embed it directly in a `Style` slot.
1378    ///
1379    /// Animations are driven by the runtime: while a transition is in flight,
1380    /// the component re-renders each animation tick (~16 ms) so the new value
1381    /// flows into the style. Keys not read during a frame are dropped, so
1382    /// transitions for hidden elements are automatically cleaned up.
1383    ///
1384    /// ```ignore
1385    /// let edge_fg = ctx.transition(
1386    ///     "prompt-edge",
1387    ///     if focused { theme.primary } else { theme.muted },
1388    ///     TransitionConfig::default(),
1389    /// );
1390    /// EdgeDecoration::new(Edge::Left).style(Style::new().fg(edge_fg))
1391    /// ```
1392    ///
1393    /// # Panics
1394    /// Panics if the same `key` is used with two different value types.
1395    pub fn transition<T>(
1396        &self,
1397        key: impl Into<Key>,
1398        target: T,
1399        config: crate::animation::TransitionConfig,
1400    ) -> T
1401    where
1402        T: crate::animation::Lerp + PartialEq + 'static,
1403    {
1404        let key = key.into();
1405        self.env.note_memo_dependency(MemoDependency::Transition);
1406        self.env.animations.transition(key, target, config)
1407    }
1408
1409    /// Transition a colour that only ever feeds a style, as a [`Paint`] the renderer resolves.
1410    ///
1411    /// Prefer this over [`transition`](Self::transition) for chrome that merely fades — focus
1412    /// borders, titles, badges. Because the returned paint *names* the transition instead of carrying
1413    /// its current colour, the element tree holds still for the whole fade, and the runtime advances
1414    /// it with a repaint rather than re-running `view()` for every one of its frames. A 160 ms fade at
1415    /// 60 fps is ten rebuilds of the window versus ten repaints.
1416    ///
1417    /// The trade is that the caller never sees the interpolated colour, so it cannot be used for
1418    /// anything but a style — which is exactly what makes skipping `view()` sound. Reach for
1419    /// [`transition`](Self::transition) when the value has to inform layout, text, or a decision.
1420    ///
1421    /// ```no_run
1422    /// # use tui_lipan::prelude::*;
1423    /// # use tui_lipan::animation::TransitionConfig;
1424    /// # fn example(ctx: &Context<impl Component>, focused: bool, cfg: TransitionConfig) -> Element {
1425    /// let border = ctx.animated_color(
1426    ///     "pane-border",
1427    ///     if focused { Color::Rgb(120, 200, 255) } else { Color::Rgb(70, 80, 90) },
1428    ///     cfg,
1429    /// );
1430    /// Frame::new().border(true).style(Style::new().fg(border)).into()
1431    /// # }
1432    /// ```
1433    ///
1434    /// [`Paint`]: crate::style::Paint
1435    pub fn animated_color(
1436        &self,
1437        key: impl Into<Key>,
1438        target: crate::style::Color,
1439        config: crate::animation::TransitionConfig,
1440    ) -> crate::style::Paint {
1441        // Deliberately no `MemoDependency::Transition`: nothing a view produces depends on where this
1442        // fade currently is, so a memoized subtree must not be invalidated as it advances.
1443        self.env
1444            .animations
1445            .animated_paint(key.into(), target, config)
1446    }
1447
1448    /// Convenience helper for responsive layouts based on viewport width.
1449    ///
1450    /// Returns:
1451    /// - `Breakpoint::Small` if `viewport().w < medium`,
1452    /// - `Breakpoint::Medium` if `viewport().w < large`,
1453    /// - `Breakpoint::Large` otherwise.
1454    pub fn breakpoint(&self, medium: u16, large: u16) -> Breakpoint {
1455        let (medium, large) = if medium <= large {
1456            (medium, large)
1457        } else {
1458            (large, medium)
1459        };
1460
1461        self.env.note_memo_dependency(MemoDependency::Viewport);
1462        let w = self.viewport.w;
1463        if w < medium {
1464            Breakpoint::Small
1465        } else if w < large {
1466            Breakpoint::Medium
1467        } else {
1468            Breakpoint::Large
1469        }
1470    }
1471
1472    pub(crate) fn env(&self) -> &RuntimeEnv {
1473        &self.env
1474    }
1475
1476    pub(crate) fn set_viewport(&mut self, viewport: Rect) {
1477        self.viewport = viewport;
1478    }
1479
1480    pub(crate) fn set_active_theme(&mut self, theme: Theme) {
1481        let mut active_theme = self.env.active_theme.borrow_mut();
1482        if *active_theme != theme {
1483            *active_theme = theme;
1484            self.env.active_theme_generation.set(
1485                self.env
1486                    .active_theme_generation
1487                    .get()
1488                    .wrapping_add(1)
1489                    .max(1),
1490            );
1491        }
1492    }
1493
1494    pub(crate) fn set_contexts(
1495        &mut self,
1496        contexts: rustc_hash::FxHashMap<TypeId, std::sync::Arc<dyn std::any::Any>>,
1497        generations: rustc_hash::FxHashMap<TypeId, u64>,
1498    ) {
1499        *self.env.contexts.borrow_mut() = contexts;
1500        *self.env.context_generations.borrow_mut() = generations;
1501    }
1502
1503    pub(crate) fn memo_key(&self, component: &C) -> Option<u64> {
1504        component.memo_key(&self.props, self)
1505    }
1506
1507    pub(crate) fn begin_memo_dependency_capture(&self) {
1508        self.env.scroll.begin_view(self.scope);
1509        self.env.begin_memo_dependency_capture();
1510    }
1511
1512    pub(crate) fn finish_memo_dependency_capture(&self) -> MemoDependencySnapshot {
1513        self.env.finish_memo_dependency_capture(self.viewport)
1514    }
1515
1516    pub(crate) fn memo_dependencies_match(&self, snapshot: &MemoDependencySnapshot) -> bool {
1517        snapshot.matches(&self.env, self.viewport)
1518    }
1519
1520    #[cfg(feature = "devtools")]
1521    pub(crate) fn memo_dependency_mismatch(
1522        &self,
1523        snapshot: &MemoDependencySnapshot,
1524    ) -> Option<crate::core::nested::MemoDependencyKind> {
1525        snapshot.first_mismatch(&self.env, self.viewport)
1526    }
1527
1528    /// Request application shutdown.
1529    pub fn quit(&mut self) {
1530        self.env.quit.set(true);
1531    }
1532
1533    /// Request shutdown without holding a unique borrow.
1534    ///
1535    /// [`Self::quit`] is the app-facing form; the runner holds its context
1536    /// shared and needs the same effect from `&self`.
1537    pub(crate) fn request_quit(&self) {
1538        self.env.quit.set(true);
1539    }
1540
1541    /// Request focus to move to the focusable node with `key`.
1542    ///
1543    /// This takes effect on the next event loop tick / render.
1544    pub fn request_focus(&mut self, key: impl Into<Key>) {
1545        *self.env.focus_request.borrow_mut() = Some(FocusRequest::Key(key.into()));
1546    }
1547
1548    /// Clear the current focus.
1549    ///
1550    /// Under [`crate::FocusPolicy::Auto`], the next render restores the default focus target.
1551    pub fn blur(&mut self) {
1552        *self.env.focus_request.borrow_mut() = Some(FocusRequest::Clear);
1553    }
1554
1555    /// Request focus to move to the next tab stop.
1556    pub fn focus_next(&mut self) {
1557        *self.env.focus_request.borrow_mut() = Some(FocusRequest::Next);
1558    }
1559
1560    /// Request focus to move to the previous tab stop.
1561    pub fn focus_prev(&mut self) {
1562        *self.env.focus_request.borrow_mut() = Some(FocusRequest::Prev);
1563    }
1564
1565    /// Request a full layout and paint pass on the next frame.
1566    ///
1567    /// Use after the host terminal was repainted by another process (external editor,
1568    /// pager, etc.) so nested components are not stuck on a layout-only update path.
1569    pub fn request_full_repaint(&self) {
1570        self.env.full_repaint.set(true);
1571    }
1572
1573    /// Flash the active copy-feedback style on [`NodeId`] `node_id`.
1574    ///
1575    /// The flash uses the duration and style from [`ClipboardConfig`](crate::ClipboardConfig).
1576    /// The request is delivered to the runner at the next animation boundary, so this method is
1577    /// safe to call from a component update without mutating renderer-owned state directly. The
1578    /// target is captured when this method is called; later focus changes cannot redirect it. The
1579    /// request is ignored if the node is no longer valid or copy feedback is disabled with a zero
1580    /// duration.
1581    pub fn flash_copy_feedback(&self, node_id: NodeId) {
1582        self.env.request_copy_feedback(node_id, None);
1583    }
1584
1585    /// Flash the copy-feedback style over an explicit grid `range` on `node_id`.
1586    ///
1587    /// [`Context::flash_copy_feedback`] paints whatever the node currently has selected, so a
1588    /// caller that copies and immediately leaves its selection mode has to keep the selection
1589    /// alive purely so the flash has something to draw. Passing the copied range instead lets
1590    /// the selection be cleared straight away. When the runner drains the request, it captures
1591    /// the terminal rows covered by the range and paints that snapshot for the flash's duration,
1592    /// independently of later selection or terminal-content changes.
1593    ///
1594    /// Columns are display columns, matching the terminal renderer. Only widgets that render a
1595    /// grid selection honor the range; elsewhere this behaves like
1596    /// [`Context::flash_copy_feedback`].
1597    pub fn flash_copy_feedback_range(&self, node_id: NodeId, range: GridSelection) {
1598        self.env.request_copy_feedback(node_id, Some(range));
1599    }
1600
1601    /// Lazily replace the host-application metric rows shown in the DevTools `App` tab.
1602    ///
1603    /// The factory is invoked only when the `devtools` feature is enabled. Its
1604    /// input order is preserved, and returning an empty iterator clears all
1605    /// application rows. This stores only the supplied values; it never invokes
1606    /// host callbacks while DevTools renders.
1607    ///
1608    /// Publishing is render-neutral: it does not schedule a frame. Calls made
1609    /// from `view()` are consumed by the DevTools extra root later in that same
1610    /// frame. Calls made elsewhere remain stored until a host-requested frame
1611    /// rebuilds the panel.
1612    ///
1613    /// Without the `devtools` feature this is a no-op, so the same application
1614    /// code continues to compile without constructing or formatting metrics.
1615    pub fn set_devtools_metrics<F, I>(&self, metrics: F)
1616    where
1617        F: FnOnce() -> I,
1618        I: IntoIterator<Item = crate::DevToolsMetric>,
1619    {
1620        #[cfg(feature = "devtools")]
1621        {
1622            self.env
1623                .devtools_metrics
1624                .replace(metrics().into_iter().collect());
1625        }
1626        #[cfg(not(feature = "devtools"))]
1627        {
1628            let _ = metrics;
1629        }
1630    }
1631
1632    #[cfg(feature = "devtools")]
1633    pub(crate) fn devtools_metrics(
1634        &self,
1635    ) -> std::rc::Rc<crate::core::runtime_env::DevToolsMetrics> {
1636        std::rc::Rc::clone(&self.env.devtools_metrics)
1637    }
1638
1639    /// Return whether the built-in DevTools panel is currently visible.
1640    ///
1641    /// This reports the runner-synchronized state after panel key handling and
1642    /// queued visibility requests have been applied. It returns `false` when
1643    /// the `devtools` feature is disabled.
1644    pub fn devtools_visible(&self) -> bool {
1645        #[cfg(feature = "devtools")]
1646        {
1647            self.env.devtools_metrics.is_visible()
1648        }
1649        #[cfg(not(feature = "devtools"))]
1650        {
1651            false
1652        }
1653    }
1654
1655    /// Request that the built-in devtools panel becomes visible.
1656    pub fn show_devtools(&self) {
1657        *self.env.devtools_request.borrow_mut() = Some(DevToolsRequest::Show);
1658    }
1659
1660    /// Request that the built-in devtools panel becomes hidden.
1661    pub fn hide_devtools(&self) {
1662        *self.env.devtools_request.borrow_mut() = Some(DevToolsRequest::Hide);
1663    }
1664
1665    /// Request that the built-in devtools panel toggles visibility.
1666    pub fn toggle_devtools(&self) {
1667        *self.env.devtools_request.borrow_mut() = Some(DevToolsRequest::Toggle);
1668    }
1669
1670    pub(crate) fn take_focus_request(&self) -> Option<FocusRequest> {
1671        self.env.focus_request.borrow_mut().take()
1672    }
1673
1674    pub(crate) fn take_full_repaint_request(&self) -> bool {
1675        self.env.full_repaint.replace(false)
1676    }
1677
1678    pub(crate) fn take_copy_feedback_requests(&self) -> Vec<CopyFeedbackRequest> {
1679        self.env.take_copy_feedback_requests()
1680    }
1681
1682    pub(crate) fn take_devtools_request(&self) -> Option<DevToolsRequest> {
1683        self.env.devtools_request.borrow_mut().take()
1684    }
1685
1686    /// Queue a UI snapshot write to `path` after the next render.
1687    ///
1688    /// Uses JSON when `path` ends with `.json` and the `ui-snapshot-json` feature is
1689    /// enabled, or PNG when `path` ends with `.png` and the `ui-snapshot-png` feature
1690    /// is enabled; otherwise writes markdown.
1691    ///
1692    /// A pending request replaces any earlier one (last writer wins). Triggers a full
1693    /// repaint so idle apps still deliver the snapshot.
1694    pub fn request_ui_snapshot_to(&self, path: impl AsRef<std::path::Path>) {
1695        let path = path.as_ref().to_path_buf();
1696        let format = crate::ui_snapshot::UiSnapshotFileFormat::from_path(&path);
1697        *self.env.ui_snapshot_request.borrow_mut() =
1698            Some(crate::ui_snapshot::UiSnapshotRequest::Write { path, format });
1699        self.request_full_repaint();
1700    }
1701
1702    /// Queue delivery of a UI snapshot into `slot` after the next render.
1703    ///
1704    /// A pending request replaces any earlier one (last writer wins). Triggers a full
1705    /// repaint so idle apps still deliver the snapshot.
1706    pub fn request_ui_snapshot_to_slot(&self, slot: &crate::ui_snapshot::UiSnapshotSlot) {
1707        *self.env.ui_snapshot_request.borrow_mut() = Some(
1708            crate::ui_snapshot::UiSnapshotRequest::Deliver(slot.shared()),
1709        );
1710        self.request_full_repaint();
1711    }
1712
1713    pub(crate) fn take_ui_snapshot_request(&self) -> Option<crate::ui_snapshot::UiSnapshotRequest> {
1714        self.env.ui_snapshot_request.borrow_mut().take()
1715    }
1716
1717    pub(crate) fn should_quit(&self) -> bool {
1718        self.env.quit.get()
1719    }
1720}
1721
1722#[cfg(test)]
1723mod tests;