From a50ecaee6863d8c9a531ba936aa2ae9d2c09b56b Mon Sep 17 00:00:00 2001 From: Joscha Date: Wed, 23 Feb 2022 17:53:26 +0100 Subject: [PATCH] Prepare ui structure --- Cargo.lock | 2 + cove-tui/Cargo.toml | 4 +- cove-tui/src/main.rs | 24 ++----- cove-tui/src/ui.rs | 146 ++++++++++++++++++++++++++++++++++++------- 4 files changed, 130 insertions(+), 46 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 91b1262..35a3c3f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -197,6 +197,7 @@ dependencies = [ "clap", "cove-core", "crossterm", + "futures", "palette", "thiserror", "tokio", @@ -222,6 +223,7 @@ checksum = "c85525306c4291d1b73ce93c8acf9c339f9b213aef6c1d85c3830cbf1c16325c" dependencies = [ "bitflags", "crossterm_winapi", + "futures-core", "libc", "mio", "parking_lot", diff --git a/cove-tui/Cargo.toml b/cove-tui/Cargo.toml index 0a20ac3..23f1439 100644 --- a/cove-tui/Cargo.toml +++ b/cove-tui/Cargo.toml @@ -7,8 +7,8 @@ edition = "2021" anyhow = "1.0.53" clap = { version = "3.1.0", features = ["derive"] } cove-core = { path = "../cove-core" } -crossterm = "0.22.1" -# futures = "0.3.21" +crossterm = { version = "0.22.1", features = ["event-stream"] } +futures = "0.3.21" palette = "0.6.0" # serde_json = "1.0.78" thiserror = "1.0.30" diff --git a/cove-tui/src/main.rs b/cove-tui/src/main.rs index beba029..e6ab493 100644 --- a/cove-tui/src/main.rs +++ b/cove-tui/src/main.rs @@ -5,31 +5,15 @@ mod room; mod textline; mod ui; -use std::io::{self, Stdout}; +use std::io; -use crossterm::event::{DisableMouseCapture, EnableMouseCapture, Event, KeyCode}; +use crossterm::event::{DisableMouseCapture, EnableMouseCapture}; use crossterm::execute; use crossterm::terminal::{EnterAlternateScreen, LeaveAlternateScreen}; use tui::backend::CrosstermBackend; use tui::Terminal; use ui::Ui; -async fn run(terminal: &mut Terminal>) -> anyhow::Result<()> { - let mut ui = Ui::default(); - loop { - ui.render_to_terminal(terminal).await?; - - let event = crossterm::event::read()?; - - if let Event::Key(k) = event { - if k.code == KeyCode::Esc { - break; - } - } - } - Ok(()) -} - #[tokio::main] async fn main() -> anyhow::Result<()> { let mut terminal = Terminal::new(CrosstermBackend::new(io::stdout()))?; @@ -42,14 +26,14 @@ async fn main() -> anyhow::Result<()> { )?; // Defer error handling so the terminal always gets restored properly - let result = run(&mut terminal).await; + let result = Ui::run(&mut terminal).await; - crossterm::terminal::disable_raw_mode()?; execute!( terminal.backend_mut(), LeaveAlternateScreen, DisableMouseCapture )?; + crossterm::terminal::disable_raw_mode()?; result?; diff --git a/cove-tui/src/ui.rs b/cove-tui/src/ui.rs index b331afd..7289d65 100644 --- a/cove-tui/src/ui.rs +++ b/cove-tui/src/ui.rs @@ -1,45 +1,143 @@ -use std::collections::HashMap; use std::io::Stdout; -use std::sync::Arc; -use tokio::sync::Mutex; +use crossterm::event::{Event, EventStream, KeyCode, KeyEvent, MouseEvent}; +use futures::StreamExt; +use tokio::sync::mpsc::{self, UnboundedReceiver, UnboundedSender}; use tui::backend::CrosstermBackend; use tui::widgets::Paragraph; -use tui::Terminal; +use tui::{Frame, Terminal}; -use crate::room::Room; +pub type Backend = CrosstermBackend; -pub enum Overlay { - Error(String), - ChooseRoom(String), +#[derive(Debug)] +pub enum UiEvent { + Term(Event), + Redraw, +} + +enum EventHandleResult { + Continue, + Stop, } -#[derive(Default)] pub struct Ui { - rooms: HashMap>>, - room: Option>>, - overlay: Option, + event_tx: UnboundedSender, + rooms_width: i32, + log: Vec, } impl Ui { - pub async fn render_to_terminal( + fn new(event_tx: UnboundedSender) -> Self { + Self { + event_tx, + rooms_width: 24, + log: vec!["Hello world!".to_string()], + } + } + + pub async fn run(terminal: &mut Terminal) -> anyhow::Result<()> { + let (event_tx, mut event_rx) = mpsc::unbounded_channel(); + let mut ui = Self::new(event_tx.clone()); + + tokio::select! { + e = ui.run_main(terminal, &mut event_rx) => e, + e = Self::shovel_crossterm_events(event_tx) => e, + } + } + + async fn shovel_crossterm_events(tx: UnboundedSender) -> anyhow::Result<()> { + // Implemented manually because UnboundedSender doesn't implement the Sink trait + let mut stream = EventStream::new(); + while let Some(event) = stream.next().await { + tx.send(UiEvent::Term(event?))?; + } + Ok(()) + } + + async fn run_main( &mut self, - terminal: &mut Terminal>, + terminal: &mut Terminal, + event_rx: &mut UnboundedReceiver, ) -> anyhow::Result<()> { - terminal.autoresize()?; + loop { + // 1. Render current state + terminal.autoresize()?; - let mut frame = terminal.get_frame(); - frame.render_widget(Paragraph::new("Hello world!"), frame.size()); + let mut frame = terminal.get_frame(); + self.render(&mut frame).await?; - // Do a little dance to please the borrow checker - let cursor = frame.cursor(); - drop(frame); - terminal.set_cursor_opt(cursor)?; + // Do a little dance to please the borrow checker + let cursor = frame.cursor(); + drop(frame); + terminal.set_cursor_opt(cursor)?; - terminal.flush()?; - terminal.flush_backend()?; - terminal.swap_buffers(); + terminal.flush()?; + terminal.flush_backend()?; + terminal.swap_buffers(); + // 2. Handle events + let event = event_rx.recv().await; + self.log.push(format!("{event:?}")); + let result = match event { + Some(UiEvent::Term(Event::Key(event))) => self.handle_key_event(event).await?, + Some(UiEvent::Term(Event::Mouse(event))) => self.handle_mouse_event(event).await?, + Some(UiEvent::Term(Event::Resize(_, _))) => EventHandleResult::Continue, + Some(UiEvent::Redraw) => EventHandleResult::Continue, + None => EventHandleResult::Stop, + }; + match result { + EventHandleResult::Continue => {} + EventHandleResult::Stop => break Ok(()), + } + } + } + + async fn handle_key_event(&mut self, event: KeyEvent) -> anyhow::Result { + Ok(match event.code { + // KeyCode::Backspace => todo!(), + // KeyCode::Enter => todo!(), + // KeyCode::Left => todo!(), + // KeyCode::Right => todo!(), + // KeyCode::Up => todo!(), + // KeyCode::Down => todo!(), + // KeyCode::Home => todo!(), + // KeyCode::End => todo!(), + // KeyCode::PageUp => todo!(), + // KeyCode::PageDown => todo!(), + // KeyCode::Tab => todo!(), + // KeyCode::BackTab => todo!(), + // KeyCode::Delete => todo!(), + // KeyCode::Insert => todo!(), + // KeyCode::F(_) => todo!(), + // KeyCode::Char(_) => todo!(), + // KeyCode::Null => todo!(), + KeyCode::Esc => EventHandleResult::Stop, + _ => EventHandleResult::Continue, + }) + } + + async fn handle_mouse_event(&mut self, event: MouseEvent) -> anyhow::Result { + Ok(match event.kind { + // MouseEventKind::Down(_) => todo!(), + // MouseEventKind::Up(_) => todo!(), + // MouseEventKind::Drag(_) => todo!(), + // MouseEventKind::Moved => todo!(), + // MouseEventKind::ScrollDown => todo!(), + // MouseEventKind::ScrollUp => todo!(), + _ => EventHandleResult::Continue, + }) + } + + async fn render(&mut self, frame: &mut Frame<'_, Backend>) -> anyhow::Result<()> { + let scroll = if self.log.len() as u16 > frame.size().height { + self.log.len() as u16 - frame.size().height + } else { + 0 + }; + frame.render_widget( + Paragraph::new(self.log.join("\n")).scroll((scroll, 0)), + frame.size(), + ); Ok(()) } }