use std::io::{self, Write};
use crossterm::cursor::{Hide, Show};
use crossterm::event::{
DisableMouseCapture, EnableMouseCapture, Event as CtEvent, KeyCode as CtKeyCode, KeyEventKind,
KeyModifiers, MouseButton as CtMouseButton, MouseEventKind as CtMouseKind,
};
use crossterm::execute;
use crossterm::terminal::{
EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode,
is_raw_mode_enabled,
};
use ratatui_core::buffer::Buffer;
use ratatui_core::layout::Rect;
use ratatui_core::style::Style;
use super::event::{Event, Key, KeyCode, Mouse, MouseButton, MouseKind};
use super::style::{StyleSheet, Theme};
use super::surface::Surface;
use super::view::{RenderCtx, View};
pub struct AltScreen {
active: bool,
}
impl AltScreen {
pub fn enter() -> io::Result<Self> {
let mut out = io::stdout();
execute!(out, EnterAlternateScreen, EnableMouseCapture)?;
out.flush()?;
Ok(Self { active: true })
}
pub fn leave(&mut self) {
if self.active {
let mut out = io::stdout();
let _ = execute!(out, DisableMouseCapture, LeaveAlternateScreen);
let _ = out.flush();
self.active = false;
}
}
}
impl Drop for AltScreen {
fn drop(&mut self) {
self.leave();
}
}
pub struct TerminalSession {
active: bool,
raw_mode_owned: bool,
}
impl TerminalSession {
pub fn enter() -> io::Result<Self> {
let raw_mode_owned = !is_raw_mode_enabled()?;
if raw_mode_owned {
enable_raw_mode()?;
}
let mut out = io::stdout();
if let Err(error) =
execute!(out, EnterAlternateScreen, EnableMouseCapture, Hide).and_then(|()| out.flush())
{
let _ = execute!(out, Show, DisableMouseCapture, LeaveAlternateScreen);
if raw_mode_owned {
let _ = disable_raw_mode();
}
return Err(error);
}
Ok(Self {
active: true,
raw_mode_owned,
})
}
pub fn leave(&mut self) {
if !self.active {
return;
}
let mut out = io::stdout();
let _ = execute!(out, Show, DisableMouseCapture, LeaveAlternateScreen);
let _ = out.flush();
if self.raw_mode_owned {
let _ = disable_raw_mode();
}
self.active = false;
}
}
impl Drop for TerminalSession {
fn drop(&mut self) {
self.leave();
}
}
pub struct Overlay<'a> {
pub area: Rect,
pub view: &'a dyn View,
pub clear: bool,
}
pub fn paint(
buffer: &mut Buffer,
area: Rect,
theme: &Theme,
root: &dyn View,
overlays: &[Overlay],
) {
paint_with_sheet(
buffer,
area,
theme,
StyleSheet::from_theme(theme),
root,
overlays,
);
}
pub fn paint_with_sheet(
buffer: &mut Buffer,
area: Rect,
theme: &Theme,
sheet: StyleSheet,
root: &dyn View,
overlays: &[Overlay],
) {
{
let mut surface = Surface::new(buffer, area);
surface.fill(Style::default().bg(theme.background));
}
let ctx = RenderCtx::new(theme).with_sheet(sheet);
{
let mut surface = Surface::new(buffer, area);
root.render(area, &mut surface, &ctx);
}
for overlay in overlays {
let mut surface = Surface::new(buffer, overlay.area);
if overlay.clear {
surface.fill(Style::default().bg(theme.surface));
}
overlay
.view
.render(overlay.area, &mut surface, &ctx.with_focus(true));
}
}
pub fn translate_event(event: CtEvent) -> Option<Event> {
match event {
CtEvent::Key(k) => {
if k.kind == KeyEventKind::Release {
return None;
}
let code = match k.code {
CtKeyCode::Char(c) => KeyCode::Char(c),
CtKeyCode::Enter => KeyCode::Enter,
CtKeyCode::Esc => KeyCode::Esc,
CtKeyCode::Backspace => KeyCode::Backspace,
CtKeyCode::Delete => KeyCode::Delete,
CtKeyCode::Tab => KeyCode::Tab,
CtKeyCode::BackTab => KeyCode::BackTab,
CtKeyCode::Up => KeyCode::Up,
CtKeyCode::Down => KeyCode::Down,
CtKeyCode::Left => KeyCode::Left,
CtKeyCode::Right => KeyCode::Right,
CtKeyCode::Home => KeyCode::Home,
CtKeyCode::End => KeyCode::End,
CtKeyCode::PageUp => KeyCode::PageUp,
CtKeyCode::PageDown => KeyCode::PageDown,
_ => return None,
};
Some(Event::Key(Key {
code,
ctrl: k.modifiers.contains(KeyModifiers::CONTROL),
alt: k.modifiers.contains(KeyModifiers::ALT),
shift: k.modifiers.contains(KeyModifiers::SHIFT),
}))
}
CtEvent::Mouse(m) => {
let kind = match m.kind {
CtMouseKind::Down(b) => MouseKind::Down(button(b)),
CtMouseKind::Up(b) => MouseKind::Up(button(b)),
CtMouseKind::Drag(b) => MouseKind::Drag(button(b)),
CtMouseKind::Moved => MouseKind::Moved,
CtMouseKind::ScrollUp => MouseKind::ScrollUp,
CtMouseKind::ScrollDown => MouseKind::ScrollDown,
CtMouseKind::ScrollLeft => MouseKind::ScrollLeft,
CtMouseKind::ScrollRight => MouseKind::ScrollRight,
};
Some(Event::Mouse(Mouse {
kind,
column: m.column,
row: m.row,
shift: m.modifiers.contains(KeyModifiers::SHIFT),
ctrl: m.modifiers.contains(KeyModifiers::CONTROL),
alt: m.modifiers.contains(KeyModifiers::ALT),
}))
}
CtEvent::Paste(text) => Some(Event::Paste(text)),
CtEvent::Resize(width, height) => Some(Event::Resize { width, height }),
_ => None,
}
}
fn button(b: CtMouseButton) -> MouseButton {
match b {
CtMouseButton::Left => MouseButton::Left,
CtMouseButton::Right => MouseButton::Right,
CtMouseButton::Middle => MouseButton::Middle,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::components::{Boxed, Flex, ProgressBar, Scroll, ScrollState, StatusBar, Text};
use crate::event::{Event, MouseButton, MouseKind};
use crate::style::Theme;
use crate::test_support::{buffer, rainbow_theme, row};
use crate::view::{Element, element};
use ratatui_core::buffer::Buffer;
use ratatui_core::layout::Rect;
use ratatui_core::text::{Line, Span};
fn demo_tree(frame: u64) -> Element {
let lines: Vec<Line<'static>> = (0..40).map(|i| Line::from(format!("row {i}"))).collect();
let mut st = ScrollState::new();
st.clamp(40, 8);
element(
Flex::column()
.grow(
1,
element(Boxed::new(element(Scroll::new(lines, &st))).title(" body ")),
)
.fixed(1, element(ProgressBar::indeterminate(frame)))
.fixed(1, element(StatusBar::new().left(vec![Span::raw("status")]))),
)
}
#[test]
fn translate_maps_button_modifiers_and_drag() {
use crossterm::event::{
Event as CtEvent, KeyModifiers, MouseButton as CtButton, MouseEvent, MouseEventKind,
};
let ct = CtEvent::Mouse(MouseEvent {
kind: MouseEventKind::Down(CtButton::Right),
column: 7,
row: 3,
modifiers: KeyModifiers::SHIFT | KeyModifiers::CONTROL,
});
let Some(Event::Mouse(m)) = translate_event(ct) else {
panic!("expected a mouse event");
};
assert_eq!(m.kind, MouseKind::Down(MouseButton::Right));
assert_eq!((m.column, m.row), (7, 3));
assert!(m.shift && m.ctrl && !m.alt);
assert!(!m.plain());
}
#[test]
fn paint_composites_background_root_and_overlay() {
let theme = Theme::default();
let mut buf = buffer(20, 5);
let area = buf.area;
let root = Text::new(vec![Line::from(Span::raw("base layer"))]);
let dialog = Text::new(vec![Line::from("MODAL")]);
let overlay_area = Rect::new(5, 2, 7, 1);
let overlays = [Overlay {
area: overlay_area,
view: &dialog,
clear: true,
}];
paint(&mut buf, area, &theme, &root, &overlays);
assert!(row(&buf, 0).starts_with("base layer"));
assert_eq!(buf[(0, 0)].bg, theme.background);
assert!(row(&buf, 2).contains("MODAL"));
assert_eq!(buf[(5, 2)].bg, theme.surface);
}
#[test]
fn paint_survives_degenerate_and_swept_sizes() {
let theme = Theme::default();
for &w in &[0u16, 1, 2, 3, 5, 10, 17, 40, 200] {
for &h in &[0u16, 1, 2, 3, 5, 8, 40] {
let mut buf = Buffer::empty(Rect::new(0, 0, w, h));
let area = buf.area;
paint(&mut buf, area, &theme, demo_tree(w as u64).as_ref(), &[]);
assert_eq!(buf.area, Rect::new(0, 0, w, h));
}
}
}
#[test]
fn resize_reflows_body_and_status() {
let theme = Theme::default();
let mut big = buffer(80, 24);
let a = big.area;
paint(&mut big, a, &theme, demo_tree(0).as_ref(), &[]);
assert!(
row(&big, 0).contains("body"),
"title at 80x24: {:?}",
row(&big, 0)
);
let mut small = buffer(20, 6);
let a2 = small.area;
paint(&mut small, a2, &theme, demo_tree(0).as_ref(), &[]);
assert!(
row(&small, 5).contains("status"),
"status row at 20x6: {:?}",
row(&small, 5)
);
}
#[test]
fn compositor_uses_theme_background_and_overlay_surface() {
let t = rainbow_theme();
let mut buf = buffer(12, 4);
let area = buf.area;
let root = Text::raw("base");
let dialog = Text::raw("hi");
let overlays = [Overlay {
area: Rect::new(4, 1, 4, 1),
view: &dialog,
clear: true,
}];
paint(&mut buf, area, &t, &root, &overlays);
assert_eq!(
buf[(0, 3)].bg,
t.background,
"base fill uses theme.background"
);
assert_eq!(
buf[(4, 1)].bg,
t.surface,
"overlay clear uses theme.surface"
);
}
}