Add basic "join room" overlay
This commit is contained in:
parent
8d1b1951f4
commit
3ac3bbb99e
6 changed files with 194 additions and 60 deletions
|
|
@ -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<Stdout>;
|
||||
|
|
@ -32,11 +37,15 @@ enum EventHandleResult {
|
|||
Stop,
|
||||
}
|
||||
|
||||
enum Overlay {
|
||||
JoinRoom(JoinRoomState),
|
||||
}
|
||||
|
||||
pub struct Ui {
|
||||
event_tx: UnboundedSender<UiEvent>,
|
||||
rooms: HashMap<String, Arc<Mutex<Room>>>,
|
||||
rooms_state: RoomsState,
|
||||
log: Vec<String>,
|
||||
overlay: Option<Overlay>,
|
||||
}
|
||||
|
||||
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<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,
|
||||
})
|
||||
const CONTINUE: anyhow::Result<EventHandleResult> = Ok(EventHandleResult::Continue);
|
||||
const STOP: anyhow::Result<EventHandleResult> = 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<EventHandleResult> {
|
||||
|
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
9
cove-tui/src/ui/input.rs
Normal file
9
cove-tui/src/ui/input.rs
Normal 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
14
cove-tui/src/ui/layout.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
9
cove-tui/src/ui/overlays.rs
Normal file
9
cove-tui/src/ui/overlays.rs
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
mod join_room;
|
||||
|
||||
pub use join_room::*;
|
||||
|
||||
pub enum OverlayReaction {
|
||||
Handled,
|
||||
Close,
|
||||
JoinRoom(String),
|
||||
}
|
||||
46
cove-tui/src/ui/overlays/join_room.rs
Normal file
46
cove-tui/src/ui/overlays/join_room.rs
Normal 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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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<B: Backend>(&self, f: &mut Frame<B>, area: Rect) {
|
||||
let prefix = self.content.chars().take(self.cursor).collect::<String>();
|
||||
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<Self::Reaction> {
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue