From 3ac3bbb99e94ed554ba3e32a14e545585e668627 Mon Sep 17 00:00:00 2001 From: Joscha Date: Sat, 26 Feb 2022 13:11:51 +0100 Subject: [PATCH] Add basic "join room" overlay --- cove-tui/src/ui.rs | 106 ++++++++++++++++---------- cove-tui/src/ui/input.rs | 9 +++ cove-tui/src/ui/layout.rs | 14 ++++ cove-tui/src/ui/overlays.rs | 9 +++ cove-tui/src/ui/overlays/join_room.rs | 46 +++++++++++ cove-tui/src/ui/textline.rs | 70 ++++++++++++----- 6 files changed, 194 insertions(+), 60 deletions(-) create mode 100644 cove-tui/src/ui/input.rs create mode 100644 cove-tui/src/ui/layout.rs create mode 100644 cove-tui/src/ui/overlays.rs create mode 100644 cove-tui/src/ui/overlays/join_room.rs diff --git a/cove-tui/src/ui.rs b/cove-tui/src/ui.rs index 518d8a6..2aa262c 100644 --- a/cove-tui/src/ui.rs +++ b/cove-tui/src/ui.rs @@ -1,3 +1,6 @@ +mod input; +mod layout; +mod overlays; mod rooms; mod textline; @@ -12,11 +15,13 @@ use tokio::sync::mpsc::{self, UnboundedReceiver, UnboundedSender}; use tokio::sync::Mutex; use tui::backend::CrosstermBackend; use tui::layout::{Constraint, Direction, Layout}; -use tui::widgets::Paragraph; use tui::{Frame, Terminal}; use crate::room::Room; +use crate::ui::overlays::OverlayReaction; +use self::input::EventHandler; +use self::overlays::{JoinRoom, JoinRoomState}; use self::rooms::{Rooms, RoomsState}; pub type Backend = CrosstermBackend; @@ -32,11 +37,15 @@ enum EventHandleResult { Stop, } +enum Overlay { + JoinRoom(JoinRoomState), +} + pub struct Ui { event_tx: UnboundedSender, rooms: HashMap>>, rooms_state: RoomsState, - log: Vec, + overlay: Option, } impl Ui { @@ -45,7 +54,7 @@ impl Ui { event_tx, rooms: HashMap::new(), rooms_state: RoomsState::default(), - log: vec!["Hello world!".to_string()], + overlay: None, } } @@ -95,7 +104,6 @@ impl Ui { None => return Ok(()), }; loop { - self.log.push(format!("{event:?}")); let result = match event { UiEvent::Term(Event::Key(event)) => self.handle_key_event(event).await?, UiEvent::Term(Event::Mouse(event)) => self.handle_mouse_event(event).await?, @@ -116,27 +124,36 @@ impl Ui { } 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, - }) + const CONTINUE: anyhow::Result = Ok(EventHandleResult::Continue); + const STOP: anyhow::Result = Ok(EventHandleResult::Stop); + + // Overlay + if let Some(overlay) = &mut self.overlay { + let reaction = match overlay { + Overlay::JoinRoom(state) => state.handle_key(event), + }; + if let Some(reaction) = reaction { + match reaction { + OverlayReaction::Handled => {} + OverlayReaction::Close => self.overlay = None, + OverlayReaction::JoinRoom(name) => todo!(), + } + } + return CONTINUE; + } + + // Main panel + // TODO Implement + + // Otherwise, global bindings + match event.code { + KeyCode::Char('q') => STOP, + KeyCode::Char('c') => { + self.overlay = Some(Overlay::JoinRoom(JoinRoomState::default())); + CONTINUE + } + _ => CONTINUE, + } } async fn handle_mouse_event(&mut self, event: MouseEvent) -> anyhow::Result { @@ -155,27 +172,36 @@ impl Ui { } async fn render(&mut self, frame: &mut Frame<'_, Backend>) -> anyhow::Result<()> { - let outer = Layout::default() + let entire_area = frame.size(); + let areas = Layout::default() .direction(Direction::Horizontal) .constraints([ - Constraint::Length(self.rooms_state.width()), - Constraint::Min(0), + Constraint::Length(self.rooms_state.width()), // Rooms list + Constraint::Min(1), // Main panel ]) - .split(frame.size()); + .split(entire_area); + let rooms_list_area = areas[0]; + let main_panel_area = areas[1]; - frame.render_stateful_widget(Rooms::new(&self.rooms), outer[0], &mut self.rooms_state); - // frame.render_stateful_widget(Rooms::dummy(), outer[0], &mut self.rooms_state); - - let scroll = if self.log.len() as u16 > outer[1].height { - self.log.len() as u16 - outer[1].height - } else { - 0 - }; - frame.render_widget( - Paragraph::new(self.log.join("\n")).scroll((scroll, 0)), - outer[1], + // Rooms list + frame.render_stateful_widget( + Rooms::new(&self.rooms), + rooms_list_area, + &mut self.rooms_state, ); + // Main panel + // TODO Implement + + // Overlays + if let Some(overlay) = &mut self.overlay { + match overlay { + Overlay::JoinRoom(state) => { + frame.render_stateful_widget(JoinRoom, entire_area, state) + } + } + } + Ok(()) } } diff --git a/cove-tui/src/ui/input.rs b/cove-tui/src/ui/input.rs new file mode 100644 index 0000000..b957f97 --- /dev/null +++ b/cove-tui/src/ui/input.rs @@ -0,0 +1,9 @@ +use crossterm::event::KeyEvent; + +pub trait EventHandler { + type Reaction; + + fn handle_key(&mut self, event: KeyEvent) -> Option; + + // TODO Add method to show currently accepted keys for F1 help +} diff --git a/cove-tui/src/ui/layout.rs b/cove-tui/src/ui/layout.rs new file mode 100644 index 0000000..a0fc103 --- /dev/null +++ b/cove-tui/src/ui/layout.rs @@ -0,0 +1,14 @@ +use tui::layout::Rect; + +pub fn centered(width: u16, height: u16, area: Rect) -> Rect { + let width = width.min(area.width); + let height = height.min(area.height); + let dx = (area.width - width) / 2; + let dy = (area.height - height) / 2; + Rect { + x: area.x + dx, + y: area.y + dy, + width, + height, + } +} diff --git a/cove-tui/src/ui/overlays.rs b/cove-tui/src/ui/overlays.rs new file mode 100644 index 0000000..cf89e76 --- /dev/null +++ b/cove-tui/src/ui/overlays.rs @@ -0,0 +1,9 @@ +mod join_room; + +pub use join_room::*; + +pub enum OverlayReaction { + Handled, + Close, + JoinRoom(String), +} diff --git a/cove-tui/src/ui/overlays/join_room.rs b/cove-tui/src/ui/overlays/join_room.rs new file mode 100644 index 0000000..e7ab91b --- /dev/null +++ b/cove-tui/src/ui/overlays/join_room.rs @@ -0,0 +1,46 @@ +use crossterm::event::{KeyCode, KeyEvent}; +use tui::buffer::Buffer; +use tui::layout::Rect; +use tui::widgets::{Block, Borders, StatefulWidget, Widget}; + +use crate::ui::input::EventHandler; +use crate::ui::layout; +use crate::ui::textline::{TextLine, TextLineReaction, TextLineState}; + +use super::OverlayReaction; + +pub struct JoinRoom; + +impl StatefulWidget for JoinRoom { + type State = JoinRoomState; + + fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) { + let area = layout::centered(50, 3, area); + + let block = Block::default().title("Join room").borders(Borders::ALL); + let inner_area = block.inner(area); + block.render(area, buf); + + TextLine.render(inner_area, buf, &mut state.room); + } +} + +#[derive(Debug, Default)] +pub struct JoinRoomState { + room: TextLineState, +} + +impl EventHandler for JoinRoomState { + type Reaction = OverlayReaction; + + fn handle_key(&mut self, event: KeyEvent) -> Option { + if event.code == KeyCode::Enter { + return Some(Self::Reaction::JoinRoom(self.room.content())); + } + + self.room.handle_key(event).map(|r| match r { + TextLineReaction::Handled => Self::Reaction::Handled, + TextLineReaction::Close => Self::Reaction::Close, + }) + } +} diff --git a/cove-tui/src/ui/textline.rs b/cove-tui/src/ui/textline.rs index d7e960a..673074e 100644 --- a/cove-tui/src/ui/textline.rs +++ b/cove-tui/src/ui/textline.rs @@ -1,6 +1,6 @@ use std::cmp; -use crossterm::event::{Event, KeyCode}; +use crossterm::event::{KeyCode, KeyEvent}; use tui::backend::Backend; use tui::buffer::Buffer; use tui::layout::Rect; @@ -8,6 +8,8 @@ use tui::widgets::{Paragraph, StatefulWidget, Widget}; use tui::Frame; use unicode_width::UnicodeWidthStr; +use super::input::EventHandler; + /// A simple single-line text box. pub struct TextLine; @@ -28,6 +30,11 @@ pub struct TextLineState { } impl TextLineState { + pub fn content(&self) -> String { + self.content.clone() + } + + /// Set a frame's cursor position to this text line's cursor position pub fn set_cursor(&self, f: &mut Frame, area: Rect) { let prefix = self.content.chars().take(self.cursor).collect::(); let position = prefix.width() as u16; @@ -64,27 +71,50 @@ impl TextLineState { .map(|(i, _)| i) .unwrap_or_else(|| self.content.len()) } +} - pub fn process_input(&mut self, event: Event) { - if let Event::Key(k) = event { - match k.code { - KeyCode::Backspace if self.cursor > 0 => { - self.move_cursor_left(); - self.content.remove(self.cursor_byte_offset()); - } - KeyCode::Left => self.move_cursor_left(), - KeyCode::Right => self.move_cursor_right(), - KeyCode::Home => self.move_cursor_start(), - KeyCode::End => self.move_cursor_end(), - KeyCode::Delete if self.cursor < self.chars() => { - self.content.remove(self.cursor_byte_offset()); - } - KeyCode::Char(c) => { - self.content.insert(self.cursor_byte_offset(), c); - self.move_cursor_right(); - } - _ => {} +pub enum TextLineReaction { + Handled, + Close, +} + +impl EventHandler for TextLineState { + type Reaction = TextLineReaction; + + fn handle_key(&mut self, event: KeyEvent) -> Option { + match event.code { + KeyCode::Backspace if self.cursor > 0 => { + self.move_cursor_left(); + self.content.remove(self.cursor_byte_offset()); + Some(TextLineReaction::Handled) } + KeyCode::Left => { + self.move_cursor_left(); + Some(TextLineReaction::Handled) + } + KeyCode::Right => { + self.move_cursor_right(); + Some(TextLineReaction::Handled) + } + KeyCode::Home => { + self.move_cursor_start(); + Some(TextLineReaction::Handled) + } + KeyCode::End => { + self.move_cursor_end(); + Some(TextLineReaction::Handled) + } + KeyCode::Delete if self.cursor < self.chars() => { + self.content.remove(self.cursor_byte_offset()); + Some(TextLineReaction::Handled) + } + KeyCode::Char(c) => { + self.content.insert(self.cursor_byte_offset(), c); + self.move_cursor_right(); + Some(TextLineReaction::Handled) + } + KeyCode::Esc => Some(TextLineReaction::Close), + _ => None, } } }