diff --git a/src/ui/euph/links.rs b/src/ui/euph/links.rs index 6e279f8..a5541e6 100644 --- a/src/ui/euph/links.rs +++ b/src/ui/euph/links.rs @@ -6,7 +6,7 @@ use toss::widgets::{BoxedAsync, Text}; use toss::{Style, Styled, WidgetExt}; use crate::ui::input::{key, InputEvent, KeyBindingsList}; -use crate::ui::widgets::{ListState, Popup}; +use crate::ui::widgets::{ListBuilder, ListState, Popup}; use crate::ui::UiError; pub struct LinksState { @@ -41,36 +41,40 @@ impl LinksState { pub fn widget(&mut self) -> BoxedAsync<'_, UiError> { let style_selected = Style::new().black().on_white(); - let mut list = self.list.widget(); + let mut list_builder = ListBuilder::new(); if self.links.is_empty() { - list.add_unsel(Text::new(("No links found", Style::new().grey().italic()))) + list_builder.add_unsel(Text::new(("No links found", Style::new().grey().italic()))) } for (id, link) in self.links.iter().enumerate() { - #[allow(clippy::collapsible_else_if)] - let text = if list.state().selected() == Some(&id) { - if let Some(number_key) = NUMBER_KEYS.get(id) { - Styled::new(format!("[{number_key}]"), style_selected.bold()) - .then(" ", style_selected) - .then(link, style_selected) - } else { - Styled::new(format!(" {link}"), style_selected) - } + let link = link.clone(); + if let Some(&number_key) = NUMBER_KEYS.get(id) { + list_builder.add_sel(id, move |selected| { + let text = if selected { + Styled::new(format!("[{number_key}]"), style_selected.bold()) + .then(" ", style_selected) + .then(link, style_selected) + } else { + Styled::new(format!("[{number_key}]"), Style::new().dark_grey().bold()) + .then_plain(" ") + .then_plain(link) + }; + Text::new(text) + }); } else { - if let Some(number_key) = NUMBER_KEYS.get(id) { - Styled::new(format!("[{number_key}]"), Style::new().dark_grey().bold()) - .then_plain(" ") - .then_plain(link) - } else { - Styled::new_plain(format!(" {link}")) - } - }; - - list.add_sel(id, Text::new(text)); + list_builder.add_sel(id, move |selected| { + let text = if selected { + Styled::new(format!(" {link}"), style_selected) + } else { + Styled::new_plain(format!(" {link}")) + }; + Text::new(text) + }); + } } - Popup::new(list, "Links").boxed_async() + Popup::new(list_builder.build(&mut self.list), "Links").boxed_async() } fn open_link_by_id(&self, id: usize) -> EventResult { diff --git a/src/ui/euph/nick_list.rs b/src/ui/euph/nick_list.rs index 2d91602..0e13c84 100644 --- a/src/ui/euph/nick_list.rs +++ b/src/ui/euph/nick_list.rs @@ -1,4 +1,3 @@ -use std::borrow::Cow; use std::iter; use crossterm::style::{Color, Stylize}; @@ -8,7 +7,7 @@ use toss::widgets::{BoxedAsync, Empty, Text}; use toss::{Style, Styled, WidgetExt}; use crate::euph; -use crate::ui::widgets::{List, ListState}; +use crate::ui::widgets::{ListBuilder, ListState}; use crate::ui::UiError; pub fn widget<'a>( @@ -16,9 +15,9 @@ pub fn widget<'a>( joined: &Joined, focused: bool, ) -> BoxedAsync<'a, UiError> { - let mut list = list.widget(); - render_rows(&mut list, joined, focused); - list.boxed_async() + let mut list_builder = ListBuilder::new(); + render_rows(&mut list_builder, joined, focused); + list_builder.build(list).boxed_async() } #[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] @@ -60,7 +59,7 @@ impl HalfSession { } fn render_rows( - list: &mut List<'_, SessionId, BoxedAsync<'static, UiError>>, + list_builder: &mut ListBuilder<'_, SessionId, BoxedAsync<'static, UiError>>, joined: &Joined, focused: bool, ) { @@ -88,14 +87,14 @@ fn render_rows( lurkers.sort_unstable(); nurkers.sort_unstable(); - render_section(list, "People", &people, &joined.session, focused); - render_section(list, "Bots", &bots, &joined.session, focused); - render_section(list, "Lurkers", &lurkers, &joined.session, focused); - render_section(list, "Nurkers", &nurkers, &joined.session, focused); + render_section(list_builder, "People", &people, &joined.session, focused); + render_section(list_builder, "Bots", &bots, &joined.session, focused); + render_section(list_builder, "Lurkers", &lurkers, &joined.session, focused); + render_section(list_builder, "Nurkers", &nurkers, &joined.session, focused); } fn render_section( - list: &mut List<'_, SessionId, BoxedAsync<'static, UiError>>, + list_builder: &mut ListBuilder<'_, SessionId, BoxedAsync<'static, UiError>>, name: &str, sessions: &[HalfSession], own_session: &SessionView, @@ -107,39 +106,40 @@ fn render_section( let heading_style = Style::new().bold(); - if !list.is_empty() { - list.add_unsel(Empty::new().boxed_async()); + if !list_builder.is_empty() { + list_builder.add_unsel(Empty::new().boxed_async()); } let row = Styled::new_plain(" ") .then(name, heading_style) .then_plain(format!(" ({})", sessions.len())); - list.add_unsel(Text::new(row).boxed_async()); + list_builder.add_unsel(Text::new(row).boxed_async()); for session in sessions { - render_row(list, session, own_session, focused); + render_row(list_builder, session, own_session, focused); } } fn render_row( - list: &mut List<'_, SessionId, BoxedAsync<'static, UiError>>, + list_builder: &mut ListBuilder<'_, SessionId, BoxedAsync<'static, UiError>>, session: &HalfSession, own_session: &SessionView, focused: bool, ) { let (name, style, style_inv, perms_style_inv) = if session.name.is_empty() { - let name = "lurk"; + let name = "lurk".to_string(); let style = Style::new().grey(); let style_inv = Style::new().black().on_grey(); - (Cow::Borrowed(name), style, style_inv, style_inv) + (name, style, style_inv, style_inv) } else { let name = &session.name as &str; let (r, g, b) = euph::nick_color(name); + let name = euph::EMOJI.replace(name).to_string(); let color = Color::Rgb { r, g, b }; let style = Style::new().bold().with(color); let style_inv = Style::new().bold().black().on(color); let perms_style_inv = Style::new().black().on(color); - (euph::EMOJI.replace(name), style, style_inv, perms_style_inv) + (name, style, style_inv, perms_style_inv) }; let perms = if session.is_staff { @@ -158,20 +158,20 @@ fn render_row( " " }; - let widget = if focused && list.state().selected() == Some(&session.session_id) { - let text = Styled::new_plain(owner) - .then(name, style_inv) - .then(perms, perms_style_inv); - Text::new(text) - .background() - .with_style(style_inv) - .boxed_async() - } else { - let text = Styled::new_plain(owner) - .then(&name, style) - .then_plain(perms); - Text::new(text).boxed_async() - }; - - list.add_sel(session.session_id.clone(), widget); + list_builder.add_sel(session.session_id.clone(), move |selected| { + if focused && selected { + let text = Styled::new_plain(owner) + .then(name, style_inv) + .then(perms, perms_style_inv); + Text::new(text) + .background() + .with_style(style_inv) + .boxed_async() + } else { + let text = Styled::new_plain(owner) + .then(&name, style) + .then_plain(perms); + Text::new(text).boxed_async() + } + }); } diff --git a/src/ui/input.rs b/src/ui/input.rs index ab3fcf2..802bfdf 100644 --- a/src/ui/input.rs +++ b/src/ui/input.rs @@ -5,7 +5,7 @@ use crossterm::style::Stylize; use toss::widgets::{BoxedAsync, Empty, Join2, Text}; use toss::{Style, Styled, WidgetExt}; -use super::widgets::ListState; +use super::widgets::{ListBuilder, ListState}; use super::UiError; #[derive(Debug, Clone)] @@ -136,12 +136,14 @@ impl KeyBindingsList { .with_horizontal(0.5) .with_vertical(0.0); - let mut list = list_state.widget(); + let mut list_builder = ListBuilder::new(); for row in self.0 { - list.add_unsel(Self::row_widget(row)); + list_builder.add_unsel(Self::row_widget(row)); } - list.padding() + list_builder + .build(list_state) + .padding() .with_horizontal(1) .border() .below(hint) diff --git a/src/ui/rooms.rs b/src/ui/rooms.rs index e9bdf4b..b8170a3 100644 --- a/src/ui/rooms.rs +++ b/src/ui/rooms.rs @@ -18,7 +18,7 @@ use crate::vault::Vault; use super::euph::room::EuphRoom; use super::input::{key, InputEvent, KeyBindingsList}; -use super::widgets::{List, ListState, Popup}; +use super::widgets::{ListBuilder, ListState, Popup}; use super::{util, UiError, UiEvent}; enum State { @@ -348,12 +348,12 @@ impl Rooms { } async fn render_rows( - list: &mut List<'_, String, Text>, + list_builder: &mut ListBuilder<'_, String, Text>, euph_rooms: &HashMap, order: Order, ) { if euph_rooms.is_empty() { - list.add_unsel(Text::new(( + list_builder.add_unsel(Text::new(( "Press F1 for key bindings", Style::new().grey().italic(), ))) @@ -367,16 +367,19 @@ impl Rooms { } Self::sort_rooms(&mut rooms, order); for (name, state, unseen) in rooms { - let style = if list.state().selected() == Some(name) { - Style::new().bold().black().on_white() - } else { - Style::new().bold().blue() - }; + let name = name.clone(); + let info = Self::format_room_info(state, unseen); + list_builder.add_sel(name.clone(), move |selected| { + let style = if selected { + Style::new().bold().black().on_white() + } else { + Style::new().bold().blue() + }; - let text = Styled::new(format!("&{name}"), style) - .and_then(Self::format_room_info(state, unseen)); + let text = Styled::new(format!("&{name}"), style).and_then(info); - list.add_sel(name.clone(), Text::new(text)); + Text::new(text) + }); } } @@ -389,12 +392,12 @@ impl Rooms { let heading_text = Styled::new("Rooms", heading_style).then_plain(format!(" ({})", euph_rooms.len())); - let mut list = list.widget(); - Self::render_rows(&mut list, euph_rooms, order).await; + let mut list_builder = ListBuilder::new(); + Self::render_rows(&mut list_builder, euph_rooms, order).await; Join2::vertical( Text::new(heading_text).segment().with_fixed(true), - list.segment(), + list_builder.build(list).segment(), ) .boxed_async() } diff --git a/src/ui/widgets/list.rs b/src/ui/widgets/list.rs index 5693a8a..d933eb8 100644 --- a/src/ui/widgets/list.rs +++ b/src/ui/widgets/list.rs @@ -211,13 +211,6 @@ impl ListState { self.move_cursor_to(new_cursor); } } - - pub fn widget(&mut self) -> List<'_, Id, W> { - List { - state: self, - rows: vec![], - } - } } impl ListState { @@ -248,39 +241,61 @@ impl ListState { } } -struct Row { +struct UnrenderedRow<'a, Id, W> { id: Option, - widget: W, + widget: Box W + 'a>, } -pub struct List<'a, Id, W> { - state: &'a mut ListState, - rows: Vec>, +pub struct ListBuilder<'a, Id, W> { + rows: Vec>, } -impl List<'_, Id, W> { - pub fn state(&self) -> &ListState { - &self.state - } - - pub fn state_mut(&mut self) -> &mut ListState { - &mut self.state +impl<'a, Id, W> ListBuilder<'a, Id, W> { + pub fn new() -> Self { + Self { rows: vec![] } } pub fn is_empty(&self) -> bool { self.rows.is_empty() } - pub fn add_unsel(&mut self, widget: W) { - self.rows.push(Row { id: None, widget }); - } - - pub fn add_sel(&mut self, id: Id, widget: W) { - self.rows.push(Row { - id: Some(id), - widget, + pub fn add_unsel(&mut self, widget: W) + where + W: 'a, + { + self.rows.push(UnrenderedRow { + id: None, + widget: Box::new(|_| widget), }); } + + pub fn add_sel(&mut self, id: Id, widget: impl FnOnce(bool) -> W + 'a) { + self.rows.push(UnrenderedRow { + id: Some(id), + widget: Box::new(widget), + }); + } + + pub fn build(self, state: &mut ListState) -> List<'_, Id, W> + where + Id: Clone + Eq, + { + state.last_rows = self.rows.iter().map(|row| row.id.clone()).collect(); + state.fix_cursor(); + + let selected = state.selected(); + let rows = self + .rows + .into_iter() + .map(|row| (row.widget)(row.id.as_ref() == selected)) + .collect(); + List { state, rows } + } +} + +pub struct List<'a, Id, W> { + state: &'a mut ListState, + rows: Vec, } #[async_trait] @@ -297,7 +312,7 @@ where ) -> Result { let mut width = 0; for row in &self.rows { - let size = row.widget.size(widthdb, max_width, Some(1)).await?; + let size = row.size(widthdb, max_width, Some(1)).await?; width = width.max(size.width); } let height = self.rows.len().try_into().unwrap_or(u16::MAX); @@ -307,9 +322,7 @@ where async fn draw(self, frame: &mut Frame) -> Result<(), E> { let size = frame.size(); - self.state.last_rows = self.rows.iter().map(|row| row.id.clone()).collect(); self.state.last_height = size.height; - self.state.fix_cursor(); for (y, row) in self .rows @@ -319,7 +332,7 @@ where .enumerate() { frame.push(Pos::new(0, y as i32), Size::new(size.width, 1)); - row.widget.draw(frame).await?; + row.draw(frame).await?; frame.pop(); }