use std::any::{Any, TypeId};
use std::panic::Location;
use std::time::{Duration, Instant};
use ratatui_core::{buffer::Buffer, layout::Rect};
use crate::component::{Component, Tracked};
use crate::context::{ContextMap, ProvidedContexts};
use crate::element::Elements;
use crate::hooks::Hooks;
use crate::insets::Insets;
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub struct NodeId(pub(crate) usize);
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum Layout {
#[default]
Vertical,
Horizontal,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum WidthConstraint {
Fixed(u16),
#[default]
Fill,
}
pub(crate) trait AnyComponent: Send + Sync {
fn props_as_any(&self) -> &dyn Any;
fn render_erased(&self, area: Rect, buf: &mut Buffer, state: &dyn Any);
fn desired_height_erased(&self, width: u16, state: &dyn Any) -> Option<u16>;
fn handle_event_capture_erased(
&self,
event: &crossterm::event::Event,
tracked_state: &mut dyn Any,
) -> crate::component::EventResult;
fn handle_event_erased(
&self,
event: &crossterm::event::Event,
tracked_state: &mut dyn Any,
) -> crate::component::EventResult;
fn cursor_position_erased(&self, area: Rect, state: &dyn Any) -> Option<(u16, u16)>;
fn is_focusable_erased(&self, state: &dyn Any) -> bool;
fn content_inset_erased(&self, state: &dyn Any) -> Insets;
fn width_constraint_erased(&self) -> WidthConstraint;
fn update_erased(
&self,
tracked_state: &mut dyn Any,
context: &ContextMap,
children: Elements,
) -> (LifecycleOutput, Elements);
}
impl<C: Component> AnyComponent for C {
fn props_as_any(&self) -> &dyn Any {
Component::props_as_any(self)
}
fn render_erased(&self, area: Rect, buf: &mut Buffer, state: &dyn Any) {
let state = state
.downcast_ref::<C::State>()
.expect("state type mismatch in render_erased");
self.render(area, buf, state);
}
fn desired_height_erased(&self, width: u16, state: &dyn Any) -> Option<u16> {
let state = state
.downcast_ref::<C::State>()
.expect("state type mismatch in desired_height_erased");
self.desired_height(width, state)
}
fn handle_event_capture_erased(
&self,
event: &crossterm::event::Event,
tracked_state: &mut dyn Any,
) -> crate::component::EventResult {
let tracked = tracked_state
.downcast_mut::<Tracked<C::State>>()
.expect("state type mismatch in handle_event_capture_erased");
self.handle_event_capture(event, tracked)
}
fn handle_event_erased(
&self,
event: &crossterm::event::Event,
tracked_state: &mut dyn Any,
) -> crate::component::EventResult {
let tracked = tracked_state
.downcast_mut::<Tracked<C::State>>()
.expect("state type mismatch in handle_event_erased");
self.handle_event(event, tracked)
}
fn cursor_position_erased(&self, area: Rect, state: &dyn Any) -> Option<(u16, u16)> {
let state = state
.downcast_ref::<C::State>()
.expect("state type mismatch in cursor_position_erased");
self.cursor_position(area, state)
}
fn is_focusable_erased(&self, state: &dyn Any) -> bool {
let state = state
.downcast_ref::<C::State>()
.expect("state type mismatch in is_focusable_erased");
self.is_focusable(state)
}
fn content_inset_erased(&self, state: &dyn Any) -> Insets {
let state = state
.downcast_ref::<C::State>()
.expect("state type mismatch in content_inset_erased");
self.content_inset(state)
}
fn width_constraint_erased(&self) -> WidthConstraint {
self.width_constraint()
}
fn update_erased(
&self,
tracked_state: &mut dyn Any,
context: &ContextMap,
children: Elements,
) -> (LifecycleOutput, Elements) {
let tracked = tracked_state
.downcast_mut::<Tracked<C::State>>()
.expect("state type mismatch in update_erased");
let (hooks_output, elements) = {
let state: &C::State = tracked;
let mut hooks = Hooks::<C, C::State>::new();
let elements = self.update(&mut hooks, state, children);
(hooks.decompose(), elements)
};
let props_any: &dyn Any = Component::props_as_any(self);
for consumer in hooks_output.consumers {
consumer(context, props_any, tracked);
}
(
LifecycleOutput {
effects: hooks_output.effects,
autofocus: hooks_output.autofocus,
focus_scope: hooks_output.focus_scope,
provided: hooks_output.provided,
focusable: hooks_output.focusable,
cursor_hook: hooks_output.cursor_hook,
event_hook: hooks_output.event_hook,
capture_hook: hooks_output.capture_hook,
layout: hooks_output.layout,
width_constraint: hooks_output.width_constraint,
height_hint: hooks_output.height_hint,
desired_height_hook: hooks_output.desired_height_hook,
},
elements,
)
}
}
pub(crate) trait AnyTrackedState: Send + Sync {
#[allow(dead_code)]
fn as_any(&self) -> &dyn Any;
fn as_any_mut(&mut self) -> &mut dyn Any;
#[allow(dead_code)]
fn is_dirty(&self) -> bool;
fn clear_dirty(&mut self);
fn inner_as_any(&self) -> &dyn Any;
}
impl<S: Send + Sync + 'static> AnyTrackedState for Tracked<S> {
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn is_dirty(&self) -> bool {
Tracked::is_dirty(self)
}
fn clear_dirty(&mut self) {
Tracked::clear_dirty(self);
}
fn inner_as_any(&self) -> &dyn Any {
use std::ops::Deref;
self.deref() as &dyn Any
}
}
pub(crate) trait AnyEffectHandler: Send + Sync {
fn call(&self, component: &dyn Any, tracked_state: &mut dyn Any);
}
type EffectHandlerFn<P, S> = Box<dyn Fn(&P, &mut Tracked<S>) + Send + Sync>;
pub(crate) struct TypedEffectHandler<P: 'static, S: 'static> {
pub(crate) handler: EffectHandlerFn<P, S>,
}
impl<P: Send + Sync + 'static, S: Send + Sync + 'static> AnyEffectHandler
for TypedEffectHandler<P, S>
{
fn call(&self, component: &dyn Any, tracked_state: &mut dyn Any) {
let props = component
.downcast_ref::<P>()
.expect("props type mismatch in effect handler");
let tracked = tracked_state
.downcast_mut::<Tracked<S>>()
.expect("state type mismatch in effect handler");
(self.handler)(props, tracked);
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub(crate) struct CallSite {
pub file: &'static str,
pub line: u32,
pub column: u32,
}
impl CallSite {
pub fn from_location(loc: &'static Location<'static>) -> Self {
Self {
file: loc.file(),
line: loc.line(),
column: loc.column(),
}
}
}
pub(crate) enum EffectKind {
Interval {
interval: Duration,
last_tick: Instant,
},
OnMount,
OnUnmount,
}
pub(crate) trait AnyEventHook: Send + Sync {
fn call(
&self,
event: &crossterm::event::Event,
component: &dyn Any,
tracked_state: &mut dyn Any,
) -> crate::component::EventResult;
}
type EventHookFn<P, S> = Box<
dyn Fn(&crossterm::event::Event, &P, &mut Tracked<S>) -> crate::component::EventResult
+ Send
+ Sync,
>;
pub(crate) struct TypedEventHook<P: 'static, S: 'static> {
pub(crate) handler: EventHookFn<P, S>,
}
impl<P: Send + Sync + 'static, S: Send + Sync + 'static> AnyEventHook for TypedEventHook<P, S> {
fn call(
&self,
event: &crossterm::event::Event,
component: &dyn Any,
tracked_state: &mut dyn Any,
) -> crate::component::EventResult {
let props = component
.downcast_ref::<P>()
.expect("props type mismatch in event hook");
let tracked = tracked_state
.downcast_mut::<Tracked<S>>()
.expect("state type mismatch in event hook");
(self.handler)(event, props, tracked)
}
}
pub(crate) trait AnyCursorHook: Send + Sync {
fn call(&self, area: Rect, component: &dyn Any, state: &dyn Any) -> Option<(u16, u16)>;
}
type CursorHookFn<P, S> = Box<dyn Fn(Rect, &P, &S) -> Option<(u16, u16)> + Send + Sync>;
pub(crate) struct TypedCursorHook<P: 'static, S: 'static> {
pub(crate) handler: CursorHookFn<P, S>,
}
impl<P: Send + Sync + 'static, S: Send + Sync + 'static> AnyCursorHook for TypedCursorHook<P, S> {
fn call(&self, area: Rect, component: &dyn Any, state: &dyn Any) -> Option<(u16, u16)> {
let props = component
.downcast_ref::<P>()
.expect("props type mismatch in cursor hook");
let state = state
.downcast_ref::<S>()
.expect("state type mismatch in cursor hook");
(self.handler)(area, props, state)
}
}
pub(crate) trait AnyDesiredHeightHook: Send + Sync {
fn call(&self, width: u16, component: &dyn Any, state: &dyn Any) -> Option<u16>;
}
type DesiredHeightHookFn<P, S> = Box<dyn Fn(u16, &P, &S) -> Option<u16> + Send + Sync>;
pub(crate) struct TypedDesiredHeightHook<P: 'static, S: 'static> {
pub(crate) handler: DesiredHeightHookFn<P, S>,
}
impl<P: Send + Sync + 'static, S: Send + Sync + 'static> AnyDesiredHeightHook
for TypedDesiredHeightHook<P, S>
{
fn call(&self, width: u16, component: &dyn Any, state: &dyn Any) -> Option<u16> {
let props = component
.downcast_ref::<P>()
.expect("props type mismatch in desired_height hook");
let state = state
.downcast_ref::<S>()
.expect("state type mismatch in desired_height hook");
(self.handler)(width, props, state)
}
}
pub(crate) struct LifecycleOutput {
pub effects: Vec<Effect>,
pub autofocus: bool,
pub focus_scope: bool,
pub provided: ProvidedContexts,
pub focusable: Option<bool>,
pub cursor_hook: Option<Box<dyn AnyCursorHook>>,
pub event_hook: Option<Box<dyn AnyEventHook>>,
pub capture_hook: Option<Box<dyn AnyEventHook>>,
pub layout: Option<Layout>,
pub width_constraint: Option<WidthConstraint>,
pub height_hint: Option<u16>,
pub desired_height_hook: Option<Box<dyn AnyDesiredHeightHook>>,
}
pub(crate) struct Effect {
pub handler: Box<dyn AnyEffectHandler>,
pub kind: EffectKind,
pub call_site: CallSite,
}
pub(crate) struct Node {
pub component: Box<dyn AnyComponent>,
pub state: Box<dyn AnyTrackedState>,
pub frozen: bool,
pub cached_buffer: Option<Buffer>,
pub last_height: Option<u16>,
pub children: Vec<NodeId>,
pub parent: Option<NodeId>,
pub force_dirty: bool,
pub probe_rendered: bool,
pub layout_rect: Option<Rect>,
pub element_type_id: Option<TypeId>,
pub key: Option<String>,
pub layout: Layout,
pub width_constraint: WidthConstraint,
pub autofocus: bool,
pub focus_scope: bool,
pub hook_focusable: Option<bool>,
pub hook_cursor: Option<Box<dyn AnyCursorHook>>,
pub hook_event: Option<Box<dyn AnyEventHook>>,
pub hook_capture: Option<Box<dyn AnyEventHook>>,
pub hook_height_hint: Option<u16>,
pub hook_desired_height: Option<Box<dyn AnyDesiredHeightHook>>,
pub has_slot: bool,
}
impl Node {
pub fn new<C: Component>(component: C) -> Self {
let state: Box<dyn AnyTrackedState> =
Box::new(Tracked::new(component.initial_state().unwrap_or_default()));
Self {
component: Box::new(component),
state,
frozen: false,
cached_buffer: None,
last_height: None,
children: Vec::new(),
parent: None,
force_dirty: false,
probe_rendered: false,
layout_rect: None,
element_type_id: None,
key: None,
layout: Layout::default(),
width_constraint: WidthConstraint::default(),
autofocus: false,
focus_scope: false,
hook_focusable: None,
hook_cursor: None,
hook_event: None,
hook_capture: None,
hook_height_hint: None,
hook_desired_height: None,
has_slot: false,
}
}
pub fn is_container(&self) -> bool {
!self.children.is_empty()
}
}
pub(crate) struct NodeArena {
slots: Vec<Option<Node>>,
free: Vec<usize>,
}
impl NodeArena {
pub fn new() -> Self {
Self {
slots: Vec::new(),
free: Vec::new(),
}
}
pub fn alloc(&mut self, node: Node) -> NodeId {
if let Some(idx) = self.free.pop() {
self.slots[idx] = Some(node);
NodeId(idx)
} else {
let idx = self.slots.len();
self.slots.push(Some(node));
NodeId(idx)
}
}
pub fn free(&mut self, id: NodeId) {
assert!(
self.slots[id.0].is_some(),
"double free of NodeId({})",
id.0
);
self.slots[id.0] = None;
self.free.push(id.0);
}
pub fn is_live(&self, id: NodeId) -> bool {
self.slots.get(id.0).is_some_and(|s| s.is_some())
}
pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut Node> {
self.slots.iter_mut().filter_map(|slot| slot.as_mut())
}
}
impl std::ops::Index<NodeId> for NodeArena {
type Output = Node;
fn index(&self, id: NodeId) -> &Node {
self.slots[id.0].as_ref().expect("accessed a freed NodeId")
}
}
impl std::ops::IndexMut<NodeId> for NodeArena {
fn index_mut(&mut self, id: NodeId) -> &mut Node {
self.slots[id.0].as_mut().expect("accessed a freed NodeId")
}
}