Use widgets for List rows instead of Styleds

This commit is contained in:
Joscha 2022-07-12 21:42:53 +02:00
parent 204eb95fa5
commit 2f60b0390e
3 changed files with 74 additions and 61 deletions

View file

@ -14,7 +14,10 @@ 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::widgets::background::Background;
use super::widgets::empty::Empty;
use super::widgets::list::{List, ListState}; use super::widgets::list::{List, ListState};
use super::widgets::text::Text;
use super::widgets::Widget; use super::widgets::Widget;
use super::{util, UiEvent}; use super::{util, UiEvent};
@ -188,7 +191,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));
list.add_sel(id, normal, style, selected, style_inv); list.add_sel(
id,
Text::new(normal),
Background::new(Text::new(selected), style_inv),
);
} }
fn render_section( fn render_section(
@ -204,11 +211,11 @@ impl EuphRoom {
let heading_style = ContentStyle::new().bold(); let heading_style = ContentStyle::new().bold();
if !list.is_empty() { if !list.is_empty() {
list.add_unsel(""); list.add_unsel(Empty);
} }
let row = Styled::new((name, heading_style)).then(format!(" ({})", sessions.len())); let row = Styled::new((name, heading_style)).then(format!(" ({})", sessions.len()));
list.add_unsel(row); list.add_unsel(Text::new(row));
for session in sessions { for session in sessions {
Self::render_row(list, session, own_session); Self::render_row(list, session, own_session);

View file

@ -15,7 +15,9 @@ use crate::euph::{Joined, Status};
use crate::vault::Vault; use crate::vault::Vault;
use super::room::EuphRoom; use super::room::EuphRoom;
use super::widgets::background::Background;
use super::widgets::list::{List, ListState}; use super::widgets::list::{List, ListState};
use super::widgets::text::Text;
use super::widgets::Widget; use super::widgets::Widget;
use super::{util, UiEvent}; use super::{util, UiEvent};
@ -120,7 +122,7 @@ impl Rooms {
async fn render_rows(&self, list: &mut List<String>, rooms: Vec<String>) { async fn render_rows(&self, list: &mut List<String>, rooms: Vec<String>) {
let heading_style = ContentStyle::default().bold(); let heading_style = ContentStyle::default().bold();
let heading = Styled::new(("Rooms", heading_style)).then(format!(" ({})", rooms.len())); let heading = Styled::new(("Rooms", heading_style)).then(format!(" ({})", rooms.len()));
list.add_unsel(heading); list.add_unsel(Text::new(heading));
for room in rooms { for room in rooms {
let bg_style = ContentStyle::default(); let bg_style = ContentStyle::default();
@ -138,7 +140,11 @@ impl Rooms {
} }
}; };
list.add_sel(room, normal, bg_style, selected, bg_sel_style); list.add_sel(
room,
Text::new(normal),
Background::new(Text::new(selected), bg_sel_style),
);
} }
} }

View file

@ -1,10 +1,8 @@
use std::sync::Arc; use std::sync::Arc;
use async_trait::async_trait; use async_trait::async_trait;
use crossterm::style::ContentStyle;
use parking_lot::Mutex; use parking_lot::Mutex;
use toss::frame::{Frame, Pos, Size}; use toss::frame::{Frame, Pos, Size};
use toss::styled::Styled;
use super::Widget; use super::Widget;
@ -12,7 +10,7 @@ use super::Widget;
// State // // State //
/////////// ///////////
#[derive(Debug)] #[derive(Debug, Clone)]
struct Cursor<Id> { struct Cursor<Id> {
/// Id of the element the cursor is pointing to. /// Id of the element the cursor is pointing to.
/// ///
@ -123,16 +121,6 @@ impl<Id: Clone> InnerListState<Id> {
} }
} }
impl<Id: Eq> InnerListState<Id> {
fn focusing(&self, id: &Id) -> bool {
if let Some(cursor) = &self.cursor {
cursor.id == *id
} else {
false
}
}
}
impl<Id: Clone + Eq> InnerListState<Id> { impl<Id: Clone + Eq> InnerListState<Id> {
fn selectable_of_id(&self, id: &Id) -> Option<Cursor<Id>> { fn selectable_of_id(&self, id: &Id) -> Option<Cursor<Id>> {
self.rows.iter().enumerate().find_map(|(i, r)| match r { self.rows.iter().enumerate().find_map(|(i, r)| match r {
@ -217,31 +205,38 @@ impl<Id: Clone> ListState<Id> {
// Widget // // Widget //
//////////// ////////////
// TODO Use widgets for rows
#[derive(Debug)]
enum Row<Id> { enum Row<Id> {
Unselectable(Styled), Unselectable {
normal: Box<dyn Widget + Send>,
},
Selectable { Selectable {
id: Id, id: Id,
normal: Styled, normal: Box<dyn Widget + Send>,
normal_bg: ContentStyle, selected: Box<dyn Widget + Send>,
selected: Styled,
selected_bg: ContentStyle,
}, },
} }
impl<Id> Row<Id> { impl<Id> Row<Id> {
fn id(&self) -> Option<&Id> { fn id(&self) -> Option<&Id> {
match self { match self {
Row::Unselectable(_) => None, Row::Unselectable { .. } => None,
Row::Selectable { id, .. } => Some(id), Row::Selectable { id, .. } => Some(id),
} }
} }
fn styled(&self) -> &Styled { fn size(&self, frame: &mut Frame, max_width: Option<u16>, max_height: Option<u16>) -> Size {
match self { match self {
Row::Unselectable(styled) => styled, Row::Unselectable { normal } => normal.size(frame, max_width, max_height),
Row::Selectable { normal, .. } => normal, Row::Selectable {
normal, selected, ..
} => {
let normal_size = normal.size(frame, max_width, max_height);
let selected_size = selected.size(frame, max_width, max_height);
Size::new(
normal_size.width.max(selected_size.width),
normal_size.height.max(selected_size.height),
)
}
} }
} }
} }
@ -270,70 +265,75 @@ impl<Id> List<Id> {
self.rows.is_empty() self.rows.is_empty()
} }
pub fn add_unsel<S: Into<Styled>>(&mut self, styled: S) { pub fn add_unsel<W: 'static + Widget + Send>(&mut self, normal: W) {
self.rows.push(Row::Unselectable(styled.into())); self.rows.push(Row::Unselectable {
normal: Box::new(normal),
});
} }
pub fn add_sel<S1, S2>( pub fn add_sel<W1, W2>(&mut self, id: Id, normal: W1, selected: W2)
&mut self, where
id: Id, W1: 'static + Widget + Send,
normal: S1, W2: 'static + Widget + Send,
normal_bg: ContentStyle,
selected: S2,
selected_bg: ContentStyle,
) where
S1: Into<Styled>,
S2: Into<Styled>,
{ {
self.rows.push(Row::Selectable { self.rows.push(Row::Selectable {
id, id,
normal: normal.into(), normal: Box::new(normal),
normal_bg, selected: Box::new(selected),
selected: selected.into(),
selected_bg,
}); });
} }
} }
#[async_trait] #[async_trait]
impl<Id: Clone + Eq + Send> Widget for List<Id> { impl<Id: Clone + Eq + Send> Widget for List<Id> {
fn size(&self, frame: &mut Frame, _max_width: Option<u16>, _max_height: Option<u16>) -> Size { fn size(&self, frame: &mut Frame, max_width: Option<u16>, _max_height: Option<u16>) -> Size {
let width = self let width = self
.rows .rows
.iter() .iter()
.map(|r| frame.width_styled(r.styled())) .map(|r| r.size(frame, max_width, Some(1)).width)
.max() .max()
.unwrap_or(0); .unwrap_or(0);
let height = self.rows.len(); let height = self.rows.len();
Size::new(width as u16, height as u16) Size::new(width, height as u16)
} }
async fn render(self: Box<Self>, frame: &mut Frame, pos: Pos, size: Size) { async fn render(self: Box<Self>, frame: &mut Frame, pos: Pos, size: Size) {
// Guard acquisition and dropping must be inside its own block or the
// compiler complains that "future created by async block is not
// `Send`", pointing to the function body.
//
// I assume this is because I'm using the parking lot mutex whose guard
// is not Send, and even though I was explicitly dropping it with
// drop(), rustc couldn't figure this out without some help.
let (offset, cursor) = {
let mut guard = self.state.lock(); let mut guard = self.state.lock();
guard.stabilize(&self.rows, size.height.into()); guard.stabilize(&self.rows, size.height.into());
(guard.offset as i32, guard.cursor.clone())
};
let row_size = Size::new(size.width, 1);
for (i, row) in self.rows.into_iter().enumerate() { for (i, row) in self.rows.into_iter().enumerate() {
let dy = i as i32 - guard.offset as i32; let dy = i as i32 - offset;
if dy < 0 || dy >= size.height as i32 { if dy < 0 || dy >= size.height as i32 {
break; break;
} }
let pos = pos + Pos::new(0, dy); let pos = pos + Pos::new(0, dy);
match row { match row {
Row::Unselectable(styled) => frame.write(pos, styled), Row::Unselectable { normal } => normal.render(frame, pos, row_size).await,
Row::Selectable { Row::Selectable {
id, id,
normal, normal,
normal_bg,
selected, selected,
selected_bg,
} => { } => {
let (fg, bg) = if self.focus && guard.focusing(&id) { let focusing = self.focus
(selected, selected_bg) && if let Some(cursor) = &cursor {
cursor.id == id
} else { } else {
(normal, normal_bg) false
}; };
frame.write(pos, (" ".repeat(size.width.into()), bg)); let widget = if focusing { selected } else { normal };
frame.write(pos, fg); widget.render(frame, pos, row_size).await;
} }
} }
} }