Use List widget for nick list
This commit is contained in:
parent
da6bab4b13
commit
dea0384162
4 changed files with 26 additions and 251 deletions
|
|
@ -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;
|
||||||
|
|
|
||||||
229
src/ui/list.rs
229
src/ui/list.rs
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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()));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue