Use List widget for nick list

This commit is contained in:
Joscha 2022-07-12 19:26:46 +02:00
parent da6bab4b13
commit dea0384162
4 changed files with 26 additions and 251 deletions

View file

@ -1,6 +1,5 @@
mod chat; mod chat;
mod editor; mod editor;
mod list;
mod room; mod room;
mod rooms; mod rooms;
mod util; mod util;

View file

@ -1,229 +0,0 @@
use crossterm::style::ContentStyle;
use toss::frame::{Frame, Pos, Size};
use toss::styled::Styled;
#[derive(Debug)]
pub enum Row<Id> {
Unselectable(Styled),
Selectable {
id: Id,
normal: Styled,
normal_bg: ContentStyle,
selected: Styled,
selected_bg: ContentStyle,
},
}
impl<Id> Row<Id> {
pub fn unsel<S: Into<Styled>>(styled: S) -> Self {
Self::Unselectable(styled.into())
}
pub fn sel<S: Into<Styled>>(
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<Id> {
cursor: Option<(Id, usize)>,
offset: usize,
}
// Implemented manually because the derived `Default` requires `Id: Default`.
impl<Id> Default for List<Id> {
fn default() -> Self {
Self {
cursor: Default::default(),
offset: Default::default(),
}
}
}
impl<Id> List<Id> {
pub fn new() -> Self {
Self::default()
}
pub fn cursor(&self) -> Option<&Id> {
self.cursor.as_ref().map(|(i, _)| i)
}
}
impl<Id: Clone + Eq> List<Id> {
fn first_selectable(rows: &[Row<Id>]) -> Option<(Id, usize)> {
rows.iter()
.enumerate()
.find_map(|(i, r)| r.id().map(|c| (c.clone(), i)))
}
fn last_selectable(rows: &[Row<Id>]) -> 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: &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<Id>], 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<Id>], 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<Id>], 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<Id>], 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<Id>]) {
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<Id>]) {
self.fix_cursor(rows);
self.clamp_scrolling(height, rows.len());
}
pub fn move_cursor_up(&mut self, height: usize, rows: &[Row<Id>]) {
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<Id>]) {
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<Id>]) {
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<Id>]) {
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<Row<Id>>,
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);
}
}
}
}
}

View file

@ -14,7 +14,8 @@ use crate::euph::{self, Joined, Status};
use crate::vault::{EuphMsg, EuphVault}; use crate::vault::{EuphMsg, EuphVault};
use super::chat::Chat; use super::chat::Chat;
use super::list::{List, Row}; use super::widgets::list::{List, ListState};
use super::widgets::Widget;
use super::{util, UiEvent}; use super::{util, UiEvent};
pub struct EuphRoom { pub struct EuphRoom {
@ -23,7 +24,7 @@ pub struct EuphRoom {
chat: Chat<EuphMsg, EuphVault>, chat: Chat<EuphMsg, EuphVault>,
nick_list_width: u16, nick_list_width: u16,
nick_list: List<String>, nick_list: ListState<String>,
} }
impl EuphRoom { impl EuphRoom {
@ -33,7 +34,7 @@ impl EuphRoom {
room: None, room: None,
chat: Chat::new(vault), chat: Chat::new(vault),
nick_list_width: 24, 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.chat.render(frame, chat_pos, chat_size).await;
self.render_status(frame, status_pos, status); 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); Self::render_vsplit_hsplit(frame, vsplit, hsplit);
} }
@ -151,7 +153,7 @@ impl EuphRoom {
frame.write(pos, info); frame.write(pos, info);
} }
fn render_row(session: &SessionView, own_session: &SessionView) -> Row<String> { fn render_row(list: &mut List<String>, session: &SessionView, own_session: &SessionView) {
let id = session.session_id.clone(); let id = session.session_id.clone();
let (name, style, style_inv) = if session.name.is_empty() { 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 normal = Styled::new(owner).then(perms).then((name, style));
let selected = Styled::new(owner).then(perms).then((name, style_inv)); 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( fn render_section(
rows: &mut Vec<Row<String>>, list: &mut List<String>,
name: &str, name: &str,
sessions: &[&SessionView], sessions: &[&SessionView],
own_session: &SessionView, own_session: &SessionView,
@ -201,19 +203,19 @@ impl EuphRoom {
let heading_style = ContentStyle::new().bold(); let heading_style = ContentStyle::new().bold();
if !rows.is_empty() { if !list.is_empty() {
rows.push(Row::unsel("")); list.add_unsel("");
} }
let row = Styled::new((name, heading_style)).then(format!(" ({})", sessions.len())); let row = Styled::new((name, heading_style)).then(format!(" ({})", sessions.len()));
rows.push(Row::unsel(row)); list.add_unsel(row);
for session in sessions { 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<Row<String>> { fn render_rows(list: &mut List<String>, joined: &Joined) {
let mut people = vec![]; let mut people = vec![];
let mut bots = vec![]; let mut bots = vec![];
let mut lurkers = vec![]; let mut lurkers = vec![];
@ -237,15 +239,13 @@ impl EuphRoom {
lurkers.sort_unstable_by_key(|s| &s.session_id); lurkers.sort_unstable_by_key(|s| &s.session_id);
nurkers.sort_unstable_by_key(|s| &s.session_id); nurkers.sort_unstable_by_key(|s| &s.session_id);
let mut rows: Vec<Row<String>> = vec![]; Self::render_section(list, "People", &people, &joined.session);
Self::render_section(&mut rows, "People", &people, &joined.session); Self::render_section(list, "Bots", &bots, &joined.session);
Self::render_section(&mut rows, "Bots", &bots, &joined.session); Self::render_section(list, "Lurkers", &lurkers, &joined.session);
Self::render_section(&mut rows, "Lurkers", &lurkers, &joined.session); Self::render_section(list, "Nurkers", &nurkers, &joined.session);
Self::render_section(&mut rows, "Nurkers", &nurkers, &joined.session);
rows
} }
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 // Clear area in case there's overdraw from the chat or status
for y in pos.y..(pos.y + size.height as i32) { for y in pos.y..(pos.y + size.height as i32) {
for x in pos.x..(pos.x + size.width 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); let mut list = self.nick_list.list();
self.nick_list.render(frame, pos, size, rows, false); Self::render_rows(&mut list, joined);
list.render(frame, pos, size).await;
} }
fn render_hsplit(frame: &mut Frame, hsplit: i32) { fn render_hsplit(frame: &mut Frame, hsplit: i32) {

View file

@ -266,6 +266,10 @@ impl<Id> List<Id> {
self self
} }
pub fn is_empty(&self) -> bool {
self.rows.is_empty()
}
pub fn add_unsel<S: Into<Styled>>(&mut self, styled: S) { pub fn add_unsel<S: Into<Styled>>(&mut self, styled: S) {
self.rows.push(Row::Unselectable(styled.into())); self.rows.push(Row::Unselectable(styled.into()));
} }