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 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
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 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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue