From 04d17179a02e7d69ac74f55c48fa6a75241fb805 Mon Sep 17 00:00:00 2001 From: Joscha Date: Sun, 27 Feb 2022 01:46:19 +0100 Subject: [PATCH] Show current room state --- cove-tui/src/room.rs | 18 ++-- cove-tui/src/ui.rs | 1 + cove-tui/src/ui/room.rs | 182 +++++++++++++++++++++++++++++++--- cove-tui/src/ui/room/users.rs | 5 +- cove-tui/src/ui/rooms.rs | 12 +-- cove-tui/src/ui/styles.rs | 17 ++++ 6 files changed, 202 insertions(+), 33 deletions(-) create mode 100644 cove-tui/src/ui/styles.rs diff --git a/cove-tui/src/room.rs b/cove-tui/src/room.rs index 647830b..456637f 100644 --- a/cove-tui/src/room.rs +++ b/cove-tui/src/room.rs @@ -9,6 +9,7 @@ use cove_core::packets::{ use cove_core::{Session, SessionId}; use tokio::sync::oneshot::{self, Sender}; use tokio::sync::Mutex; +use tui::widgets::StatefulWidget; use crate::config::Config; use crate::never::Never; @@ -41,18 +42,15 @@ pub struct Present { pub others: HashMap, } -enum Status { +pub enum Status { /// No action required by the UI. Nominal, /// User must enter a nick. NickRequired, - /// Identifying to the server. No action required by the UI. - Identifying, CouldNotConnect, InvalidRoom(String), InvalidNick(String), InvalidIdentity(String), - InvalidContent(String), } pub struct Room { @@ -95,6 +93,14 @@ impl Room { room } + pub fn status(&self) -> &Status { + &self.status + } + + pub fn connected(&self) -> bool { + self.connected.is_some() + } + pub fn present(&self) -> Option<&Present> { self.present.as_ref() } @@ -209,9 +215,7 @@ impl Room { Rpl::Send(SendRpl::Success { message }) => { // TODO Send message to store } - Rpl::Send(SendRpl::InvalidContent { reason }) => { - self.status = Status::InvalidContent(reason.clone()); - } + Rpl::Send(SendRpl::InvalidContent { reason }) => {} Rpl::Who(WhoRpl { you, others }) => { if let Some(present) = &mut self.present { present.session = you.clone(); diff --git a/cove-tui/src/ui.rs b/cove-tui/src/ui.rs index 71b05d1..eb7df64 100644 --- a/cove-tui/src/ui.rs +++ b/cove-tui/src/ui.rs @@ -5,6 +5,7 @@ mod pane; mod room; mod rooms; mod textline; +mod styles; use std::collections::hash_map::Entry; use std::collections::HashMap; diff --git a/cove-tui/src/ui/room.rs b/cove-tui/src/ui/room.rs index c09a954..bc88a6a 100644 --- a/cove-tui/src/ui/room.rs +++ b/cove-tui/src/ui/room.rs @@ -4,41 +4,191 @@ use std::sync::Arc; use tokio::sync::Mutex; use tui::backend::Backend; -use tui::layout::Rect; -use tui::widgets::{Block, BorderType, Borders}; +use tui::layout::{Alignment, Constraint, Direction, Layout, Rect}; +use tui::style::Style; +use tui::text::{Span, Spans, Text}; +use tui::widgets::{Block, BorderType, Borders, Paragraph}; use tui::Frame; +use unicode_width::UnicodeWidthStr; -use crate::room::Room; +use crate::room::{Room, Status}; use self::users::Users; +use super::textline::{TextLine, TextLineState}; +use super::{layout, styles}; + +enum Main { + Empty, + Connecting, + Identifying, + ChooseNick { + nick: TextLineState, + prev_error: Option, + }, + Messages, + FatalError(String), +} + +impl Main { + fn choose_nick() -> Self { + Self::ChooseNick { + nick: TextLineState::default(), + prev_error: None, + } + } + + fn fatal(s: S) -> Self { + Self::FatalError(s.to_string()) + } +} + pub struct RoomInfo { name: String, room: Arc>, + main: Main, } impl RoomInfo { pub fn new(name: String, room: Arc>) -> Self { - Self { name, room } + Self { + name, + room, + main: Main::Empty, + } } pub fn name(&self) -> &str { &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(&mut self, frame: &mut Frame<'_, B>, area: Rect) { - // TODO Implement - frame.render_widget( - Block::default() - .borders(Borders::TOP) - .border_type(BorderType::Double), - Rect { - x: area.x, - y: area.y + 1, - width: area.width, - height: 1, - }, - ); + self.align_main().await; + + let areas = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Length(1), + Constraint::Length(1), + Constraint::Min(0), + ]) + .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(&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(&mut self, frame: &mut Frame<'_, B>, area: Rect) { diff --git a/cove-tui/src/ui/room/users.rs b/cove-tui/src/ui/room/users.rs index 0ca9583..b17216b 100644 --- a/cove-tui/src/ui/room/users.rs +++ b/cove-tui/src/ui/room/users.rs @@ -9,6 +9,7 @@ use tui::text::{Span, Spans}; use tui::widgets::{Paragraph, Widget}; use crate::room::Present; +use crate::ui::styles; #[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] struct UserInfo { @@ -42,8 +43,6 @@ impl Users { impl Widget for Users { fn render(self, area: Rect, buf: &mut Buffer) { - let title_style = Style::default().add_modifier(Modifier::BOLD); - let sessions = self.users.len(); let identities = self .users @@ -53,7 +52,7 @@ impl Widget for Users { .len(); 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 { // TODO Colour users based on identity lines.push(Spans::from(Span::from(user.nick))); diff --git a/cove-tui/src/ui/rooms.rs b/cove-tui/src/ui/rooms.rs index 9d9cba6..06f3818 100644 --- a/cove-tui/src/ui/rooms.rs +++ b/cove-tui/src/ui/rooms.rs @@ -10,6 +10,8 @@ use tui::widgets::{Paragraph, Widget}; use crate::room::Room; +use super::styles; + #[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] struct RoomInfo { name: String, @@ -45,27 +47,23 @@ impl Rooms { impl Widget for Rooms { 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 { format!("Rooms ({}/{})", selected + 1, self.rooms.len()) } else { 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() { let name = format!("&{}", room.name); if Some(i) == self.selected { lines.push(Spans::from(vec![ Span::raw("\n>"), - Span::styled(name, selected_room_style), + Span::styled(name, styles::selected_room()), ])); } else { lines.push(Spans::from(vec![ Span::raw("\n "), - Span::styled(name, room_style), + Span::styled(name, styles::room()), ])); } } diff --git a/cove-tui/src/ui/styles.rs b/cove-tui/src/ui/styles.rs new file mode 100644 index 0000000..ded8a7b --- /dev/null +++ b/cove-tui/src/ui/styles.rs @@ -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) +}