use crate::{
cell::Cell,
component::Component,
event::{KeyCode, KeyEvent, KeyModifiers, MouseButton, MouseEvent, MouseEventKind, ResizeEvent},
overlay::{OverlayBox, OverlayContent, OverlayHandle, OverlayOptions},
renderer::Renderer,
surface::Surface,
terminal::{CrosstermTerminal, Size, Terminal},
};
use anyhow::Result;
use std::io::{self, stdout, Write};
enum RenderStrategy {
Full,
Incremental,
}
pub struct TUI {
terminal: Box<dyn Terminal>,
children: Vec<Box<dyn Component>>,
focus_index: usize,
overlay_stack: Vec<OverlayHandleWrapper>,
dirty: bool,
prev_surface: Option<Surface>,
renderer: Renderer,
last_width: u16,
last_height: u16,
running: bool,
event_handler: Option<Box<dyn FnMut(crate::Event) + Send>>,
}
struct OverlayHandleWrapper {
handle: Box<dyn OverlayHandle>,
content: Box<dyn Component>,
}
impl TUI {
pub fn new(mut terminal: impl Terminal + 'static) -> Self {
let size = terminal.size().unwrap_or(Size { width: 80, height: 24 });
Self {
terminal: Box::new(terminal),
children: Vec::new(),
focus_index: 0,
overlay_stack: Vec::new(),
dirty: true,
prev_surface: None,
renderer: Renderer::new(),
last_width: size.width,
last_height: size.height,
running: false,
event_handler: None,
}
}
pub fn with_crossterm() -> Result<Self> {
let terminal = CrosstermTerminal::new()?;
Ok(Self::new(terminal))
}
pub fn add_child(&mut self, component: impl Component + 'static) -> usize {
let index = self.children.len();
self.children.push(Box::new(component));
self.request_render();
index
}
pub fn remove_child(&mut self, index: usize) {
if index < self.children.len() {
self.children.remove(index);
if self.focus_index >= self.children.len() && !self.children.is_empty() {
self.focus_index = self.children.len() - 1;
}
self.request_render();
}
}
pub fn set_focus(&mut self, index: usize) {
if index < self.children.len() {
if self.focus_index < self.children.len() {
if let Some(child) = self.children.get_mut(self.focus_index) {
child.unfocus();
}
}
self.focus_index = index;
if let Some(child) = self.children.get_mut(index) {
child.focus();
}
self.request_render();
}
}
pub fn focus_index(&self) -> usize {
self.focus_index
}
pub fn children_count(&self) -> usize {
self.children.len()
}
pub fn add_overlay<T: OverlayContent + 'static>(
&mut self,
content: T,
options: OverlayOptions,
) -> usize {
let id = self.overlay_stack.len();
let mut boxed: Box<OverlayBox<T>> = Box::new(OverlayBox::new(content, options));
boxed.set_id(id);
let content_ptr = boxed.as_mut() as *mut OverlayBox<T> as *mut dyn Component;
self.overlay_stack.push(OverlayHandleWrapper {
handle: boxed,
content: unsafe { Box::from_raw(content_ptr) },
});
self.request_render();
id
}
pub fn remove_overlay(&mut self, id: usize) {
if id < self.overlay_stack.len() {
self.overlay_stack.remove(id);
self.request_render();
}
}
pub fn clear_overlays(&mut self) {
self.overlay_stack.clear();
self.request_render();
}
pub fn request_render(&mut self) {
self.dirty = true;
}
pub fn on_event(&mut self, handler: impl FnMut(crate::Event) + Send + 'static) {
self.event_handler = Some(Box::new(handler));
}
pub fn start(&mut self) -> Result<()> {
if self.running {
return Ok(());
}
self.running = true;
crossterm::execute!(stdout(), crossterm::terminal::EnterAlternateScreen)?;
crossterm::execute!(stdout(), crossterm::cursor::Hide)?;
crossterm::execute!(
stdout(),
crossterm::event::EnableMouseCapture
)?;
self.render()?;
while self.running {
if let Some(event) = self.poll_event(std::time::Duration::from_millis(16)) {
self.handle_event(event);
}
if self.dirty {
self.render()?;
}
}
self.cleanup()?;
Ok(())
}
pub fn stop(&mut self) {
self.running = false;
}
pub fn is_running(&self) -> bool {
self.running
}
fn poll_event(&self, timeout: std::time::Duration) -> Option<crate::Event> {
if crossterm::event::poll(timeout).ok()? {
crossterm::event::read().ok().map(Self::convert_event)
} else {
None
}
}
fn convert_event(event: crossterm::event::Event) -> crate::Event {
match event {
crossterm::event::Event::Key(key) => {
let code = match key.code {
crossterm::event::KeyCode::Enter => KeyCode::Enter,
crossterm::event::KeyCode::Esc => KeyCode::Escape,
crossterm::event::KeyCode::Tab => KeyCode::Tab,
crossterm::event::KeyCode::Backspace => KeyCode::Backspace,
crossterm::event::KeyCode::Delete => KeyCode::Delete,
crossterm::event::KeyCode::Up => KeyCode::Up,
crossterm::event::KeyCode::Down => KeyCode::Down,
crossterm::event::KeyCode::Left => KeyCode::Left,
crossterm::event::KeyCode::Right => KeyCode::Right,
crossterm::event::KeyCode::Home => KeyCode::Home,
crossterm::event::KeyCode::End => KeyCode::End,
crossterm::event::KeyCode::PageUp => KeyCode::PageUp,
crossterm::event::KeyCode::PageDown => KeyCode::PageDown,
crossterm::event::KeyCode::Insert => KeyCode::Insert,
crossterm::event::KeyCode::F(n) => KeyCode::F(n),
crossterm::event::KeyCode::Char(c) => KeyCode::Char(c),
_ => KeyCode::Enter, };
let modifiers = KeyModifiers {
shift: key.modifiers.contains(crossterm::event::KeyModifiers::SHIFT),
ctrl: key.modifiers.contains(crossterm::event::KeyModifiers::CONTROL),
alt: key.modifiers.contains(crossterm::event::KeyModifiers::ALT),
meta: key.modifiers.contains(crossterm::event::KeyModifiers::META),
};
crate::Event::Key(KeyEvent::with_modifiers(code, modifiers))
}
crossterm::event::Event::Mouse(mouse) => {
let kind = match mouse.kind {
crossterm::event::MouseEventKind::Moved => MouseEventKind::Drag,
crossterm::event::MouseEventKind::Drag(_) => MouseEventKind::Drag,
crossterm::event::MouseEventKind::ScrollDown => MouseEventKind::ScrollDown,
crossterm::event::MouseEventKind::ScrollUp => MouseEventKind::ScrollUp,
_ => MouseEventKind::Click,
};
let button = if mouse.modifiers.contains(crossterm::event::KeyModifiers::CONTROL) {
MouseButton::Right
} else if mouse.modifiers.contains(crossterm::event::KeyModifiers::ALT) {
MouseButton::Middle
} else {
MouseButton::Left
};
crate::Event::Mouse(MouseEvent {
kind,
button,
row: mouse.row,
col: mouse.column,
})
}
crossterm::event::Event::Resize(cols, rows) => crate::Event::Resize(ResizeEvent {
width: cols,
height: rows,
}),
crossterm::event::Event::FocusGained => crate::Event::FocusGained,
crossterm::event::Event::FocusLost => crate::Event::FocusLost,
_ => crate::Event::None,
}
}
fn handle_event(&mut self, event: crate::Event) {
if let Some(top) = self.overlay_stack.last_mut() {
if top.handle.is_hidden() {
return;
}
if top.content.handle_event(&event) {
self.request_render();
return;
}
}
if let crate::Event::Key(ref key) = event {
if key.code == KeyCode::Escape && !self.overlay_stack.is_empty() {
self.overlay_stack.pop();
self.request_render();
return;
}
}
if self.focus_index < self.children.len() {
if self.children[self.focus_index].handle_event(&event) {
self.request_render();
return;
}
}
if let crate::Event::Key(key) = &event {
match key.code {
KeyCode::Tab => {
if self.children.len() > 1 {
let next = if key.modifiers.shift {
self.focus_index.saturating_sub(1)
} else {
(self.focus_index + 1) % self.children.len()
};
self.set_focus(next);
}
}
KeyCode::Char('c') if key.modifiers.ctrl => {
self.stop();
}
_ => {}
}
}
if let Some(ref mut handler) = self.event_handler {
handler(event);
}
}
fn render(&mut self) -> Result<()> {
let size = self.terminal.size()?;
let strategy = self.determine_render_strategy(size);
let mut surface = Surface::new(size.width, size.height);
let empty_cell = Cell::new(' ');
surface.fill(empty_cell);
let area = surface.area();
for child in &mut self.children {
child.render(&mut surface, area);
}
for overlay in &mut self.overlay_stack {
if !overlay.handle.is_hidden() {
overlay.content.render(&mut surface, area);
}
}
match strategy {
RenderStrategy::Full => {
self.renderer.begin_sync();
self.renderer.clear_screen();
for row in 0..size.height {
for col in 0..size.width {
if let Some(cell) = surface.get(row, col) {
self.renderer.render_cell(row, col, cell);
}
}
}
self.renderer.end_sync()?;
}
RenderStrategy::Incremental => {
self.renderer.begin_sync();
if let (Some(first), Some(last)) = (surface.first_dirty(), surface.last_dirty()) {
self.renderer
.render_changed_lines(&surface, first, last.min(size.height - 1))?;
}
self.renderer.end_sync()?;
}
}
self.dirty = false;
surface.clear_dirty();
self.prev_surface = Some(surface);
print!("\x1b[?25l");
io::stdout().flush()?;
Ok(())
}
fn determine_render_strategy(&mut self, size: Size) -> RenderStrategy {
if self.prev_surface.is_none() {
self.last_width = size.width;
self.last_height = size.height;
return RenderStrategy::Full;
}
if size.width != self.last_width {
self.last_width = size.width;
self.last_height = size.height;
return RenderStrategy::Full;
}
if let Some(ref prev) = self.prev_surface {
if let (Some(_first), Some(last)) = (prev.first_dirty(), prev.last_dirty()) {
if last > size.height / 4 * 3 {
return RenderStrategy::Full;
}
}
}
RenderStrategy::Incremental
}
fn cleanup(&mut self) -> Result<()> {
crossterm::execute!(stdout(), crossterm::cursor::Show)?;
crossterm::execute!(
stdout(),
crossterm::event::DisableMouseCapture
)?;
crossterm::execute!(stdout(), crossterm::terminal::LeaveAlternateScreen)?;
io::stdout().flush()?;
Ok(())
}
pub fn force_redraw(&mut self) {
if let Some(ref mut surf) = self.prev_surface {
surf.mark_all_dirty();
}
self.dirty = true;
}
pub fn size(&mut self) -> Result<Size> {
self.terminal.size()
}
}
impl Drop for TUI {
fn drop(&mut self) {
if self.running {
let _ = self.cleanup();
}
}
}