From 2f60b0390e8413bce73d1ffff5e7545bb1ad8d0b Mon Sep 17 00:00:00 2001 From: Joscha Date: Tue, 12 Jul 2022 21:42:53 +0200 Subject: [PATCH] Use widgets for List rows instead of Styleds --- src/ui/room.rs | 13 +++-- src/ui/rooms.rs | 10 +++- src/ui/widgets/list.rs | 112 ++++++++++++++++++++--------------------- 3 files changed, 74 insertions(+), 61 deletions(-) diff --git a/src/ui/room.rs b/src/ui/room.rs index 73737b7..af2bb8b 100644 --- a/src/ui/room.rs +++ b/src/ui/room.rs @@ -14,7 +14,10 @@ use crate::euph::{self, Joined, Status}; use crate::vault::{EuphMsg, EuphVault}; use super::chat::Chat; +use super::widgets::background::Background; +use super::widgets::empty::Empty; use super::widgets::list::{List, ListState}; +use super::widgets::text::Text; use super::widgets::Widget; use super::{util, UiEvent}; @@ -188,7 +191,11 @@ impl EuphRoom { let normal = Styled::new(owner).then(perms).then((name, style)); 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( @@ -204,11 +211,11 @@ impl EuphRoom { let heading_style = ContentStyle::new().bold(); if !list.is_empty() { - list.add_unsel(""); + list.add_unsel(Empty); } 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 { Self::render_row(list, session, own_session); diff --git a/src/ui/rooms.rs b/src/ui/rooms.rs index 7f0f141..8cc5b26 100644 --- a/src/ui/rooms.rs +++ b/src/ui/rooms.rs @@ -15,7 +15,9 @@ use crate::euph::{Joined, Status}; use crate::vault::Vault; use super::room::EuphRoom; +use super::widgets::background::Background; use super::widgets::list::{List, ListState}; +use super::widgets::text::Text; use super::widgets::Widget; use super::{util, UiEvent}; @@ -120,7 +122,7 @@ impl Rooms { async fn render_rows(&self, list: &mut List, rooms: Vec) { let heading_style = ContentStyle::default().bold(); 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 { 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), + ); } } diff --git a/src/ui/widgets/list.rs b/src/ui/widgets/list.rs index f21bc91..d6a1a61 100644 --- a/src/ui/widgets/list.rs +++ b/src/ui/widgets/list.rs @@ -1,10 +1,8 @@ use std::sync::Arc; use async_trait::async_trait; -use crossterm::style::ContentStyle; use parking_lot::Mutex; use toss::frame::{Frame, Pos, Size}; -use toss::styled::Styled; use super::Widget; @@ -12,7 +10,7 @@ use super::Widget; // State // /////////// -#[derive(Debug)] +#[derive(Debug, Clone)] struct Cursor { /// Id of the element the cursor is pointing to. /// @@ -123,16 +121,6 @@ impl InnerListState { } } -impl InnerListState { - fn focusing(&self, id: &Id) -> bool { - if let Some(cursor) = &self.cursor { - cursor.id == *id - } else { - false - } - } -} - impl InnerListState { fn selectable_of_id(&self, id: &Id) -> Option> { self.rows.iter().enumerate().find_map(|(i, r)| match r { @@ -217,31 +205,38 @@ impl ListState { // Widget // //////////// -// TODO Use widgets for rows -#[derive(Debug)] enum Row { - Unselectable(Styled), + Unselectable { + normal: Box, + }, Selectable { id: Id, - normal: Styled, - normal_bg: ContentStyle, - selected: Styled, - selected_bg: ContentStyle, + normal: Box, + selected: Box, }, } impl Row { fn id(&self) -> Option<&Id> { match self { - Row::Unselectable(_) => None, + Row::Unselectable { .. } => None, Row::Selectable { id, .. } => Some(id), } } - fn styled(&self) -> &Styled { + fn size(&self, frame: &mut Frame, max_width: Option, max_height: Option) -> Size { match self { - Row::Unselectable(styled) => styled, - Row::Selectable { normal, .. } => normal, + Row::Unselectable { normal } => normal.size(frame, max_width, max_height), + 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 List { self.rows.is_empty() } - pub fn add_unsel>(&mut self, styled: S) { - self.rows.push(Row::Unselectable(styled.into())); + pub fn add_unsel(&mut self, normal: W) { + self.rows.push(Row::Unselectable { + normal: Box::new(normal), + }); } - pub fn add_sel( - &mut self, - id: Id, - normal: S1, - normal_bg: ContentStyle, - selected: S2, - selected_bg: ContentStyle, - ) where - S1: Into, - S2: Into, + pub fn add_sel(&mut self, id: Id, normal: W1, selected: W2) + where + W1: 'static + Widget + Send, + W2: 'static + Widget + Send, { self.rows.push(Row::Selectable { id, - normal: normal.into(), - normal_bg, - selected: selected.into(), - selected_bg, + normal: Box::new(normal), + selected: Box::new(selected), }); } } #[async_trait] impl Widget for List { - fn size(&self, frame: &mut Frame, _max_width: Option, _max_height: Option) -> Size { + fn size(&self, frame: &mut Frame, max_width: Option, _max_height: Option) -> Size { let width = self .rows .iter() - .map(|r| frame.width_styled(r.styled())) + .map(|r| r.size(frame, max_width, Some(1)).width) .max() .unwrap_or(0); let height = self.rows.len(); - Size::new(width as u16, height as u16) + Size::new(width, height as u16) } async fn render(self: Box, frame: &mut Frame, pos: Pos, size: Size) { - let mut guard = self.state.lock(); - guard.stabilize(&self.rows, size.height.into()); + // 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(); + 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() { - let dy = i as i32 - guard.offset as i32; + let dy = i as i32 - offset; 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::Unselectable { normal } => normal.render(frame, pos, row_size).await, Row::Selectable { id, normal, - normal_bg, selected, - selected_bg, } => { - let (fg, bg) = if self.focus && guard.focusing(&id) { - (selected, selected_bg) - } else { - (normal, normal_bg) - }; - frame.write(pos, (" ".repeat(size.width.into()), bg)); - frame.write(pos, fg); + let focusing = self.focus + && if let Some(cursor) = &cursor { + cursor.id == id + } else { + false + }; + let widget = if focusing { selected } else { normal }; + widget.render(frame, pos, row_size).await; } } }