use rustc_hash::FxHashMap;
use std::any::TypeId;
use std::cell::{Cell, RefCell};
use std::marker::PhantomData;
use std::sync::Arc;
use crate::app::context::SurfaceMode;
use crate::app::input::command_registry::CommandEntry;
use crate::app::input::command_registry::CommandRegistry;
use crate::callback::{CancellationToken, CommandLink, CommandTx, Dispatcher, Link, ScopeId};
use crate::core::context_value::ContextValue;
use crate::core::element::{Element, Key};
use crate::core::event::KeyEvent;
use crate::core::node::{NodeId, NodeKind, NodeTree};
use crate::core::runtime_env::{
DevToolsRequest, MemoDependency, MemoDependencySnapshot, RuntimeEnv, TranscriptEntry,
};
use crate::style::{HostTerminalColors, Rect, RichText, Theme, ThemeExtension};
#[non_exhaustive]
pub struct Command {
action: Box<dyn CommandAction>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum TaskPolicy {
QueueAll,
DropIfRunning,
LatestOnly,
}
mod task_policy;
use task_policy::Task;
#[cfg(not(target_arch = "wasm32"))]
mod executor_native;
#[cfg(target_arch = "wasm32")]
mod executor_wasm;
#[cfg(not(target_arch = "wasm32"))]
use executor_native::TaskExecutor;
#[cfg(target_arch = "wasm32")]
use executor_wasm::TaskExecutor;
impl std::fmt::Debug for Command {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Command").finish_non_exhaustive()
}
}
impl Command {
pub fn new(action: impl FnOnce() + 'static) -> Self {
Self {
action: Box::new(RunAction(Some(action))),
}
}
pub fn spawn<Msg, F>(f: F) -> Self
where
Msg: Send + 'static,
F: FnOnce(CommandLink<Msg>) + Send + 'static,
{
Self {
action: Box::new(SpawnAction::<Msg, F> {
f: Some(f),
_marker: PhantomData,
}),
}
}
pub fn spawn_keyed<Msg, F>(key: impl Into<Arc<str>>, policy: TaskPolicy, f: F) -> Self
where
Msg: Send + 'static,
F: FnOnce(CommandLink<Msg>) + Send + 'static,
{
Self {
action: Box::new(SpawnKeyedAction::<Msg, F> {
key: key.into(),
policy,
f: Some(f),
_marker: PhantomData,
}),
}
}
pub(crate) fn run(self, runtime: CommandRuntime) {
self.action.run(runtime);
}
}
pub(crate) struct CommandRuntime {
pub(crate) scope: ScopeId,
pub(crate) tx: CommandTx,
}
trait CommandAction {
fn run(self: Box<Self>, runtime: CommandRuntime);
}
struct RunAction<F>(Option<F>);
impl<F> CommandAction for RunAction<F>
where
F: FnOnce() + 'static,
{
fn run(mut self: Box<Self>, _runtime: CommandRuntime) {
if let Some(f) = self.0.take() {
f();
}
}
}
struct SpawnAction<Msg, F> {
f: Option<F>,
_marker: PhantomData<fn(Msg)>,
}
struct SpawnKeyedAction<Msg, F> {
key: Arc<str>,
policy: TaskPolicy,
f: Option<F>,
_marker: PhantomData<fn(Msg)>,
}
impl<Msg, F> CommandAction for SpawnAction<Msg, F>
where
Msg: Send + 'static,
F: FnOnce(CommandLink<Msg>) + Send + 'static,
{
fn run(mut self: Box<Self>, runtime: CommandRuntime) {
let Some(f) = self.f.take() else {
return;
};
let token = CancellationToken::default();
let link = CommandLink::new(runtime.scope, runtime.tx, token.clone());
TaskExecutor::global().execute(Task::with_token(move || f(link), token));
}
}
impl<Msg, F> CommandAction for SpawnKeyedAction<Msg, F>
where
Msg: Send + 'static,
F: FnOnce(CommandLink<Msg>) + Send + 'static,
{
fn run(mut self: Box<Self>, runtime: CommandRuntime) {
let Some(f) = self.f.take() else {
return;
};
let key = Arc::clone(&self.key);
let policy = self.policy;
let token = CancellationToken::default();
let link = CommandLink::new(runtime.scope, runtime.tx, token.clone());
TaskExecutor::global().execute_keyed(key, policy, Task::with_token(move || f(link), token));
}
}
impl<Msg: 'static> Link<Msg> {
pub fn command<F>(&self, f: F) -> Command
where
Msg: Send + 'static,
F: FnOnce(CommandLink<Msg>) + Send + 'static,
{
Command::spawn::<Msg, F>(f)
}
pub fn command_keyed<F>(&self, key: impl Into<Arc<str>>, policy: TaskPolicy, f: F) -> Command
where
Msg: Send + 'static,
F: FnOnce(CommandLink<Msg>) + Send + 'static,
{
Command::spawn_keyed::<Msg, F>(key, policy, f)
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub(crate) enum UpdateLevel {
#[default]
None,
Paint,
Layout,
Full,
}
pub struct Update {
pub dirty: bool,
pub(crate) level: UpdateLevel,
pub command: Option<Command>,
}
impl Update {
pub fn paint() -> Self {
Self {
dirty: true,
level: UpdateLevel::Paint,
command: None,
}
}
pub fn layout() -> Self {
Self {
dirty: true,
level: UpdateLevel::Layout,
command: None,
}
}
pub fn full() -> Self {
Self {
dirty: true,
level: UpdateLevel::Full,
command: None,
}
}
pub fn command_only(command: Command) -> Self {
Self {
dirty: false,
level: UpdateLevel::None,
command: Some(command),
}
}
pub fn none() -> Self {
Self {
dirty: false,
level: UpdateLevel::None,
command: None,
}
}
pub fn with_command(command: impl Into<Option<Command>>) -> Self {
match command.into() {
Some(cmd) => Self {
dirty: true,
level: UpdateLevel::Full,
command: Some(cmd),
},
None => Self::full(),
}
}
pub(crate) fn level(&self) -> UpdateLevel {
self.level
}
}
pub struct KeyUpdate {
pub handled: bool,
pub update: Update,
}
impl KeyUpdate {
pub fn handled(update: Update) -> Self {
Self {
handled: true,
update,
}
}
pub fn unhandled(update: Update) -> Self {
Self {
handled: false,
update,
}
}
}
pub trait Component: Sized + 'static {
type Message: 'static;
type Properties: Clone + PartialEq + 'static;
type State: 'static;
fn create_state(&self, props: &Self::Properties) -> Self::State;
fn memo_key(&self, _props: &Self::Properties, _ctx: &Context<Self>) -> Option<u64> {
None
}
fn init(&mut self, _ctx: &mut Context<Self>) -> Option<Command> {
None
}
fn view(&self, ctx: &Context<Self>) -> Element;
fn on_key(&mut self, _key: KeyEvent, _ctx: &mut Context<Self>) -> KeyUpdate {
KeyUpdate::unhandled(Update::none())
}
fn update(&mut self, msg: Self::Message, ctx: &mut Context<Self>) -> Update;
fn on_props_changed(
&mut self,
_old_props: &Self::Properties,
_ctx: &mut Context<Self>,
) -> Update {
Update::none()
}
fn unmount(&mut self, _ctx: &mut Context<Self>) {}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Breakpoint {
Small,
Medium,
Large,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct ScrollbarVisibility {
pub v: bool,
pub h: bool,
}
#[derive(Default)]
struct NodeChainState {
current_node: Cell<Option<NodeId>>,
chain_scopes: RefCell<Vec<ScopeId>>,
chain_keys: RefCell<Vec<Key>>,
generation: Cell<u64>,
}
impl NodeChainState {
fn node_id(&self) -> Option<NodeId> {
self.current_node.get()
}
fn has_within_scope(&self, scope: ScopeId) -> bool {
self.chain_scopes.borrow().contains(&scope)
}
fn has_within_key(&self, key: &Key) -> bool {
self.chain_keys
.borrow()
.iter()
.any(|candidate| candidate == key)
}
fn push_scope_if_missing(scopes: &mut Vec<ScopeId>, scope: ScopeId) {
if !scopes.contains(&scope) {
scopes.push(scope);
}
}
fn push_key_if_missing(keys: &mut Vec<Key>, key: &Key) {
if !keys.iter().any(|candidate| candidate == key) {
keys.push(key.clone());
}
}
fn replace_snapshot(&self, node: Option<NodeId>, scopes: Vec<ScopeId>, keys: Vec<Key>) {
let mut scope_ref = self.chain_scopes.borrow_mut();
let mut key_ref = self.chain_keys.borrow_mut();
let changed = self.current_node.get() != node || *scope_ref != scopes || *key_ref != keys;
self.current_node.set(node);
*scope_ref = scopes;
*key_ref = keys;
if changed {
self.generation
.set(self.generation.get().wrapping_add(1).max(1));
}
}
fn update_chain(&self, tree: &NodeTree, node: Option<NodeId>) {
let mut scopes = Vec::new();
let mut keys = Vec::new();
if let Some(mut cur) = node {
Self::push_scope_if_missing(&mut scopes, ScopeId(1));
loop {
if !tree.is_valid(cur) {
break;
}
let node_ref = tree.node(cur);
if let Some(k) = &node_ref.key {
Self::push_key_if_missing(&mut keys, k);
}
if let NodeKind::Group(group) = &node_ref.kind {
Self::push_scope_if_missing(&mut scopes, group.scope);
}
let Some(parent) = node_ref.parent else {
break;
};
cur = parent;
}
}
self.replace_snapshot(node, scopes, keys);
}
fn generation(&self) -> u64 {
self.generation.get()
}
}
#[derive(Default)]
pub(crate) struct FocusContext {
inner: NodeChainState,
}
impl FocusContext {
pub(crate) fn update_from_tree(
&self,
tree: &NodeTree,
focused: Option<NodeId>,
focused_key: Option<&Key>,
) {
let mut cur = focused.filter(|id| tree.is_valid(*id));
if cur.is_none()
&& let Some(key) = focused_key
{
if let Some(id) = tree
.iter()
.find(|n| n.key.as_ref() == Some(key))
.map(|n| n.id)
{
cur = Some(id);
} else {
self.inner
.replace_snapshot(None, Vec::new(), vec![key.clone()]);
return;
}
}
self.inner.update_chain(tree, cur);
}
pub(crate) fn focused_node_id(&self) -> Option<NodeId> {
self.inner.node_id()
}
pub(crate) fn has_focus_within_scope(&self, scope: ScopeId) -> bool {
self.inner.has_within_scope(scope)
}
pub(crate) fn has_focus_within_key(&self, key: &Key) -> bool {
self.inner.has_within_key(key)
}
pub(crate) fn generation(&self) -> u64 {
self.inner.generation()
}
}
#[derive(Default)]
pub(crate) struct HoverContext {
inner: NodeChainState,
}
impl HoverContext {
pub(crate) fn update_from_tree(&self, tree: &NodeTree, hovered: Option<NodeId>) {
let cur = hovered.filter(|id| tree.is_valid(*id));
self.inner.update_chain(tree, cur);
}
pub(crate) fn hovered_node_id(&self) -> Option<NodeId> {
self.inner.node_id()
}
pub(crate) fn has_hover_within_scope(&self, scope: ScopeId) -> bool {
self.inner.has_within_scope(scope)
}
pub(crate) fn has_hover_within_key(&self, key: &Key) -> bool {
self.inner.has_within_key(key)
}
pub(crate) fn generation(&self) -> u64 {
self.inner.generation()
}
}
#[derive(Default)]
pub(crate) struct ScrollContext {
by_key: RefCell<FxHashMap<Key, ScrollbarVisibility>>,
text_area_metrics_by_key: RefCell<FxHashMap<Key, crate::widgets::TextAreaMetrics>>,
generation: Cell<u64>,
}
impl ScrollContext {
pub(crate) fn update_from_tree(&self, tree: &NodeTree) {
let mut map = self.by_key.borrow_mut();
let prev = std::mem::take(&mut *map);
let mut metrics_map = self.text_area_metrics_by_key.borrow_mut();
let prev_metrics = std::mem::take(&mut *metrics_map);
for node in tree.iter_with_overlays() {
if let (Some(key), NodeKind::TextArea(text_area)) = (&node.key, &node.kind) {
let metrics = text_area.metrics(node.rect);
map.insert(key.clone(), metrics.scrollbars);
metrics_map.insert(key.clone(), metrics);
}
}
if *map != prev || *metrics_map != prev_metrics {
self.generation
.set(self.generation.get().wrapping_add(1).max(1));
}
}
pub(crate) fn get(&self, key: &Key) -> Option<ScrollbarVisibility> {
self.by_key.borrow().get(key).copied()
}
pub(crate) fn text_area_metrics(&self, key: &Key) -> Option<crate::widgets::TextAreaMetrics> {
self.text_area_metrics_by_key.borrow().get(key).cloned()
}
pub(crate) fn generation(&self) -> u64 {
self.generation.get()
}
}
pub struct Context<C: Component> {
pub state: C::State,
pub props: C::Properties,
viewport: Rect,
link: Link<C::Message>,
env: RuntimeEnv,
scope: ScopeId,
}
impl<C: Component> Context<C> {
pub(crate) fn new(
component: &C,
scope: ScopeId,
dispatcher: Dispatcher,
props: C::Properties,
env: RuntimeEnv,
viewport: Rect,
) -> Self {
let state = component.create_state(&props);
Self {
state,
props,
viewport,
link: Link::new(scope, dispatcher),
env,
scope,
}
}
pub fn link(&self) -> &Link<C::Message> {
&self.link
}
pub fn toast(&self) -> crate::overlay::ToastHandle {
crate::overlay::ToastHandle::new(self.env.overlay_manager.clone())
}
pub fn clipboard(&self) -> crate::clipboard::ClipboardHandle {
crate::clipboard::ClipboardHandle::new(
self.env.clipboard.clone(),
self.env.clipboard_config.clone(),
)
}
pub fn command_registry(&self) -> CommandRegistry {
self.env.command_registry.clone()
}
pub fn register_command(&self, entry: CommandEntry) {
self.env
.command_registry
.register_for_scope(self.scope, entry);
}
pub fn viewport(&self) -> Rect {
self.env.note_memo_dependency(MemoDependency::Viewport);
self.viewport
}
pub fn theme(&self) -> Theme {
self.env.note_memo_dependency(MemoDependency::Theme);
self.env.active_theme.borrow().clone()
}
pub fn theme_extension<T>(&self) -> Option<T>
where
T: ThemeExtension,
{
self.env.note_memo_dependency(MemoDependency::Theme);
self.env.active_theme.borrow().extension_cloned::<T>()
}
pub fn use_context<T>(&self) -> Option<T>
where
T: ContextValue,
{
self.env
.note_memo_dependency(MemoDependency::Context(TypeId::of::<T>()));
self.env
.contexts
.borrow()
.get(&TypeId::of::<T>())
.and_then(|value| value.as_ref().downcast_ref::<T>())
.cloned()
}
pub fn context<T>(&self) -> Option<T>
where
T: ContextValue,
{
self.use_context::<T>()
}
pub fn is_inline(&self) -> bool {
self.env.surface_mode.is_inline()
}
pub fn surface_mode(&self) -> SurfaceMode {
self.env.surface_mode
}
pub fn effect_phase(&self) -> u64 {
self.env.effect_phase.get()
}
pub fn host_terminal_colors(&self) -> Option<HostTerminalColors> {
self.env.host_terminal_colors()
}
pub fn host_terminal_color_generation(&self) -> u64 {
self.env.host_terminal_color_generation()
}
pub fn request_host_terminal_color_refresh(&self) {
self.env.request_host_terminal_color_refresh();
}
pub fn mouse_capture_enabled(&self) -> bool {
self.env.note_memo_dependency(MemoDependency::MouseCapture);
self.env.mouse_capture.get()
}
pub fn set_mouse_capture(&self, enabled: bool) {
if self.env.mouse_capture.get() != enabled {
self.env.mouse_capture.set(enabled);
self.env.mouse_capture_generation.set(
self.env
.mouse_capture_generation
.get()
.wrapping_add(1)
.max(1),
);
}
}
pub fn toggle_mouse_capture(&self) -> bool {
let next = !self.env.mouse_capture.get();
self.set_mouse_capture(next);
next
}
pub fn append_transcript_lines<I, L>(&mut self, lines: I)
where
I: IntoIterator<Item = L>,
L: Into<RichText>,
{
if !matches!(self.env.surface_mode, SurfaceMode::InlineTranscript { .. }) {
return;
}
let lines: Vec<RichText> = lines.into_iter().map(Into::into).collect();
if lines.is_empty() {
return;
}
self.env
.transcript_history
.borrow_mut()
.push(TranscriptEntry::Lines(lines.clone()));
self.env
.pending_transcript_entries
.borrow_mut()
.push_back(TranscriptEntry::Lines(lines));
}
pub fn append_transcript_element(&mut self, element: impl Into<Element>) {
if !matches!(self.env.surface_mode, SurfaceMode::InlineTranscript { .. }) {
return;
}
let element = element.into();
if element.contains_unexpanded_component() {
crate::debug::internal_log!(
"[tui-lipan] append_transcript_element ignored an element containing Component nodes"
);
return;
}
self.env
.transcript_history
.borrow_mut()
.push(TranscriptEntry::Element(Box::new(element.clone())));
self.env
.pending_transcript_entries
.borrow_mut()
.push_back(TranscriptEntry::Element(Box::new(element)));
}
pub fn has_focus_within(&self) -> bool {
self.env.note_memo_dependency(MemoDependency::Focus);
self.env.focus.has_focus_within_scope(self.scope)
}
pub fn has_focus_within_key(&self, key: impl Into<Key>) -> bool {
let key = key.into();
self.env.note_memo_dependency(MemoDependency::Focus);
self.env.focus.has_focus_within_key(&key)
}
pub fn text_area_scrollbars(&self, key: impl Into<Key>) -> ScrollbarVisibility {
let key = key.into();
self.env.note_memo_dependency(MemoDependency::Scroll);
self.text_area_metrics(key.clone())
.map(|metrics| metrics.scrollbars)
.or_else(|| self.env.scroll.get(&key))
.unwrap_or_default()
}
pub fn text_area_metrics(
&self,
key: impl Into<Key>,
) -> Option<crate::widgets::TextAreaMetrics> {
let key = key.into();
self.env.note_memo_dependency(MemoDependency::Scroll);
self.env.scroll.text_area_metrics(&key)
}
pub fn has_hover_within(&self) -> bool {
self.env.note_memo_dependency(MemoDependency::Hover);
self.env.hover.has_hover_within_scope(self.scope)
}
pub fn has_hover_within_key(&self, key: impl Into<Key>) -> bool {
let key = key.into();
self.env.note_memo_dependency(MemoDependency::Hover);
self.env.hover.has_hover_within_key(&key)
}
pub fn focused_node_id(&self) -> Option<NodeId> {
self.env.note_memo_dependency(MemoDependency::Focus);
self.env.focus.focused_node_id()
}
pub fn hovered_node_id(&self) -> Option<NodeId> {
self.env.note_memo_dependency(MemoDependency::Hover);
self.env.hover.hovered_node_id()
}
pub fn transition<T>(
&self,
key: impl Into<Key>,
target: T,
config: crate::animation::TransitionConfig,
) -> T
where
T: crate::animation::Lerp + PartialEq + 'static,
{
let key = key.into();
self.env.note_memo_dependency(MemoDependency::Transition);
self.env.animations.transition(key, target, config)
}
pub fn breakpoint(&self, medium: u16, large: u16) -> Breakpoint {
let (medium, large) = if medium <= large {
(medium, large)
} else {
(large, medium)
};
self.env.note_memo_dependency(MemoDependency::Viewport);
let w = self.viewport.w;
if w < medium {
Breakpoint::Small
} else if w < large {
Breakpoint::Medium
} else {
Breakpoint::Large
}
}
pub(crate) fn env(&self) -> &RuntimeEnv {
&self.env
}
pub(crate) fn set_viewport(&mut self, viewport: Rect) {
self.viewport = viewport;
}
pub(crate) fn set_active_theme(&mut self, theme: Theme) {
let mut active_theme = self.env.active_theme.borrow_mut();
if *active_theme != theme {
*active_theme = theme;
self.env.active_theme_generation.set(
self.env
.active_theme_generation
.get()
.wrapping_add(1)
.max(1),
);
}
}
pub(crate) fn set_contexts(
&mut self,
contexts: rustc_hash::FxHashMap<TypeId, std::sync::Arc<dyn std::any::Any>>,
generations: rustc_hash::FxHashMap<TypeId, u64>,
) {
*self.env.contexts.borrow_mut() = contexts;
*self.env.context_generations.borrow_mut() = generations;
}
pub(crate) fn memo_key(&self, component: &C) -> Option<u64> {
component.memo_key(&self.props, self)
}
pub(crate) fn begin_memo_dependency_capture(&self) {
self.env.begin_memo_dependency_capture();
}
pub(crate) fn finish_memo_dependency_capture(&self) -> MemoDependencySnapshot {
self.env.finish_memo_dependency_capture(self.viewport)
}
pub(crate) fn memo_dependencies_match(&self, snapshot: &MemoDependencySnapshot) -> bool {
snapshot.matches(&self.env, self.viewport)
}
pub fn quit(&mut self) {
self.env.quit.set(true);
}
pub fn request_focus(&mut self, key: impl Into<Key>) {
*self.env.focus_request.borrow_mut() = Some(key.into());
}
pub fn request_full_repaint(&self) {
self.env.full_repaint.set(true);
}
pub fn show_devtools(&self) {
*self.env.devtools_request.borrow_mut() = Some(DevToolsRequest::Show);
}
pub fn hide_devtools(&self) {
*self.env.devtools_request.borrow_mut() = Some(DevToolsRequest::Hide);
}
pub fn toggle_devtools(&self) {
*self.env.devtools_request.borrow_mut() = Some(DevToolsRequest::Toggle);
}
pub(crate) fn take_focus_request(&self) -> Option<Key> {
self.env.focus_request.borrow_mut().take()
}
pub(crate) fn take_full_repaint_request(&self) -> bool {
self.env.full_repaint.replace(false)
}
pub(crate) fn take_devtools_request(&self) -> Option<DevToolsRequest> {
self.env.devtools_request.borrow_mut().take()
}
pub fn request_ui_snapshot_to(&self, path: impl AsRef<std::path::Path>) {
let path = path.as_ref().to_path_buf();
let extension = path.extension();
let format = if extension.is_some_and(|ext| ext == "json" || ext == "JSON") {
#[cfg(feature = "ui-snapshot-json")]
{
crate::ui_snapshot::UiSnapshotFileFormat::Json
}
#[cfg(not(feature = "ui-snapshot-json"))]
{
crate::ui_snapshot::UiSnapshotFileFormat::Markdown
}
} else if extension.is_some_and(|ext| ext == "png" || ext == "PNG") {
#[cfg(feature = "ui-snapshot-png")]
{
crate::ui_snapshot::UiSnapshotFileFormat::Png
}
#[cfg(not(feature = "ui-snapshot-png"))]
{
crate::ui_snapshot::UiSnapshotFileFormat::Markdown
}
} else {
crate::ui_snapshot::UiSnapshotFileFormat::Markdown
};
*self.env.ui_snapshot_request.borrow_mut() =
Some(crate::ui_snapshot::UiSnapshotRequest::Write { path, format });
self.request_full_repaint();
}
pub fn request_ui_snapshot_to_slot(&self, slot: &crate::ui_snapshot::UiSnapshotSlot) {
*self.env.ui_snapshot_request.borrow_mut() = Some(
crate::ui_snapshot::UiSnapshotRequest::Deliver(slot.shared()),
);
self.request_full_repaint();
}
pub(crate) fn take_ui_snapshot_request(&self) -> Option<crate::ui_snapshot::UiSnapshotRequest> {
self.env.ui_snapshot_request.borrow_mut().take()
}
pub(crate) fn should_quit(&self) -> bool {
self.env.quit.get()
}
}
#[cfg(test)]
mod tests;