use crate::backend::{CrosstermBackend, TerminalBackend};
use crate::buffer::Buffer;
use crate::component::Component;
use crate::dirty::Dirty;
use std::cell::RefCell;
use std::time::Instant;
use crate::event::{Command, Event, EventPhase, Key, KeyEvent};
use crate::geom::Rect;
use crate::geom::Pos;
use crate::node::{Node, NodeId};
use crate::Result;
thread_local! {
pub static COMMAND_QUEUE: RefCell<Vec<Command>> = RefCell::new(Vec::new());
pub static DEBUG_MODE: RefCell<bool> = RefCell::new(false);
pub static FORCE_FULL_PAINT: RefCell<bool> = RefCell::new(false);
pub static TIMER_QUEUE: RefCell<TimerQueue> = RefCell::new(TimerQueue { requests: Vec::new(), next_id: 0 });
pub static WORKER_REGISTRY: RefCell<WorkerRegistry> = RefCell::new(WorkerRegistry { flags: std::collections::HashMap::new(), next_id: 0 });
}
pub struct WorkerRegistry {
pub flags: std::collections::HashMap<crate::event::WorkerId, std::sync::Arc<std::sync::atomic::AtomicBool>>,
pub next_id: u64,
}
pub struct TimerQueue {
pub requests: Vec<TimerRequest>,
pub next_id: u64,
}
pub enum TimerAction {
SetOneShot { id: u64, duration_ms: u64 },
SetInterval { id: u64, interval_ms: u64 },
Cancel { id: u64 },
}
pub struct TimerRequest {
pub action: TimerAction,
pub owner: NodeId,
}
pub struct TimerEntry {
pub id: u64,
pub interval_ms: Option<u64>,
pub next_fire: Instant,
pub owner: NodeId,
}
pub struct Runtime<B: TerminalBackend = CrosstermBackend> {
pub(crate) root: Node,
pub(crate) backend: B,
pub(crate) front: Buffer,
pub(crate) back: Buffer,
pub(crate) root_rect: Rect,
pub(crate) quit: bool,
pub(crate) full_paint: bool,
pub(crate) dirty: Dirty,
pub(crate) focused_id: Option<NodeId>,
pub(crate) tick_rate: std::time::Duration,
pub(crate) task_rx: std::sync::mpsc::Receiver<String>,
pub(crate) task_tx: std::sync::mpsc::Sender<String>,
pub(crate) timers: Vec<TimerEntry>,
}
const ZERO_RECT: Rect = Rect {
x: 0,
y: 0,
width: 0,
height: 0,
};
impl<B: TerminalBackend> Runtime<B> {
pub fn new(root: impl Component + 'static, backend: B) -> Result<Self> {
let (tx, rx) = std::sync::mpsc::channel::<String>();
Ok(Self {
root: Node::root(root),
backend,
front: Buffer::new(crate::geom::Size { width: 1, height: 1 }),
back: Buffer::new(crate::geom::Size { width: 1, height: 1 }),
root_rect: ZERO_RECT,
quit: false,
full_paint: false,
dirty: Dirty::NONE,
focused_id: None,
tick_rate: std::time::Duration::from_millis(250),
task_rx: rx,
task_tx: tx,
timers: Vec::new(),
})
}
pub fn tick_rate(mut self, rate: std::time::Duration) -> Self {
self.tick_rate = rate;
self
}
pub fn step(&mut self) -> Result<()> {
if self.quit {
return Ok(());
}
let commands: Vec<Command> = COMMAND_QUEUE.with(|q| q.borrow_mut().drain(..).collect());
for cmd in commands {
match cmd {
Command::Quit => self.quit = true,
Command::FocusNext => self.focus_next(false),
Command::FocusPrev => self.focus_next(true),
Command::Custom(_) => {}
}
}
while let Ok(result) = self.task_rx.try_recv() {
if let Some(rest) = result.strip_prefix("__worker_done__") {
if let Some((id_str, payload)) = rest.split_once("__") {
if let Ok(id) = id_str.parse::<u64>() {
self.dispatch_event(Event::WorkerDone(
crate::event::WorkerId(id), payload.to_string()))?;
}
}
} else if let Some(rest) = result.strip_prefix("__worker_cancelled__") {
if let Ok(id) = rest.parse::<u64>() {
self.dispatch_event(Event::WorkerDone(
crate::event::WorkerId(id), String::new()))?;
}
} else {
self.dispatch_event(Event::TaskComplete(result))?;
}
if self.dirty.contains(Dirty::PAINT) {
self.paint_frame()?;
self.dirty = Dirty::NONE;
}
}
let now = Instant::now();
let mut expired: Vec<(u64, Option<u64>, NodeId)> = Vec::new(); let mut i = 0;
while i < self.timers.len() {
if now >= self.timers[i].next_fire {
let entry = &self.timers[i];
expired.push((entry.id, entry.interval_ms, entry.owner));
self.timers.remove(i);
} else {
i += 1;
}
}
for (id, interval_ms, owner) in expired {
self.dispatch_to_target(owner, &Event::Timer(id));
if self.dirty.intersects(Dirty::LAYOUT | Dirty::TREE) {
self.layout_children()?;
}
if self.dirty.contains(Dirty::PAINT) {
self.paint_frame()?;
self.dirty = Dirty::NONE;
}
if let Some(interval) = interval_ms {
self.timers.push(TimerEntry {
id,
interval_ms: Some(interval),
next_fire: now + std::time::Duration::from_millis(interval),
owner,
});
}
}
let poll_timeout = if self.task_rx.try_recv().is_ok() {
std::time::Duration::from_millis(0)
} else {
let next_timer = self.timers.iter().map(|t| {
let remaining = t.next_fire.duration_since(now);
if remaining.as_millis() > 0 {
remaining
} else {
std::time::Duration::from_millis(0)
}
}).min();
match next_timer {
Some(t) if t < self.tick_rate => t,
_ => self.tick_rate,
}
};
let event = match self.backend.poll_event(poll_timeout)? {
Some(e) => e,
None => Event::Tick,
};
self.dispatch_event(event)?;
self.process_timer_requests();
if self.dirty.intersects(Dirty::LAYOUT | Dirty::TREE) {
self.layout_children()?;
}
if self.dirty.contains(Dirty::PAINT) {
self.paint_frame()?;
self.dirty = Dirty::NONE;
}
Ok(())
}
pub(crate) fn process_timer_requests(&mut self) {
let requests: Vec<TimerRequest> = TIMER_QUEUE.with(|q| q.borrow_mut().requests.drain(..).collect());
let now = Instant::now();
for req in requests {
match req.action {
TimerAction::SetOneShot { id, duration_ms } => {
self.timers.push(TimerEntry {
id,
interval_ms: None,
next_fire: now + std::time::Duration::from_millis(duration_ms),
owner: req.owner,
});
}
TimerAction::SetInterval { id, interval_ms } => {
self.timers.push(TimerEntry {
id,
interval_ms: Some(interval_ms),
next_fire: now + std::time::Duration::from_millis(interval_ms),
owner: req.owner,
});
}
TimerAction::Cancel { id } => {
self.timers.retain(|t| t.id != id);
}
}
}
}
pub fn run(&mut self) -> Result<()> {
self.backend.enter()?;
self.initial_layout_and_paint()?;
self.process_timer_requests();
while !self.quit {
self.step()?;
}
self.root.unmount(&mut self.dirty, &mut self.quit, Some(self.task_tx.clone()));
self.backend.leave()?;
Ok(())
}
pub fn initial_layout_and_paint(&mut self) -> Result<()> {
let size = self.backend.size()?;
self.root_rect = Rect {
x: 0,
y: 0,
width: size.width,
height: size.height,
};
self.front = Buffer::new(size);
self.back = Buffer::new(size);
self.full_paint = true;
self.dirty = Dirty::LAYOUT | Dirty::PAINT;
self.layout_children()?;
self.root.mount(&mut self.dirty, &mut self.quit, Some(self.task_tx.clone()));
let mut ids = Vec::new();
self.root.collect_focusable(&mut ids);
if !ids.is_empty() {
self.focused_id = Some(ids[0]);
}
self.paint_frame()?;
self.dirty = Dirty::NONE;
Ok(())
}
fn dispatch_event(&mut self, event: Event) -> Result<()> {
match event {
Event::Resize(size) => {
let new_rect = Rect {
x: 0,
y: 0,
width: size.width,
height: size.height,
};
if new_rect != self.root_rect {
self.root_rect = new_rect;
self.front.resize(size);
self.back.resize(size);
self.full_paint = true;
self.dirty = Dirty::LAYOUT | Dirty::PAINT;
}
}
Event::Key(KeyEvent { key: Key::Tab, modifiers }) => {
self.focus_next(modifiers.shift);
}
Event::Mouse(mouse_event) => {
let pos = Pos {
x: mouse_event.x,
y: mouse_event.y,
};
if let Some(id) = self.root.hit_test(pos) {
self.dispatch_to_target(id, &Event::Mouse(mouse_event));
}
}
Event::Focus | Event::Blur => {
}
other => {
let target = self.focused_id.unwrap_or(NodeId::ROOT);
self.dispatch_to_target(target, &other);
}
}
Ok(())
}
pub(crate) fn focus_next(&mut self, backwards: bool) {
let mut ids = Vec::new();
self.root.collect_focusable(&mut ids);
if ids.is_empty() {
return;
}
let old_id = self.focused_id;
let new_idx = match old_id {
None => {
if backwards {
ids.len() - 1
} else {
0
}
}
Some(old) => {
if let Some(pos) = ids.iter().position(|id| *id == old) {
if backwards {
if pos == 0 {
ids.len() - 1
} else {
pos - 1
}
} else if pos + 1 >= ids.len() {
0
} else {
pos + 1
}
} else {
0
}
}
};
let new_id = ids[new_idx];
self.set_focus(new_id);
}
fn dispatch_to_target(&mut self, target_id: NodeId, event: &Event) {
let (path, effective_target) = match self.root.find_path_to(target_id) {
Some(p) => (p, target_id),
None => (vec![NodeId::ROOT], NodeId::ROOT),
};
let mut stopped = false;
for &node_id in &path[..path.len().saturating_sub(1)] {
if stopped {
break;
}
self.root.send_event(
node_id,
event,
&mut self.dirty,
&mut self.quit,
EventPhase::Capture,
&mut stopped,
Some(self.task_tx.clone()),
);
}
if !stopped {
self.root.send_event(
effective_target,
event,
&mut self.dirty,
&mut self.quit,
EventPhase::Target,
&mut stopped,
Some(self.task_tx.clone()),
);
}
for &node_id in path[..path.len().saturating_sub(1)].iter().rev() {
if stopped {
break;
}
self.root.send_event(
node_id,
event,
&mut self.dirty,
&mut self.quit,
EventPhase::Bubble,
&mut stopped,
Some(self.task_tx.clone()),
);
}
}
fn set_focus(&mut self, new_id: NodeId) {
let old_id = self.focused_id;
if let Some(old) = old_id {
if old != new_id {
let mut stopped = false;
self.root.send_event(
old,
&Event::Blur,
&mut self.dirty,
&mut self.quit,
EventPhase::Target,
&mut stopped,
Some(self.task_tx.clone()),
);
}
}
self.focused_id = Some(new_id);
if old_id != Some(new_id) {
let mut stopped = false;
self.root.send_event(
new_id,
&Event::Focus,
&mut self.dirty,
&mut self.quit,
EventPhase::Target,
&mut stopped,
Some(self.task_tx.clone()),
);
}
self.dirty |= Dirty::PAINT;
}
pub(crate) fn layout_children(&mut self) -> Result<()> {
self.root.layout(self.root_rect);
Ok(())
}
pub(crate) fn paint_frame(&mut self) -> Result<()> {
self.back.clear();
self.root.render_with_clip(&mut self.back, self.focused_id, None);
if DEBUG_MODE.with(|d| *d.borrow()) {
self.root.debug_render(&mut self.back, self.focused_id);
}
if self.full_paint || FORCE_FULL_PAINT.with(|f| f.replace(false)) {
let ops = self.back.all_ops();
self.backend.flush(&ops)?;
self.full_paint = false;
} else {
let ops = self.front.diff(&self.back);
self.backend.flush(&ops)?;
}
std::mem::swap(&mut self.front, &mut self.back);
Ok(())
}
}