Show current room state

This commit is contained in:
Joscha 2022-02-27 01:46:19 +01:00
parent f34bf63be4
commit 04d17179a0
6 changed files with 202 additions and 33 deletions

View file

@ -9,6 +9,7 @@ use cove_core::packets::{
use cove_core::{Session, SessionId}; use cove_core::{Session, SessionId};
use tokio::sync::oneshot::{self, Sender}; use tokio::sync::oneshot::{self, Sender};
use tokio::sync::Mutex; use tokio::sync::Mutex;
use tui::widgets::StatefulWidget;
use crate::config::Config; use crate::config::Config;
use crate::never::Never; use crate::never::Never;
@ -41,18 +42,15 @@ pub struct Present {
pub others: HashMap<SessionId, Session>, pub others: HashMap<SessionId, Session>,
} }
enum Status { pub enum Status {
/// No action required by the UI. /// No action required by the UI.
Nominal, Nominal,
/// User must enter a nick. /// User must enter a nick.
NickRequired, NickRequired,
/// Identifying to the server. No action required by the UI.
Identifying,
CouldNotConnect, CouldNotConnect,
InvalidRoom(String), InvalidRoom(String),
InvalidNick(String), InvalidNick(String),
InvalidIdentity(String), InvalidIdentity(String),
InvalidContent(String),
} }
pub struct Room { pub struct Room {
@ -95,6 +93,14 @@ impl Room {
room room
} }
pub fn status(&self) -> &Status {
&self.status
}
pub fn connected(&self) -> bool {
self.connected.is_some()
}
pub fn present(&self) -> Option<&Present> { pub fn present(&self) -> Option<&Present> {
self.present.as_ref() self.present.as_ref()
} }
@ -209,9 +215,7 @@ impl Room {
Rpl::Send(SendRpl::Success { message }) => { Rpl::Send(SendRpl::Success { message }) => {
// TODO Send message to store // TODO Send message to store
} }
Rpl::Send(SendRpl::InvalidContent { reason }) => { Rpl::Send(SendRpl::InvalidContent { reason }) => {}
self.status = Status::InvalidContent(reason.clone());
}
Rpl::Who(WhoRpl { you, others }) => { Rpl::Who(WhoRpl { you, others }) => {
if let Some(present) = &mut self.present { if let Some(present) = &mut self.present {
present.session = you.clone(); present.session = you.clone();

View file

@ -5,6 +5,7 @@ mod pane;
mod room; mod room;
mod rooms; mod rooms;
mod textline; mod textline;
mod styles;
use std::collections::hash_map::Entry; use std::collections::hash_map::Entry;
use std::collections::HashMap; use std::collections::HashMap;

View file

@ -4,41 +4,191 @@ use std::sync::Arc;
use tokio::sync::Mutex; use tokio::sync::Mutex;
use tui::backend::Backend; use tui::backend::Backend;
use tui::layout::Rect; use tui::layout::{Alignment, Constraint, Direction, Layout, Rect};
use tui::widgets::{Block, BorderType, Borders}; use tui::style::Style;
use tui::text::{Span, Spans, Text};
use tui::widgets::{Block, BorderType, Borders, Paragraph};
use tui::Frame; use tui::Frame;
use unicode_width::UnicodeWidthStr;
use crate::room::Room; use crate::room::{Room, Status};
use self::users::Users; use self::users::Users;
use super::textline::{TextLine, TextLineState};
use super::{layout, styles};
enum Main {
Empty,
Connecting,
Identifying,
ChooseNick {
nick: TextLineState,
prev_error: Option<String>,
},
Messages,
FatalError(String),
}
impl Main {
fn choose_nick() -> Self {
Self::ChooseNick {
nick: TextLineState::default(),
prev_error: None,
}
}
fn fatal<S: ToString>(s: S) -> Self {
Self::FatalError(s.to_string())
}
}
pub struct RoomInfo { pub struct RoomInfo {
name: String, name: String,
room: Arc<Mutex<Room>>, room: Arc<Mutex<Room>>,
main: Main,
} }
impl RoomInfo { impl RoomInfo {
pub fn new(name: String, room: Arc<Mutex<Room>>) -> Self { pub fn new(name: String, room: Arc<Mutex<Room>>) -> Self {
Self { name, room } Self {
name,
room,
main: Main::Empty,
}
} }
pub fn name(&self) -> &str { pub fn name(&self) -> &str {
&self.name &self.name
} }
async fn align_main(&mut self) {
let room = self.room.lock().await;
match room.status() {
Status::Nominal if room.connected() && room.present().is_some() => {
if !matches!(self.main, Main::Messages) {
self.main = Main::Messages;
}
}
Status::Nominal if room.connected() => self.main = Main::Connecting,
Status::Nominal => self.main = Main::Identifying,
Status::NickRequired => self.main = Main::choose_nick(),
Status::CouldNotConnect => self.main = Main::fatal("Could not connect to room"),
Status::InvalidRoom(err) => self.main = Main::fatal(format!("Invalid room:\n{err}")),
Status::InvalidNick(err) => {
if let Main::ChooseNick { prev_error, .. } = &mut self.main {
*prev_error = Some(err.clone());
} else {
self.main = Main::choose_nick();
}
}
Status::InvalidIdentity(err) => {
self.main = Main::fatal(format!("Invalid identity:\n{err}"))
}
}
}
pub async fn render_main<B: Backend>(&mut self, frame: &mut Frame<'_, B>, area: Rect) { pub async fn render_main<B: Backend>(&mut self, frame: &mut Frame<'_, B>, area: Rect) {
// TODO Implement self.align_main().await;
frame.render_widget(
Block::default() let areas = Layout::default()
.borders(Borders::TOP) .direction(Direction::Vertical)
.border_type(BorderType::Double), .constraints([
Rect { Constraint::Length(1),
x: area.x, Constraint::Length(1),
y: area.y + 1, Constraint::Min(0),
width: area.width, ])
height: 1, .split(area);
}, let room_name_area = areas[0];
); let separator_area = areas[1];
let main_area = areas[2];
// Room name at the top
let room_name = Paragraph::new(Span::styled(
format!("&{}", self.name()),
styles::selected_room(),
))
.alignment(Alignment::Center);
frame.render_widget(room_name, room_name_area);
let separator = Block::default()
.borders(Borders::BOTTOM)
.border_type(BorderType::Double);
frame.render_widget(separator, separator_area);
// Main below
self.render_main_inner(frame, main_area).await;
}
async fn render_main_inner<B: Backend>(&mut self, frame: &mut Frame<'_, B>, area: Rect) {
match &mut self.main {
Main::Empty => {}
Main::Connecting => {
let text = "Connecing...";
let area = layout::centered(text.width() as u16, 1, area);
frame.render_widget(Paragraph::new(Span::styled(text, styles::title())), area);
}
Main::Identifying => {
let text = "Identifying...";
let area = layout::centered(text.width() as u16, 1, area);
frame.render_widget(Paragraph::new(Span::styled(text, styles::title())), area);
}
Main::ChooseNick {
nick,
prev_error: None,
} => {
let area = layout::centered(50, 2, area);
let top = Rect { height: 1, ..area };
let bot = Rect {
y: top.y + 1,
..top
};
let text = "Choose a nick:";
frame.render_widget(Paragraph::new(Span::styled(text, styles::title())), top);
frame.render_stateful_widget(TextLine, bot, nick);
}
Main::ChooseNick { nick, prev_error } => {
let width = prev_error
.as_ref()
.map(|e| e.width() as u16)
.unwrap_or(0)
.max(50);
let height = if prev_error.is_some() { 5 } else { 2 };
let area = layout::centered(width, height, area);
let top = Rect {
height: height - 1,
..area
};
let bot = Rect {
y: area.bottom() - 1,
height: 1,
..area
};
let mut lines = vec![];
if let Some(err) = &prev_error {
lines.push(Spans::from(Span::styled("Error:", styles::title())));
lines.push(Spans::from(Span::styled(err, styles::error())));
lines.push(Spans::from(""));
}
lines.push(Spans::from(Span::styled("Choose a nick:", styles::title())));
frame.render_widget(Paragraph::new(lines), top);
frame.render_stateful_widget(TextLine, bot, nick);
}
Main::Messages => {
// TODO Actually render messages
frame.render_widget(Paragraph::new("TODO: Messages"), area);
}
Main::FatalError(err) => {
let title = "Fatal error:";
let width = (err.width() as u16).max(title.width() as u16);
let area = layout::centered(width, 2, area);
let pg = Paragraph::new(vec![
Spans::from(Span::styled(title, styles::title())),
Spans::from(Span::styled(err as &str, styles::error())),
])
.alignment(Alignment::Center);
frame.render_widget(pg, area);
}
}
} }
pub async fn render_users<B: Backend>(&mut self, frame: &mut Frame<'_, B>, area: Rect) { pub async fn render_users<B: Backend>(&mut self, frame: &mut Frame<'_, B>, area: Rect) {

View file

@ -9,6 +9,7 @@ use tui::text::{Span, Spans};
use tui::widgets::{Paragraph, Widget}; use tui::widgets::{Paragraph, Widget};
use crate::room::Present; use crate::room::Present;
use crate::ui::styles;
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] #[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
struct UserInfo { struct UserInfo {
@ -42,8 +43,6 @@ impl Users {
impl Widget for Users { impl Widget for Users {
fn render(self, area: Rect, buf: &mut Buffer) { fn render(self, area: Rect, buf: &mut Buffer) {
let title_style = Style::default().add_modifier(Modifier::BOLD);
let sessions = self.users.len(); let sessions = self.users.len();
let identities = self let identities = self
.users .users
@ -53,7 +52,7 @@ impl Widget for Users {
.len(); .len();
let title = format!("Users ({identities}/{sessions})"); let title = format!("Users ({identities}/{sessions})");
let mut lines = vec![Spans::from(Span::styled(title, title_style))]; let mut lines = vec![Spans::from(Span::styled(title, styles::title()))];
for user in self.users { for user in self.users {
// TODO Colour users based on identity // TODO Colour users based on identity
lines.push(Spans::from(Span::from(user.nick))); lines.push(Spans::from(Span::from(user.nick)));

View file

@ -10,6 +10,8 @@ use tui::widgets::{Paragraph, Widget};
use crate::room::Room; use crate::room::Room;
use super::styles;
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] #[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
struct RoomInfo { struct RoomInfo {
name: String, name: String,
@ -45,27 +47,23 @@ impl Rooms {
impl Widget for Rooms { impl Widget for Rooms {
fn render(self, area: Rect, buf: &mut Buffer) { fn render(self, area: Rect, buf: &mut Buffer) {
let title_style = Style::default().add_modifier(Modifier::BOLD);
let room_style = Style::default().fg(Color::LightBlue);
let selected_room_style = room_style.add_modifier(Modifier::BOLD);
let title = if let Some(selected) = self.selected { let title = if let Some(selected) = self.selected {
format!("Rooms ({}/{})", selected + 1, self.rooms.len()) format!("Rooms ({}/{})", selected + 1, self.rooms.len())
} else { } else {
format!("Rooms ({})", self.rooms.len()) format!("Rooms ({})", self.rooms.len())
}; };
let mut lines = vec![Spans::from(Span::styled(title, title_style))]; let mut lines = vec![Spans::from(Span::styled(title, styles::title()))];
for (i, room) in self.rooms.iter().enumerate() { for (i, room) in self.rooms.iter().enumerate() {
let name = format!("&{}", room.name); let name = format!("&{}", room.name);
if Some(i) == self.selected { if Some(i) == self.selected {
lines.push(Spans::from(vec![ lines.push(Spans::from(vec![
Span::raw("\n>"), Span::raw("\n>"),
Span::styled(name, selected_room_style), Span::styled(name, styles::selected_room()),
])); ]));
} else { } else {
lines.push(Spans::from(vec![ lines.push(Spans::from(vec![
Span::raw("\n "), Span::raw("\n "),
Span::styled(name, room_style), Span::styled(name, styles::room()),
])); ]));
} }
} }

17
cove-tui/src/ui/styles.rs Normal file
View file

@ -0,0 +1,17 @@
use tui::style::{Color, Modifier, Style};
pub fn title() -> Style {
Style::default().add_modifier(Modifier::BOLD)
}
pub fn error()->Style{
Style::default().fg(Color::Red)
}
pub fn room() -> Style {
Style::default().fg(Color::LightBlue)
}
pub fn selected_room() -> Style {
room().add_modifier(Modifier::BOLD)
}