From e9e3b6e21c4d6a70bc1789335382873b7ed4e6b0 Mon Sep 17 00:00:00 2001 From: Joscha Date: Sun, 26 Jun 2022 19:03:44 +0200 Subject: [PATCH] Render list of known rooms --- src/euph/room.rs | 4 ++ src/ui.rs | 49 +++++++--------- src/ui/rooms.rs | 144 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 167 insertions(+), 30 deletions(-) create mode 100644 src/ui/rooms.rs diff --git a/src/euph/room.rs b/src/euph/room.rs index 3be5ec8..8296711 100644 --- a/src/euph/room.rs +++ b/src/euph/room.rs @@ -273,6 +273,10 @@ impl Room { } } + pub fn stopped(&self) -> bool { + self.event_tx.is_closed() + } + pub async fn status(&self) -> Result, Error> { let (tx, rx) = oneshot::channel(); self.event_tx diff --git a/src/ui.rs b/src/ui.rs index 637bc4c..9771880 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -1,3 +1,5 @@ +mod rooms; + use std::sync::{Arc, Weak}; use std::time::Duration; @@ -11,9 +13,10 @@ use toss::frame::{Frame, Pos, Size}; use toss::terminal::Terminal; use crate::chat::Chat; -use crate::euph; use crate::logger::{LogMsg, Logger}; -use crate::vault::{EuphMsg, EuphVault, Vault}; +use crate::vault::Vault; + +use self::rooms::Rooms; #[derive(Debug)] pub enum UiEvent { @@ -26,18 +29,17 @@ enum EventHandleResult { Stop, } -enum Visible { +enum Mode { Main, Log, } pub struct Ui { event_tx: UnboundedSender, - vault: Vault, - visible: Visible, - room: euph::Room, - chat: Chat, + mode: Mode, + + rooms: Rooms, log_chat: Chat, } @@ -60,10 +62,6 @@ impl Ui { Self::poll_crossterm_events(event_tx_clone, weak_crossterm_lock) }); - let room_vault = vault.euph("test".to_string()); - let chat = Chat::new(room_vault.clone()); - let room = euph::Room::new("test".to_string(), room_vault, event_tx.clone()); - // Run main UI. // // If the run_main method exits at any point or if this `run` method is @@ -75,10 +73,8 @@ impl Ui { // the rest of the UI is also shut down and the client stops. let mut ui = Self { event_tx: event_tx.clone(), - vault, - visible: Visible::Main, - room, - chat, + mode: Mode::Main, + rooms: Rooms::new(vault), log_chat: Chat::new(logger), }; tokio::select! { @@ -163,9 +159,9 @@ impl Ui { } async fn render(&mut self, frame: &mut Frame) -> anyhow::Result<()> { - match self.visible { - Visible::Main => self.chat.render(frame, Pos::new(0, 0), frame.size()).await, - Visible::Log => { + match self.mode { + Mode::Main => self.rooms.render(frame).await, + Mode::Log => { self.log_chat .render(frame, Pos::new(0, 0), frame.size()) .await @@ -190,21 +186,14 @@ impl Ui { match event.code { KeyCode::Char('e') => debug!("{:#?}", event), - KeyCode::F(1) => self.visible = Visible::Main, - KeyCode::F(2) => self.visible = Visible::Log, + KeyCode::F(1) => self.mode = Mode::Main, + KeyCode::F(2) => self.mode = Mode::Log, _ => {} } - match self.visible { - Visible::Main => { - self.chat.handle_navigation(terminal, size, event).await; - self.chat - .handle_messaging(terminal, crossterm_lock, event) - .await; - } - Visible::Log => { - self.log_chat.handle_navigation(terminal, size, event).await; - } + match self.mode { + Mode::Main => self.rooms.handle_key_event(terminal, event).await, + Mode::Log => self.log_chat.handle_navigation(terminal, size, event).await, } EventHandleResult::Continue diff --git a/src/ui/rooms.rs b/src/ui/rooms.rs new file mode 100644 index 0000000..5e3380a --- /dev/null +++ b/src/ui/rooms.rs @@ -0,0 +1,144 @@ +use std::collections::{HashMap, HashSet}; + +use crossterm::event::KeyEvent; +use toss::frame::{Frame, Pos}; +use toss::terminal::Terminal; + +use crate::chat::Chat; +use crate::euph; +use crate::vault::{EuphMsg, EuphVault, Vault}; + +mod style { + use crossterm::style::{ContentStyle, Stylize}; + + pub fn room() -> ContentStyle { + ContentStyle::default().bold().blue() + } + + pub fn room_inverted() -> ContentStyle { + ContentStyle::default().bold().black().on_white() + } +} + +#[derive(Debug, Default, Clone, Copy)] +struct Cursor { + index: usize, + line: i32, +} + +pub struct Rooms { + vault: Vault, + + /// Cursor position inside the room list. + /// + /// If there are any rooms, this should point to a valid room. + cursor: Option, + + /// If set, a single room is displayed in full instead of the room list. + focus: Option, + + euph_rooms: HashMap, + euph_chats: HashMap>, +} + +impl Rooms { + pub fn new(vault: Vault) -> Self { + Self { + vault, + cursor: None, + focus: None, + euph_rooms: HashMap::new(), + euph_chats: HashMap::new(), + } + } + + async fn rooms(&self) -> Vec { + let mut rooms = self.vault.euph_rooms().await; + rooms.sort_unstable(); + rooms + } + + fn make_cursor_consistent(&mut self, rooms: &[String], height: i32) { + // Fix index if it's wrong + if rooms.is_empty() { + self.cursor = None; + } else if let Some(cursor) = &mut self.cursor { + let max_index = rooms.len() - 1; + if cursor.index > max_index { + cursor.index = max_index; + } + } else { + self.cursor = Some(Cursor::default()); + } + + // Fix line if it's wrong + if let Some(cursor) = &mut self.cursor { + cursor.line = cursor + .line + // Make sure the cursor is visible on screen + .clamp(0, height - 1) + // Make sure there is no free space below the room list: + // height - line <= len - index + // height - len + index <= line + .max(height - rooms.len() as i32 + cursor.index as i32) + // Make sure there is no free space above the room list: + // line <= len + .min(rooms.len() as i32); + } + } + + fn make_euph_rooms_consistent(&mut self, rooms: &[String]) { + let rooms = rooms + .iter() + .map(|n| n.to_string()) + .collect::>(); + + self.euph_rooms + .retain(|n, r| rooms.contains(n) && !r.stopped()); + self.euph_chats.retain(|n, _| rooms.contains(n)); + } + + fn make_consistent(&mut self, rooms: &[String], height: i32) { + self.make_cursor_consistent(rooms, height); + self.make_euph_rooms_consistent(rooms); + } + + pub async fn render(&mut self, frame: &mut Frame) { + if let Some(room) = &self.focus { + let chat = self + .euph_chats + .entry(room.clone()) + .or_insert_with(|| Chat::new(self.vault.euph(room.clone()))); + chat.render(frame, Pos::new(0, 0), frame.size()).await; + } else { + self.render_rooms(frame).await; + } + } + + async fn render_rooms(&mut self, frame: &mut Frame) { + let size = frame.size(); + + let rooms = self.rooms().await; + self.make_consistent(&rooms, size.height.into()); + + let cursor = self.cursor.unwrap_or_default(); + for (index, room) in rooms.iter().enumerate() { + let y = index as i32 - cursor.index as i32 + cursor.line; + + let style = if index == cursor.index { + style::room_inverted() + } else { + style::room() + }; + + for x in 0..size.width { + frame.write(Pos::new(x.into(), y), " ", style); + } + frame.write(Pos::new(0, y), &format!("&{room}"), style); + } + } + + pub async fn handle_key_event(&mut self, terminal: &mut Terminal, event: KeyEvent) { + // TODO + } +}