Add basic "join room" overlay

This commit is contained in:
Joscha 2022-02-26 13:11:51 +01:00
parent 8d1b1951f4
commit 3ac3bbb99e
6 changed files with 194 additions and 60 deletions

View file

@ -1,3 +1,6 @@
mod input;
mod layout;
mod overlays;
mod rooms; mod rooms;
mod textline; mod textline;
@ -12,11 +15,13 @@ use tokio::sync::mpsc::{self, UnboundedReceiver, UnboundedSender};
use tokio::sync::Mutex; use tokio::sync::Mutex;
use tui::backend::CrosstermBackend; use tui::backend::CrosstermBackend;
use tui::layout::{Constraint, Direction, Layout}; use tui::layout::{Constraint, Direction, Layout};
use tui::widgets::Paragraph;
use tui::{Frame, Terminal}; use tui::{Frame, Terminal};
use crate::room::Room; use crate::room::Room;
use crate::ui::overlays::OverlayReaction;
use self::input::EventHandler;
use self::overlays::{JoinRoom, JoinRoomState};
use self::rooms::{Rooms, RoomsState}; use self::rooms::{Rooms, RoomsState};
pub type Backend = CrosstermBackend<Stdout>; pub type Backend = CrosstermBackend<Stdout>;
@ -32,11 +37,15 @@ enum EventHandleResult {
Stop, Stop,
} }
enum Overlay {
JoinRoom(JoinRoomState),
}
pub struct Ui { pub struct Ui {
event_tx: UnboundedSender<UiEvent>, event_tx: UnboundedSender<UiEvent>,
rooms: HashMap<String, Arc<Mutex<Room>>>, rooms: HashMap<String, Arc<Mutex<Room>>>,
rooms_state: RoomsState, rooms_state: RoomsState,
log: Vec<String>, overlay: Option<Overlay>,
} }
impl Ui { impl Ui {
@ -45,7 +54,7 @@ impl Ui {
event_tx, event_tx,
rooms: HashMap::new(), rooms: HashMap::new(),
rooms_state: RoomsState::default(), rooms_state: RoomsState::default(),
log: vec!["Hello world!".to_string()], overlay: None,
} }
} }
@ -95,7 +104,6 @@ impl Ui {
None => return Ok(()), None => return Ok(()),
}; };
loop { loop {
self.log.push(format!("{event:?}"));
let result = match event { let result = match event {
UiEvent::Term(Event::Key(event)) => self.handle_key_event(event).await?, UiEvent::Term(Event::Key(event)) => self.handle_key_event(event).await?,
UiEvent::Term(Event::Mouse(event)) => self.handle_mouse_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<EventHandleResult> { async fn handle_key_event(&mut self, event: KeyEvent) -> anyhow::Result<EventHandleResult> {
Ok(match event.code { const CONTINUE: anyhow::Result<EventHandleResult> = Ok(EventHandleResult::Continue);
// KeyCode::Backspace => todo!(), const STOP: anyhow::Result<EventHandleResult> = Ok(EventHandleResult::Stop);
// KeyCode::Enter => todo!(),
// KeyCode::Left => todo!(), // Overlay
// KeyCode::Right => todo!(), if let Some(overlay) = &mut self.overlay {
// KeyCode::Up => todo!(), let reaction = match overlay {
// KeyCode::Down => todo!(), Overlay::JoinRoom(state) => state.handle_key(event),
// KeyCode::Home => todo!(), };
// KeyCode::End => todo!(), if let Some(reaction) = reaction {
// KeyCode::PageUp => todo!(), match reaction {
// KeyCode::PageDown => todo!(), OverlayReaction::Handled => {}
// KeyCode::Tab => todo!(), OverlayReaction::Close => self.overlay = None,
// KeyCode::BackTab => todo!(), OverlayReaction::JoinRoom(name) => todo!(),
// KeyCode::Delete => todo!(), }
// KeyCode::Insert => todo!(), }
// KeyCode::F(_) => todo!(), return CONTINUE;
// KeyCode::Char(_) => todo!(), }
// KeyCode::Null => todo!(),
KeyCode::Esc => EventHandleResult::Stop, // Main panel
_ => EventHandleResult::Continue, // 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<EventHandleResult> { async fn handle_mouse_event(&mut self, event: MouseEvent) -> anyhow::Result<EventHandleResult> {
@ -155,27 +172,36 @@ impl Ui {
} }
async fn render(&mut self, frame: &mut Frame<'_, Backend>) -> anyhow::Result<()> { 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) .direction(Direction::Horizontal)
.constraints([ .constraints([
Constraint::Length(self.rooms_state.width()), Constraint::Length(self.rooms_state.width()), // Rooms list
Constraint::Min(0), 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); // Rooms list
// frame.render_stateful_widget(Rooms::dummy(), outer[0], &mut self.rooms_state); frame.render_stateful_widget(
Rooms::new(&self.rooms),
let scroll = if self.log.len() as u16 > outer[1].height { rooms_list_area,
self.log.len() as u16 - outer[1].height &mut self.rooms_state,
} else {
0
};
frame.render_widget(
Paragraph::new(self.log.join("\n")).scroll((scroll, 0)),
outer[1],
); );
// 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(()) Ok(())
} }
} }

9
cove-tui/src/ui/input.rs Normal file
View file

@ -0,0 +1,9 @@
use crossterm::event::KeyEvent;
pub trait EventHandler {
type Reaction;
fn handle_key(&mut self, event: KeyEvent) -> Option<Self::Reaction>;
// TODO Add method to show currently accepted keys for F1 help
}

14
cove-tui/src/ui/layout.rs Normal file
View file

@ -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,
}
}

View file

@ -0,0 +1,9 @@
mod join_room;
pub use join_room::*;
pub enum OverlayReaction {
Handled,
Close,
JoinRoom(String),
}

View file

@ -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<Self::Reaction> {
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,
})
}
}

View file

@ -1,6 +1,6 @@
use std::cmp; use std::cmp;
use crossterm::event::{Event, KeyCode}; use crossterm::event::{KeyCode, KeyEvent};
use tui::backend::Backend; use tui::backend::Backend;
use tui::buffer::Buffer; use tui::buffer::Buffer;
use tui::layout::Rect; use tui::layout::Rect;
@ -8,6 +8,8 @@ use tui::widgets::{Paragraph, StatefulWidget, Widget};
use tui::Frame; use tui::Frame;
use unicode_width::UnicodeWidthStr; use unicode_width::UnicodeWidthStr;
use super::input::EventHandler;
/// A simple single-line text box. /// A simple single-line text box.
pub struct TextLine; pub struct TextLine;
@ -28,6 +30,11 @@ pub struct TextLineState {
} }
impl 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<B: Backend>(&self, f: &mut Frame<B>, area: Rect) { pub fn set_cursor<B: Backend>(&self, f: &mut Frame<B>, area: Rect) {
let prefix = self.content.chars().take(self.cursor).collect::<String>(); let prefix = self.content.chars().take(self.cursor).collect::<String>();
let position = prefix.width() as u16; let position = prefix.width() as u16;
@ -64,27 +71,50 @@ impl TextLineState {
.map(|(i, _)| i) .map(|(i, _)| i)
.unwrap_or_else(|| self.content.len()) .unwrap_or_else(|| self.content.len())
} }
}
pub fn process_input(&mut self, event: Event) { pub enum TextLineReaction {
if let Event::Key(k) = event { Handled,
match k.code { Close,
KeyCode::Backspace if self.cursor > 0 => { }
self.move_cursor_left();
self.content.remove(self.cursor_byte_offset()); impl EventHandler for TextLineState {
} type Reaction = TextLineReaction;
KeyCode::Left => self.move_cursor_left(),
KeyCode::Right => self.move_cursor_right(), fn handle_key(&mut self, event: KeyEvent) -> Option<Self::Reaction> {
KeyCode::Home => self.move_cursor_start(), match event.code {
KeyCode::End => self.move_cursor_end(), KeyCode::Backspace if self.cursor > 0 => {
KeyCode::Delete if self.cursor < self.chars() => { self.move_cursor_left();
self.content.remove(self.cursor_byte_offset()); 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();
}
_ => {}
} }
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,
} }
} }
} }