use std::{
any::{Any, type_name},
collections::{HashMap, hash_map::Entry},
fmt,
};
use ratatui::Frame;
use ratatui::buffer::Buffer;
use ratatui::layout::{Position, Rect, Size};
use ratatui::widgets::{StatefulWidget, Widget};
use crate::Theme;
use super::engine::{DeclarationEnv, LayerKind, RenderPass};
use super::{ChildId, Event, HoverState, KeyChord, MouseButton, TabWrap};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum EventResult<Msg> {
Ignored,
Consumed,
Emit(Msg),
}
#[expect(
clippy::struct_excessive_bools,
reason = "two independent (leaf, within) flag pairs — focus and hover; the bools are the natural shape"
)]
pub struct RenderCtx<'a, 'frame, State, Msg> {
pub(crate) frame: &'a mut Frame<'frame>,
pub(crate) area: Rect,
pub theme: &'a Theme,
pub focused: bool,
pub contains_focus: bool,
pub hovered: bool,
pub contains_hover: bool,
pub(crate) hover_position: Option<Position>,
pub(crate) hover: &'a HoverState,
pub(crate) transients: Option<&'a mut TransientMap>,
pub(crate) depth: usize,
pub(crate) pass: Option<&'a mut RenderPass<State, Msg>>,
pub(crate) state: Option<&'a State>,
}
impl<State, Msg> fmt::Debug for RenderCtx<'_, '_, State, Msg> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("RenderCtx")
.field("area", &self.area)
.field("theme", self.theme)
.field("focused", &self.focused)
.field("contains_focus", &self.contains_focus)
.field("hovered", &self.hovered)
.field("contains_hover", &self.contains_hover)
.field("hover_position", &self.hover_position)
.field("depth", &self.depth)
.field("declaration_active", &self.pass.is_some())
.finish_non_exhaustive()
}
}
impl<'a, 'frame, State, Msg> RenderCtx<'a, 'frame, State, Msg> {
fn paints(&self) -> bool {
self.pass.as_deref().is_none_or(RenderPass::paints)
}
pub fn render_widget(&mut self, widget: impl Widget, area: Rect) {
if !self.paints() {
return;
}
if let Some(canvas) = self
.pass
.as_deref_mut()
.and_then(RenderPass::active_canvas_mut)
{
widget.render(area.intersection(canvas.buffer.area), &mut canvas.buffer);
canvas.mark_painted(area);
} else {
self.frame.render_widget(widget, area);
}
}
pub fn render_stateful_widget<W: StatefulWidget>(
&mut self,
widget: W,
area: Rect,
state: &mut W::State,
) {
if !self.paints() {
return;
}
if let Some(canvas) = self
.pass
.as_deref_mut()
.and_then(RenderPass::active_canvas_mut)
{
widget.render(
area.intersection(canvas.buffer.area),
&mut canvas.buffer,
state,
);
canvas.mark_painted(area);
} else {
self.frame.render_stateful_widget(widget, area, state);
}
}
#[expect(
clippy::missing_panics_doc,
reason = "the expect is unreachable: a suppressed paint call implies an active pass"
)]
pub fn with_buffer<R>(&mut self, paint: impl FnOnce(&mut Buffer) -> R) -> R {
if !self.paints() {
let area = self.frame.area();
let pass = self
.pass
.as_deref_mut()
.expect("a suppressed paint call always has a pass");
return paint(pass.scratch_buffer(area));
}
if let Some(canvas) = self
.pass
.as_deref_mut()
.and_then(RenderPass::active_canvas_mut)
{
let area = canvas.buffer.area;
canvas.mark_painted(area);
paint(&mut canvas.buffer)
} else {
paint(self.frame.buffer_mut())
}
}
#[must_use]
pub const fn area(&self) -> Rect {
self.area
}
#[must_use]
pub fn frame_area(&self) -> Rect {
self.frame.area()
}
#[must_use]
pub const fn hover_position(&self) -> Option<Position> {
self.hover_position
}
#[must_use]
pub fn transient<T: 'static>(&self) -> Option<&T> {
let transients = self.transients.as_deref()?;
let path = self.pass.as_deref()?.current_path()?;
Some(transients.get(path)?.expect_ref(path))
}
pub fn transient_mut<T: 'static>(&mut self) -> Option<&mut T> {
let path = self.pass.as_deref()?.current_path()?.to_vec();
let transients = self.transients.as_deref_mut()?;
Some(transients.get_mut(&path)?.expect_mut(&path))
}
#[must_use]
pub fn state(&self) -> &'a State {
self.state
.expect("RenderCtx::state is unavailable outside a Ratcn declaration pass")
}
#[doc(hidden)]
pub fn in_area(
&mut self,
area: Rect,
render: impl FnOnce(&mut RenderCtx<'_, 'frame, State, Msg>),
) {
let mut ctx = RenderCtx {
frame: &mut *self.frame,
area,
theme: self.theme,
focused: self.focused,
contains_focus: self.contains_focus,
hovered: self.hovered,
contains_hover: self.contains_hover,
hover_position: self.hover_position,
hover: self.hover,
transients: self.transients.as_deref_mut(),
depth: self.depth,
pass: self.pass.as_deref_mut(),
state: self.state,
};
render(&mut ctx);
}
fn declaring(
&mut self,
area: Rect,
method: &str,
) -> (
&mut RenderPass<State, Msg>,
DeclarationEnv<'_, 'frame, State>,
) {
let state = self
.state
.unwrap_or_else(|| panic!("{method} called without declaration state"));
let env = DeclarationEnv {
frame: &mut *self.frame,
area,
state,
theme: self.theme,
hover: self.hover,
transients: self.transients.as_deref_mut(),
depth: self.depth,
};
let pass = self
.pass
.as_deref_mut()
.unwrap_or_else(|| panic!("{method} called outside a Ratcn declaration pass"));
(pass, env)
}
pub fn scope(
&mut self,
id: impl Into<ChildId>,
area: Rect,
options: ScopeOptions,
declare: impl FnOnce(&mut RenderCtx<'_, 'frame, State, Msg>),
) {
let (pass, env) = self.declaring(area, "scope");
pass.scope(id.into(), options, env, declare);
}
pub fn render_component(
&mut self,
id: impl Into<ChildId>,
component: impl Component<State, Msg> + 'static,
area: Rect,
) {
let (pass, env) = self.declaring(area, "render_component");
pass.render_component(id.into(), component, env);
}
#[doc(hidden)]
pub fn render_prepared_component(
&mut self,
id: ChildId,
component: PreparedComponent<State, Msg>,
area: Rect,
) {
let (pass, env) = self.declaring(area, "render_prepared_component");
pass.render_prepared_component(id, component, env);
}
pub fn modal(
&mut self,
id: impl Into<ChildId>,
component: impl Component<State, Msg> + 'static,
area: Rect,
) {
let (pass, env) = self.declaring(area, "modal");
pass.modal(id.into(), component, env);
}
pub fn hint(
&mut self,
id: impl Into<ChildId>,
options: ScopeOptions,
area: Rect,
declare: impl FnOnce(&mut RenderCtx<'_, 'frame, State, Msg>),
) {
let (pass, env) = self.declaring(area, "hint");
pass.layer_scope(id.into(), LayerKind::Hint, options, None, env, declare);
}
pub fn popup(
&mut self,
id: impl Into<ChildId>,
options: PopupOptions<Msg>,
area: Rect,
declare: impl FnOnce(&mut RenderCtx<'_, 'frame, State, Msg>),
) {
let (pass, env) = self.declaring(area, "popup");
pass.layer_scope(
id.into(),
LayerKind::Popup,
options.scope_options,
options.on_dismiss,
env,
declare,
);
}
pub fn modal_scope(
&mut self,
id: impl Into<ChildId>,
area: Rect,
options: ScopeOptions,
declare: impl FnOnce(&mut RenderCtx<'_, 'frame, State, Msg>),
) {
let (pass, env) = self.declaring(area, "modal_scope");
pass.modal_scope(id.into(), options, env, declare);
}
pub fn defer_paint(&mut self, paint: impl FnOnce(&mut Painter<'_, '_>, &State) + 'static) {
self.pass
.as_deref_mut()
.expect("defer_paint called outside a Ratcn declaration pass")
.defer_paint(paint);
}
}
pub struct PopupOptions<Msg> {
pub(crate) scope_options: ScopeOptions,
pub(crate) on_dismiss: Option<Box<dyn Fn() -> Msg>>,
}
impl<Msg> Default for PopupOptions<Msg> {
fn default() -> Self {
Self {
scope_options: ScopeOptions::default(),
on_dismiss: None,
}
}
}
impl<Msg> fmt::Debug for PopupOptions<Msg> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("PopupOptions")
.field("scope_options", &self.scope_options)
.field("on_dismiss", &self.on_dismiss.is_some())
.finish()
}
}
impl<Msg> PopupOptions<Msg> {
#[must_use]
pub fn on_dismiss(mut self, message: impl Fn() -> Msg + 'static) -> Self {
self.on_dismiss = Some(Box::new(message));
self
}
#[must_use]
pub fn scope_options(mut self, options: ScopeOptions) -> Self {
self.scope_options = options;
self
}
}
pub(crate) enum PaintTarget<'a, 'frame> {
Frame(&'a mut Frame<'frame>),
Canvas(&'a mut super::engine::LayerCanvas),
}
pub struct Painter<'a, 'frame> {
pub(crate) target: PaintTarget<'a, 'frame>,
pub theme: &'a Theme,
}
impl fmt::Debug for Painter<'_, '_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Painter").finish_non_exhaustive()
}
}
impl Painter<'_, '_> {
pub fn render_widget(&mut self, widget: impl Widget, area: Rect) {
match &mut self.target {
PaintTarget::Frame(frame) => frame.render_widget(widget, area),
PaintTarget::Canvas(canvas) => {
widget.render(area.intersection(canvas.buffer.area), &mut canvas.buffer);
canvas.mark_painted(area);
}
}
}
pub fn render_stateful_widget<W: StatefulWidget>(
&mut self,
widget: W,
area: Rect,
state: &mut W::State,
) {
match &mut self.target {
PaintTarget::Frame(frame) => frame.render_stateful_widget(widget, area, state),
PaintTarget::Canvas(canvas) => {
widget.render(
area.intersection(canvas.buffer.area),
&mut canvas.buffer,
state,
);
canvas.mark_painted(area);
}
}
}
pub fn with_buffer<R>(&mut self, paint: impl FnOnce(&mut Buffer) -> R) -> R {
match &mut self.target {
PaintTarget::Frame(frame) => paint(frame.buffer_mut()),
PaintTarget::Canvas(canvas) => {
let area = canvas.buffer.area;
canvas.mark_painted(area);
paint(&mut canvas.buffer)
}
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ScopeOptions {
pub(crate) tab_wrap: TabWrap,
pub(crate) focusable: bool,
pub(crate) hover_focus: bool,
pub(crate) focus_keys: Vec<FocusKeyBinding>,
}
impl ScopeOptions {
#[must_use]
pub const fn tab_wrap(mut self, tab_wrap: TabWrap) -> Self {
self.tab_wrap = tab_wrap;
self
}
#[must_use]
pub const fn focusable(mut self) -> Self {
self.focusable = true;
self
}
#[must_use]
pub const fn hover_focus(mut self) -> Self {
self.hover_focus = true;
self
}
#[must_use]
pub fn focus_key(
mut self,
chord: impl Into<KeyChord>,
path: impl IntoIterator<Item = impl Into<ChildId>>,
) -> Self {
self.focus_keys.push(FocusKeyBinding {
chord: chord.into(),
path: path.into_iter().map(Into::into).collect(),
});
self
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct FocusKeyBinding {
pub(crate) chord: KeyChord,
pub(crate) path: Vec<ChildId>,
}
pub(crate) struct TransientValue {
type_name: &'static str,
value: Box<dyn Any>,
}
impl TransientValue {
pub(crate) fn expect_mut<T: 'static>(&mut self, path: &[ChildId]) -> &mut T {
let requested = type_name::<T>();
let stored = self.type_name;
assert_eq!(
stored, requested,
"transient type mismatch at path {path:?}: stored `{stored}`, requested `{requested}`",
);
self.value.downcast_mut::<T>().unwrap_or_else(|| {
panic!(
"transient type metadata mismatch at path {path:?}: stored `{stored}`, requested `{requested}`",
)
})
}
pub(crate) fn expect_ref<T: 'static>(&self, path: &[ChildId]) -> &T {
let requested = type_name::<T>();
assert_eq!(
self.type_name, requested,
"transient type mismatch at path {path:?}: stored `{}`, requested `{requested}`",
self.type_name
);
self.value.downcast_ref::<T>().unwrap_or_else(|| {
panic!(
"transient type metadata mismatch at path {path:?}: stored `{}`, requested `{requested}`",
self.type_name
)
})
}
}
pub(crate) type TransientMap = HashMap<Vec<ChildId>, TransientValue>;
#[doc(hidden)]
pub struct PreparedComponent<State, Msg> {
pub(crate) component: Box<dyn Component<State, Msg>>,
pub(crate) options: ScopeOptions,
pub(crate) self_focusable: bool,
pub(crate) focuses_on_click: bool,
}
impl<State, Msg> fmt::Debug for PreparedComponent<State, Msg> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("PreparedComponent")
.field("self_focusable", &self.self_focusable)
.field("focuses_on_click", &self.focuses_on_click)
.finish_non_exhaustive()
}
}
impl<State, Msg> PreparedComponent<State, Msg> {
#[doc(hidden)]
pub fn prepare(mut component: Box<dyn Component<State, Msg>>, state: &State) -> Self {
component.prepare(state);
let options = component.scope_options();
let self_focusable = component.is_focusable(state);
let focuses_on_click = component.focuses_on_click(state);
Self {
component,
options,
self_focusable,
focuses_on_click,
}
}
}
#[derive(Default)]
pub struct EventCtx<'a> {
path: Vec<ChildId>,
area: Rect,
transients: Option<&'a mut TransientMap>,
detached_transients: Option<Box<TransientMap>>,
capture: Option<&'a mut Option<Vec<ChildId>>>,
capture_button: Option<MouseButton>,
}
impl fmt::Debug for EventCtx<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("EventCtx")
.field("path", &self.path)
.field("transients_available", &self.transients.is_some())
.field("capture_button", &self.capture_button)
.finish()
}
}
impl<'a> EventCtx<'a> {
pub(crate) fn at(
path: &[ChildId],
area: Rect,
transients: &'a mut TransientMap,
capture: &'a mut Option<Vec<ChildId>>,
capture_button: Option<MouseButton>,
) -> Self {
Self {
path: path.to_vec(),
area,
transients: Some(transients),
detached_transients: None,
capture: Some(capture),
capture_button,
}
}
fn transients_mut(&mut self) -> &mut TransientMap {
match &mut self.transients {
Some(transients) => transients,
None => self.detached_transients.get_or_insert_with(Box::default),
}
}
#[must_use]
pub fn with_area(mut self, area: Rect) -> Self {
self.area = area;
self
}
#[must_use]
pub fn path(&self) -> &[ChildId] {
&self.path
}
#[must_use]
pub const fn area(&self) -> Rect {
self.area
}
pub fn transient<T: Default + 'static>(&mut self) -> &mut T {
let requested = type_name::<T>();
let path = self.path.clone();
let value = match self.transients_mut().entry(path.clone()) {
Entry::Vacant(entry) => entry.insert(TransientValue {
type_name: requested,
value: Box::<T>::default(),
}),
Entry::Occupied(entry) => {
let stored = entry.get().type_name;
assert_eq!(
stored, requested,
"transient type mismatch at path {path:?}: stored `{stored}`, requested `{requested}`",
);
entry.into_mut()
}
};
let stored = value.type_name;
value.value.downcast_mut::<T>().unwrap_or_else(|| {
panic!(
"transient type metadata mismatch at path {path:?}: stored `{stored}`, requested `{requested}`",
)
})
}
pub(super) fn transient_if_present<T: 'static>(&mut self) -> Option<&mut T> {
let path = self.path.clone();
let value = self.transients_mut().get_mut(&path)?;
let requested = type_name::<T>();
let stored = value.type_name;
assert_eq!(
stored, requested,
"transient type mismatch at path {path:?}: stored `{stored}`, requested `{requested}`",
);
Some(value.value.downcast_mut::<T>().unwrap_or_else(|| {
panic!(
"transient type metadata mismatch at path {path:?}: stored `{stored}`, requested `{requested}`",
)
}))
}
pub(super) fn take_transient<T: 'static>(&mut self) -> Option<T> {
let path = self.path.clone();
let value = self.transients_mut().remove(&path)?;
let requested = type_name::<T>();
let stored = value.type_name;
assert_eq!(
stored, requested,
"transient type mismatch at path {path:?}: stored `{stored}`, requested `{requested}`",
);
Some(*value.value.downcast::<T>().unwrap_or_else(|_| {
panic!(
"transient type metadata mismatch at path {path:?}: stored `{stored}`, requested `{requested}`",
)
}))
}
pub fn capture_pointer(&mut self, button: MouseButton) {
assert_eq!(
self.capture_button,
Some(button),
"EventCtx::capture_pointer({button:?}) requires the matching MouseKind::Down"
);
let capture = self
.capture
.as_deref_mut()
.expect("EventCtx::capture_pointer is unavailable outside Ratcn event dispatch");
if capture.is_none() {
*capture = Some(self.path.clone());
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Step {
Forward,
Backward,
}
pub trait Component<State, Msg> {
fn prepare(&mut self, _state: &State) {}
fn render(&mut self, ctx: &mut RenderCtx<'_, '_, State, Msg>);
fn scope_options(&self) -> ScopeOptions {
ScopeOptions::default()
}
fn interaction_area(&self, area: Rect) -> Rect {
area
}
fn handle_event(
&mut self,
_event: &Event,
_state: &State,
_ctx: &mut EventCtx<'_>,
) -> EventResult<Msg> {
EventResult::Ignored
}
fn is_focusable(&self, _state: &State) -> bool {
false
}
#[doc(hidden)]
fn focuses_on_click(&self, _state: &State) -> bool {
false
}
}
pub trait MeasuredComponent<State, Msg>: Component<State, Msg> {
fn measure(&self) -> Size;
}