use futures::{
FutureExt,
future::{Either, select},
};
use std::io::{self};
use crate::{
ElementKey,
component::{ComponentHelperExt, InstantiatedComponent},
context::{ContextStack, SystemContext},
element::{ElementRepr, RunOptions},
props::AnyProps,
terminal::{CrossTerminal, Terminal, TerminalImpl, UpdaterTerminal},
};
use super::ComponentDrawer;
struct RestoreGuard {
mouse: bool,
}
impl Drop for RestoreGuard {
fn drop(&mut self) {
if self.mouse {
let _ = crate::terminal::set_mouse_capture(false);
}
ratatui::restore();
}
}
fn should_quit_on_ctrl_c(system_context: &SystemContext, event: &crossterm::event::Event) -> bool {
system_context.auto_quit_on_ctrl_c() && CrossTerminal::received_ctrl_c(event.clone())
}
#[doc(hidden)]
pub struct Tree<'a> {
root_component: InstantiatedComponent,
props: AnyProps<'a>,
system_context: SystemContext,
}
impl<'a> Tree<'a> {
pub(crate) fn new_with_mouse(
mut props: AnyProps<'a>,
helper: Box<dyn ComponentHelperExt>,
mouse: bool,
) -> Self {
Tree {
root_component: InstantiatedComponent::new(
ElementKey::user("_root_tree_"),
props.borrow(),
helper,
),
props,
system_context: SystemContext::new_with_mouse(mouse),
}
}
pub(crate) fn update_once(&mut self, terminal: &mut dyn UpdaterTerminal) {
self.system_context.input.begin_frame();
let mut component_context_stack = ContextStack::root(&mut self.system_context);
self.root_component
.update(terminal, &mut component_context_stack, self.props.borrow());
}
pub(crate) fn draw_root(&mut self, drawer: &mut ComponentDrawer) {
self.root_component.draw(drawer);
}
#[cfg(test)]
pub(crate) fn system_context_mut(&mut self) -> &mut SystemContext {
&mut self.system_context
}
fn render<T: TerminalImpl<Event = crossterm::event::Event>>(
&mut self,
terminal: &mut Terminal<T>,
) -> io::Result<()> {
self.update_once(terminal);
terminal.draw(|frame| {
let area = frame.area();
let mut drawer = ComponentDrawer::new(frame, area);
self.draw_root(&mut drawer);
})?;
Ok(())
}
async fn render_loop<T: TerminalImpl<Event = crossterm::event::Event>>(
&mut self,
terminal: &mut Terminal<T>,
) -> io::Result<()> {
let mut skip_render = false;
loop {
if !skip_render {
self.render(terminal)?;
}
if self.system_context.mouse_enabled()
&& self.system_context.input.needs_any_motion_upgrade()
{
terminal.enable_any_motion()?;
self.system_context.input.mark_any_motion_enabled();
}
skip_render = false;
if self.system_context.should_exit() {
break;
}
match select(
self.root_component.wait().boxed_local(),
terminal.next_event().boxed_local(),
)
.await
{
Either::Left(((), _)) => continue,
Either::Right((Some(event), _)) => {
if should_quit_on_ctrl_c(&self.system_context, &event) {
break;
}
let repaint = self.system_context.input.dispatch(event);
if repaint || self.system_context.should_exit() {
continue;
}
skip_render = true;
continue;
}
Either::Right((None, _)) => break,
}
}
Ok(())
}
}
pub(crate) async fn render_loop<E: ElementRepr>(
mut element: E,
mut terminal: Terminal,
run: RunOptions,
) -> io::Result<()> {
let helper = element.helper();
let mut tree = Tree::new_with_mouse(element.props_mut(), helper, run.mouse);
let _restore_guard = RestoreGuard { mouse: run.mouse };
tree.render_loop(&mut terminal).await
}
#[cfg(test)]
mod tests {
use super::*;
use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
fn ctrl_c_event() -> Event {
Event::Key(KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL))
}
#[test]
fn ctrl_c_quits_by_default() {
assert!(should_quit_on_ctrl_c(
&SystemContext::new_with_mouse(false),
&ctrl_c_event()
));
}
#[test]
fn ctrl_c_is_dispatched_when_auto_quit_is_disabled() {
let mut system_context = SystemContext::new_with_mouse(false);
system_context.set_auto_quit_on_ctrl_c(false);
assert!(!should_quit_on_ctrl_c(&system_context, &ctrl_c_event()));
}
#[test]
fn regular_key_does_not_quit() {
let event = Event::Key(KeyEvent::new(KeyCode::Char('c'), KeyModifiers::NONE));
assert!(!should_quit_on_ctrl_c(
&SystemContext::new_with_mouse(false),
&event
));
}
use crate::{
AnyElement, Hooks, UseEventHandler,
components::Text,
input::{EventOptions, EventPriority, EventResult, EventScope},
terminal::TerminalImpl,
};
use crossterm::event::{MouseEvent, MouseEventKind};
use futures::{StreamExt, stream::BoxStream};
use std::sync::{
Arc,
atomic::{AtomicUsize, Ordering},
};
struct ScriptedTerminal {
events: Vec<Event>,
draws: Arc<AtomicUsize>,
motion_upgrades: Arc<AtomicUsize>,
}
impl TerminalImpl for ScriptedTerminal {
type Event = Event;
fn event_stream(&mut self) -> io::Result<BoxStream<'static, Self::Event>> {
Ok(futures::stream::iter(std::mem::take(&mut self.events)).boxed())
}
fn received_ctrl_c(event: Self::Event) -> bool {
CrossTerminal::received_ctrl_c(event)
}
fn enable_any_motion(&mut self) -> io::Result<()> {
self.motion_upgrades.fetch_add(1, Ordering::Relaxed);
Ok(())
}
fn draw<F>(&mut self, _f: F) -> io::Result<()>
where
F: FnOnce(&mut ratatui::Frame),
{
self.draws.fetch_add(1, Ordering::Relaxed);
Ok(())
}
fn insert_before<F>(&mut self, _height: u16, _draw_fn: F) -> io::Result<()>
where
F: FnOnce(&mut ratatui::buffer::Buffer),
{
Ok(())
}
}
fn moved(col: u16, row: u16) -> Event {
Event::Mouse(MouseEvent {
kind: MouseEventKind::Moved,
column: col,
row,
modifiers: KeyModifiers::NONE,
})
}
#[crate::component]
fn ConsumeMoved(mut hooks: Hooks) -> impl Into<AnyElement<'static>> {
hooks.use_event_handler_with_options(
EventScope::Current,
EventPriority::Normal,
EventOptions { hit_test: false },
|event| {
if matches!(
event,
Event::Mouse(crossterm::event::MouseEvent {
kind: MouseEventKind::Moved,
..
})
) {
EventResult::Consumed
} else {
EventResult::Ignored
}
},
);
crate::element!(Text(text: "x"))
}
#[tokio::test]
async fn moved_without_consumer_skips_re_render() {
let draws = Arc::new(AtomicUsize::new(0));
let mut terminal = Terminal::new(ScriptedTerminal {
events: vec![moved(1, 1), moved(2, 2), ctrl_c_event()],
draws: draws.clone(),
motion_upgrades: Arc::default(),
})
.unwrap();
let mut el = crate::element!(Text(text: "x"));
let helper = el.helper();
let mut tree = Tree::new_with_mouse(el.props_mut(), helper, false);
tree.render_loop(&mut terminal).await.unwrap();
assert_eq!(draws.load(Ordering::Relaxed), 1);
}
#[tokio::test]
async fn moved_with_consumer_re_renders() {
let draws = Arc::new(AtomicUsize::new(0));
let mut terminal = Terminal::new(ScriptedTerminal {
events: vec![moved(1, 1), moved(2, 2), ctrl_c_event()],
draws: draws.clone(),
motion_upgrades: Arc::default(),
})
.unwrap();
let mut el = crate::element!(ConsumeMoved);
let helper = el.helper();
let mut tree = Tree::new_with_mouse(el.props_mut(), helper, false);
tree.render_loop(&mut terminal).await.unwrap();
assert_eq!(draws.load(Ordering::Relaxed), 3);
}
#[test]
fn run_options_default_disables_mouse() {
assert!(!crate::RunOptions::default().mouse);
}
#[crate::component]
fn HoverOnMount(mut hooks: Hooks) -> impl Into<AnyElement<'static>> {
use crate::UseHover;
let _ = hooks.use_hover();
crate::element!(Text(text: "hover"))
}
#[tokio::test]
async fn hover_on_mount_enables_motion_before_first_event() {
let motion_upgrades = Arc::new(AtomicUsize::new(0));
let mut terminal = Terminal::new(ScriptedTerminal {
events: vec![ctrl_c_event()],
draws: Arc::default(),
motion_upgrades: motion_upgrades.clone(),
})
.unwrap();
let mut el = crate::element!(HoverOnMount);
let helper = el.helper();
let mut tree = Tree::new_with_mouse(el.props_mut(), helper, true);
tree.render_loop(&mut terminal).await.unwrap();
assert_eq!(motion_upgrades.load(Ordering::Relaxed), 1);
}
#[test]
fn tree_with_mouse_configures_system_context() {
let mut el = crate::element!(Text(text: "x"));
let helper = el.helper();
let tree = Tree::new_with_mouse(el.props_mut(), helper, true);
assert!(tree.system_context.mouse_enabled());
}
}