Prepare ui structure

This commit is contained in:
Joscha 2022-02-23 17:53:26 +01:00
parent 9c5f027898
commit a50ecaee68
4 changed files with 130 additions and 46 deletions

2
Cargo.lock generated
View file

@ -197,6 +197,7 @@ dependencies = [
"clap", "clap",
"cove-core", "cove-core",
"crossterm", "crossterm",
"futures",
"palette", "palette",
"thiserror", "thiserror",
"tokio", "tokio",
@ -222,6 +223,7 @@ checksum = "c85525306c4291d1b73ce93c8acf9c339f9b213aef6c1d85c3830cbf1c16325c"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"crossterm_winapi", "crossterm_winapi",
"futures-core",
"libc", "libc",
"mio", "mio",
"parking_lot", "parking_lot",

View file

@ -7,8 +7,8 @@ edition = "2021"
anyhow = "1.0.53" anyhow = "1.0.53"
clap = { version = "3.1.0", features = ["derive"] } clap = { version = "3.1.0", features = ["derive"] }
cove-core = { path = "../cove-core" } cove-core = { path = "../cove-core" }
crossterm = "0.22.1" crossterm = { version = "0.22.1", features = ["event-stream"] }
# futures = "0.3.21" futures = "0.3.21"
palette = "0.6.0" palette = "0.6.0"
# serde_json = "1.0.78" # serde_json = "1.0.78"
thiserror = "1.0.30" thiserror = "1.0.30"

View file

@ -5,31 +5,15 @@ mod room;
mod textline; mod textline;
mod ui; 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::execute;
use crossterm::terminal::{EnterAlternateScreen, LeaveAlternateScreen}; use crossterm::terminal::{EnterAlternateScreen, LeaveAlternateScreen};
use tui::backend::CrosstermBackend; use tui::backend::CrosstermBackend;
use tui::Terminal; use tui::Terminal;
use ui::Ui; use ui::Ui;
async fn run(terminal: &mut Terminal<CrosstermBackend<Stdout>>) -> 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] #[tokio::main]
async fn main() -> anyhow::Result<()> { async fn main() -> anyhow::Result<()> {
let mut terminal = Terminal::new(CrosstermBackend::new(io::stdout()))?; 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 // 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!( execute!(
terminal.backend_mut(), terminal.backend_mut(),
LeaveAlternateScreen, LeaveAlternateScreen,
DisableMouseCapture DisableMouseCapture
)?; )?;
crossterm::terminal::disable_raw_mode()?;
result?; result?;

View file

@ -1,45 +1,143 @@
use std::collections::HashMap;
use std::io::Stdout; 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::backend::CrosstermBackend;
use tui::widgets::Paragraph; use tui::widgets::Paragraph;
use tui::Terminal; use tui::{Frame, Terminal};
use crate::room::Room; pub type Backend = CrosstermBackend<Stdout>;
pub enum Overlay { #[derive(Debug)]
Error(String), pub enum UiEvent {
ChooseRoom(String), Term(Event),
Redraw,
}
enum EventHandleResult {
Continue,
Stop,
} }
#[derive(Default)]
pub struct Ui { pub struct Ui {
rooms: HashMap<String, Arc<Mutex<Room>>>, event_tx: UnboundedSender<UiEvent>,
room: Option<Arc<Mutex<Room>>>, rooms_width: i32,
overlay: Option<Overlay>, log: Vec<String>,
} }
impl Ui { impl Ui {
pub async fn render_to_terminal( fn new(event_tx: UnboundedSender<UiEvent>) -> Self {
Self {
event_tx,
rooms_width: 24,
log: vec!["Hello world!".to_string()],
}
}
pub async fn run(terminal: &mut Terminal<Backend>) -> 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<UiEvent>) -> 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, &mut self,
terminal: &mut Terminal<CrosstermBackend<Stdout>>, terminal: &mut Terminal<Backend>,
event_rx: &mut UnboundedReceiver<UiEvent>,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
terminal.autoresize()?; loop {
// 1. Render current state
terminal.autoresize()?;
let mut frame = terminal.get_frame(); let mut frame = terminal.get_frame();
frame.render_widget(Paragraph::new("Hello world!"), frame.size()); self.render(&mut frame).await?;
// Do a little dance to please the borrow checker // Do a little dance to please the borrow checker
let cursor = frame.cursor(); let cursor = frame.cursor();
drop(frame); drop(frame);
terminal.set_cursor_opt(cursor)?; terminal.set_cursor_opt(cursor)?;
terminal.flush()?; terminal.flush()?;
terminal.flush_backend()?; terminal.flush_backend()?;
terminal.swap_buffers(); 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<EventHandleResult> {
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<EventHandleResult> {
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(()) Ok(())
} }
} }