diff --git a/src/ui.rs b/src/ui.rs index a76e13d..a22ea89 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -1,6 +1,5 @@ mod chat; mod editor; -mod list; mod room; mod rooms; mod util; diff --git a/src/ui/list.rs b/src/ui/list.rs deleted file mode 100644 index 6d76c83..0000000 --- a/src/ui/list.rs +++ /dev/null @@ -1,229 +0,0 @@ -use crossterm::style::ContentStyle; -use toss::frame::{Frame, Pos, Size}; -use toss::styled::Styled; - -#[derive(Debug)] -pub enum Row { - Unselectable(Styled), - Selectable { - id: Id, - normal: Styled, - normal_bg: ContentStyle, - selected: Styled, - selected_bg: ContentStyle, - }, -} - -impl Row { - pub fn unsel>(styled: S) -> Self { - Self::Unselectable(styled.into()) - } - - pub fn sel>( - id: Id, - normal: S, - normal_bg: ContentStyle, - selected: S, - selected_bg: ContentStyle, - ) -> Self { - Self::Selectable { - id, - normal: normal.into(), - normal_bg, - selected: selected.into(), - selected_bg, - } - } - - fn id(&self) -> Option<&Id> { - match self { - Row::Unselectable(_) => None, - Row::Selectable { id, .. } => Some(id), - } - } -} - -#[derive(Debug)] -pub struct List { - cursor: Option<(Id, usize)>, - offset: usize, -} - -// Implemented manually because the derived `Default` requires `Id: Default`. -impl Default for List { - fn default() -> Self { - Self { - cursor: Default::default(), - offset: Default::default(), - } - } -} - -impl List { - pub fn new() -> Self { - Self::default() - } - - pub fn cursor(&self) -> Option<&Id> { - self.cursor.as_ref().map(|(i, _)| i) - } -} - -impl List { - fn first_selectable(rows: &[Row]) -> Option<(Id, usize)> { - rows.iter() - .enumerate() - .find_map(|(i, r)| r.id().map(|c| (c.clone(), i))) - } - - fn last_selectable(rows: &[Row]) -> Option<(Id, usize)> { - rows.iter() - .enumerate() - .rev() - .find_map(|(i, r)| r.id().map(|c| (c.clone(), i))) - } - - fn selectable_of_id(rows: &[Row], id: &Id) -> Option<(Id, usize)> { - rows.iter() - .enumerate() - .find_map(|(i, r)| r.id().filter(|i| *i == id).map(|c| (c.clone(), i))) - } - - fn selectable_at_or_before_index(rows: &[Row], i: usize) -> Option<(Id, usize)> { - rows.iter() - .enumerate() - .take(i + 1) - .rev() - .find_map(|(i, r)| r.id().map(|c| (c.clone(), i))) - } - - fn selectable_at_or_after_index(rows: &[Row], i: usize) -> Option<(Id, usize)> { - rows.iter() - .enumerate() - .skip(i) - .find_map(|(i, r)| r.id().map(|c| (c.clone(), i))) - } - - fn selectable_before_index(rows: &[Row], i: usize) -> Option<(Id, usize)> { - rows.iter() - .enumerate() - .take(i) - .rev() - .find_map(|(i, r)| r.id().map(|c| (c.clone(), i))) - } - - fn selectable_after_index(rows: &[Row], i: usize) -> Option<(Id, usize)> { - rows.iter() - .enumerate() - .skip(i + 1) - .find_map(|(i, r)| r.id().map(|c| (c.clone(), i))) - } - - fn fix_cursor(&mut self, rows: &[Row]) { - self.cursor = if let Some((cid, cidx)) = &self.cursor { - Self::selectable_of_id(rows, cid) - .or_else(|| Self::selectable_at_or_before_index(rows, *cidx)) - .or_else(|| Self::selectable_at_or_after_index(rows, *cidx)) - } else { - Self::first_selectable(rows) - } - } - - fn make_cursor_visible(&mut self, height: usize) { - if let Some(cursor) = &self.cursor { - // As long as height > 0, min <= max is true - assert!(height > 0); - let min = (cursor.1 + 1).saturating_sub(height); - let max = cursor.1; - self.offset = self.offset.clamp(min, max); - } - } - - fn clamp_scrolling(&mut self, height: usize, rows: usize) { - let min = 0; - let max = rows.saturating_sub(height); - self.offset = self.offset.clamp(min, max); - } - - /// Bring the list into a state consistent with the current rows and height. - fn stabilize(&mut self, height: usize, rows: &[Row]) { - self.fix_cursor(rows); - self.clamp_scrolling(height, rows.len()); - } - - pub fn move_cursor_up(&mut self, height: usize, rows: &[Row]) { - self.stabilize(height, rows); - - self.cursor = if let Some((_, cidx)) = &self.cursor { - Self::selectable_before_index(rows, *cidx).or_else(|| Self::first_selectable(rows)) - } else { - Self::last_selectable(rows) - }; - - self.make_cursor_visible(height); - self.clamp_scrolling(height, rows.len()); - } - - pub fn move_cursor_down(&mut self, height: usize, rows: &[Row]) { - self.stabilize(height, rows); - - self.cursor = if let Some((_, cidx)) = &self.cursor { - Self::selectable_after_index(rows, *cidx).or_else(|| Self::last_selectable(rows)) - } else { - Self::first_selectable(rows) - }; - - self.make_cursor_visible(height); - self.clamp_scrolling(height, rows.len()); - } - - pub fn scroll_up(&mut self, height: usize, rows: &[Row]) { - self.stabilize(height, rows); - self.offset = self.offset.saturating_sub(1); - self.clamp_scrolling(height, rows.len()); - } - - pub fn scroll_down(&mut self, height: usize, rows: &[Row]) { - self.stabilize(height, rows); - self.offset = self.offset.saturating_add(1); - self.clamp_scrolling(height, rows.len()); - } - - pub fn render( - &mut self, - frame: &mut Frame, - pos: Pos, - size: Size, - rows: Vec>, - focus: bool, - ) { - self.stabilize(size.height as usize, &rows); - - for (i, row) in rows.into_iter().enumerate() { - let dy = i as i32 - self.offset as i32; - if dy < 0 || dy >= size.height as i32 { - break; - } - - let pos = pos + Pos::new(0, dy); - match row { - Row::Unselectable(styled) => frame.write(pos, styled), - Row::Selectable { - id, - normal, - normal_bg, - selected, - selected_bg, - } => { - let (fg, bg) = if focus && self.cursor() == Some(&id) { - (selected, selected_bg) - } else { - (normal, normal_bg) - }; - frame.write(pos, (" ".repeat(size.width.into()), bg)); - frame.write(pos, fg); - } - } - } - } -} diff --git a/src/ui/room.rs b/src/ui/room.rs index b8f1548..af403b4 100644 --- a/src/ui/room.rs +++ b/src/ui/room.rs @@ -14,7 +14,8 @@ use crate::euph::{self, Joined, Status}; use crate::vault::{EuphMsg, EuphVault}; use super::chat::Chat; -use super::list::{List, Row}; +use super::widgets::list::{List, ListState}; +use super::widgets::Widget; use super::{util, UiEvent}; pub struct EuphRoom { @@ -23,7 +24,7 @@ pub struct EuphRoom { chat: Chat, nick_list_width: u16, - nick_list: List, + nick_list: ListState, } impl EuphRoom { @@ -33,7 +34,7 @@ impl EuphRoom { room: None, chat: Chat::new(vault), nick_list_width: 24, - nick_list: List::new(), + nick_list: ListState::new(), } } @@ -125,7 +126,8 @@ impl EuphRoom { self.chat.render(frame, chat_pos, chat_size).await; self.render_status(frame, status_pos, status); - self.render_nick_list(frame, nick_list_pos, nick_list_size, joined); + self.render_nick_list(frame, nick_list_pos, nick_list_size, joined) + .await; Self::render_vsplit_hsplit(frame, vsplit, hsplit); } @@ -151,7 +153,7 @@ impl EuphRoom { frame.write(pos, info); } - fn render_row(session: &SessionView, own_session: &SessionView) -> Row { + fn render_row(list: &mut List, session: &SessionView, own_session: &SessionView) { let id = session.session_id.clone(); let (name, style, style_inv) = if session.name.is_empty() { @@ -186,11 +188,11 @@ impl EuphRoom { let normal = Styled::new(owner).then(perms).then((name, style)); let selected = Styled::new(owner).then(perms).then((name, style_inv)); - Row::sel(id, normal, style, selected, style_inv) + list.add_sel(id, normal, style, selected, style_inv); } fn render_section( - rows: &mut Vec>, + list: &mut List, name: &str, sessions: &[&SessionView], own_session: &SessionView, @@ -201,19 +203,19 @@ impl EuphRoom { let heading_style = ContentStyle::new().bold(); - if !rows.is_empty() { - rows.push(Row::unsel("")); + if !list.is_empty() { + list.add_unsel(""); } let row = Styled::new((name, heading_style)).then(format!(" ({})", sessions.len())); - rows.push(Row::unsel(row)); + list.add_unsel(row); for session in sessions { - rows.push(Self::render_row(session, own_session)); + Self::render_row(list, session, own_session); } } - fn render_rows(joined: &Joined) -> Vec> { + fn render_rows(list: &mut List, joined: &Joined) { let mut people = vec![]; let mut bots = vec![]; let mut lurkers = vec![]; @@ -237,15 +239,13 @@ impl EuphRoom { lurkers.sort_unstable_by_key(|s| &s.session_id); nurkers.sort_unstable_by_key(|s| &s.session_id); - let mut rows: Vec> = vec![]; - Self::render_section(&mut rows, "People", &people, &joined.session); - Self::render_section(&mut rows, "Bots", &bots, &joined.session); - Self::render_section(&mut rows, "Lurkers", &lurkers, &joined.session); - Self::render_section(&mut rows, "Nurkers", &nurkers, &joined.session); - rows + Self::render_section(list, "People", &people, &joined.session); + Self::render_section(list, "Bots", &bots, &joined.session); + Self::render_section(list, "Lurkers", &lurkers, &joined.session); + Self::render_section(list, "Nurkers", &nurkers, &joined.session); } - fn render_nick_list(&mut self, frame: &mut Frame, pos: Pos, size: Size, joined: &Joined) { + async fn render_nick_list(&mut self, frame: &mut Frame, pos: Pos, size: Size, joined: &Joined) { // Clear area in case there's overdraw from the chat or status for y in pos.y..(pos.y + size.height as i32) { for x in pos.x..(pos.x + size.width as i32) { @@ -253,8 +253,9 @@ impl EuphRoom { } } - let rows = Self::render_rows(joined); - self.nick_list.render(frame, pos, size, rows, false); + let mut list = self.nick_list.list(); + Self::render_rows(&mut list, joined); + list.render(frame, pos, size).await; } fn render_hsplit(frame: &mut Frame, hsplit: i32) { diff --git a/src/ui/widgets/list.rs b/src/ui/widgets/list.rs index 78be8a9..d70ee2c 100644 --- a/src/ui/widgets/list.rs +++ b/src/ui/widgets/list.rs @@ -266,6 +266,10 @@ impl List { self } + pub fn is_empty(&self) -> bool { + self.rows.is_empty() + } + pub fn add_unsel>(&mut self, styled: S) { self.rows.push(Row::Unselectable(styled.into())); }