Fix list cursor being invisible until first redraw

This commit is contained in:
Joscha 2023-04-17 11:10:33 +02:00
parent 3f18b76c7d
commit 07b761e0f9
5 changed files with 129 additions and 107 deletions

View file

@ -6,7 +6,7 @@ use toss::widgets::{BoxedAsync, Text};
use toss::{Style, Styled, WidgetExt}; use toss::{Style, Styled, WidgetExt};
use crate::ui::input::{key, InputEvent, KeyBindingsList}; use crate::ui::input::{key, InputEvent, KeyBindingsList};
use crate::ui::widgets::{ListState, Popup}; use crate::ui::widgets::{ListBuilder, ListState, Popup};
use crate::ui::UiError; use crate::ui::UiError;
pub struct LinksState { pub struct LinksState {
@ -41,36 +41,40 @@ impl LinksState {
pub fn widget(&mut self) -> BoxedAsync<'_, UiError> { pub fn widget(&mut self) -> BoxedAsync<'_, UiError> {
let style_selected = Style::new().black().on_white(); 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() { 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() { for (id, link) in self.links.iter().enumerate() {
#[allow(clippy::collapsible_else_if)] let link = link.clone();
let text = if list.state().selected() == Some(&id) { if let Some(&number_key) = NUMBER_KEYS.get(id) {
if let Some(number_key) = NUMBER_KEYS.get(id) { list_builder.add_sel(id, move |selected| {
Styled::new(format!("[{number_key}]"), style_selected.bold()) let text = if selected {
.then(" ", style_selected) Styled::new(format!("[{number_key}]"), style_selected.bold())
.then(link, style_selected) .then(" ", style_selected)
} else { .then(link, style_selected)
Styled::new(format!(" {link}"), style_selected) } else {
} Styled::new(format!("[{number_key}]"), Style::new().dark_grey().bold())
.then_plain(" ")
.then_plain(link)
};
Text::new(text)
});
} else { } else {
if let Some(number_key) = NUMBER_KEYS.get(id) { list_builder.add_sel(id, move |selected| {
Styled::new(format!("[{number_key}]"), Style::new().dark_grey().bold()) let text = if selected {
.then_plain(" ") Styled::new(format!(" {link}"), style_selected)
.then_plain(link) } else {
} else { Styled::new_plain(format!(" {link}"))
Styled::new_plain(format!(" {link}")) };
} Text::new(text)
}; });
}
list.add_sel(id, 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 { fn open_link_by_id(&self, id: usize) -> EventResult {

View file

@ -1,4 +1,3 @@
use std::borrow::Cow;
use std::iter; use std::iter;
use crossterm::style::{Color, Stylize}; use crossterm::style::{Color, Stylize};
@ -8,7 +7,7 @@ use toss::widgets::{BoxedAsync, Empty, Text};
use toss::{Style, Styled, WidgetExt}; use toss::{Style, Styled, WidgetExt};
use crate::euph; use crate::euph;
use crate::ui::widgets::{List, ListState}; use crate::ui::widgets::{ListBuilder, ListState};
use crate::ui::UiError; use crate::ui::UiError;
pub fn widget<'a>( pub fn widget<'a>(
@ -16,9 +15,9 @@ pub fn widget<'a>(
joined: &Joined, joined: &Joined,
focused: bool, focused: bool,
) -> BoxedAsync<'a, UiError> { ) -> BoxedAsync<'a, UiError> {
let mut list = list.widget(); let mut list_builder = ListBuilder::new();
render_rows(&mut list, joined, focused); render_rows(&mut list_builder, joined, focused);
list.boxed_async() list_builder.build(list).boxed_async()
} }
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] #[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
@ -60,7 +59,7 @@ impl HalfSession {
} }
fn render_rows( fn render_rows(
list: &mut List<'_, SessionId, BoxedAsync<'static, UiError>>, list_builder: &mut ListBuilder<'_, SessionId, BoxedAsync<'static, UiError>>,
joined: &Joined, joined: &Joined,
focused: bool, focused: bool,
) { ) {
@ -88,14 +87,14 @@ fn render_rows(
lurkers.sort_unstable(); lurkers.sort_unstable();
nurkers.sort_unstable(); nurkers.sort_unstable();
render_section(list, "People", &people, &joined.session, focused); render_section(list_builder, "People", &people, &joined.session, focused);
render_section(list, "Bots", &bots, &joined.session, focused); render_section(list_builder, "Bots", &bots, &joined.session, focused);
render_section(list, "Lurkers", &lurkers, &joined.session, focused); render_section(list_builder, "Lurkers", &lurkers, &joined.session, focused);
render_section(list, "Nurkers", &nurkers, &joined.session, focused); render_section(list_builder, "Nurkers", &nurkers, &joined.session, focused);
} }
fn render_section( fn render_section(
list: &mut List<'_, SessionId, BoxedAsync<'static, UiError>>, list_builder: &mut ListBuilder<'_, SessionId, BoxedAsync<'static, UiError>>,
name: &str, name: &str,
sessions: &[HalfSession], sessions: &[HalfSession],
own_session: &SessionView, own_session: &SessionView,
@ -107,39 +106,40 @@ fn render_section(
let heading_style = Style::new().bold(); let heading_style = Style::new().bold();
if !list.is_empty() { if !list_builder.is_empty() {
list.add_unsel(Empty::new().boxed_async()); list_builder.add_unsel(Empty::new().boxed_async());
} }
let row = Styled::new_plain(" ") let row = Styled::new_plain(" ")
.then(name, heading_style) .then(name, heading_style)
.then_plain(format!(" ({})", sessions.len())); .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 { for session in sessions {
render_row(list, session, own_session, focused); render_row(list_builder, session, own_session, focused);
} }
} }
fn render_row( fn render_row(
list: &mut List<'_, SessionId, BoxedAsync<'static, UiError>>, list_builder: &mut ListBuilder<'_, SessionId, BoxedAsync<'static, UiError>>,
session: &HalfSession, session: &HalfSession,
own_session: &SessionView, own_session: &SessionView,
focused: bool, focused: bool,
) { ) {
let (name, style, style_inv, perms_style_inv) = if session.name.is_empty() { 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 = Style::new().grey();
let style_inv = Style::new().black().on_grey(); let style_inv = Style::new().black().on_grey();
(Cow::Borrowed(name), style, style_inv, style_inv) (name, style, style_inv, style_inv)
} else { } else {
let name = &session.name as &str; let name = &session.name as &str;
let (r, g, b) = euph::nick_color(name); let (r, g, b) = euph::nick_color(name);
let name = euph::EMOJI.replace(name).to_string();
let color = Color::Rgb { r, g, b }; let color = Color::Rgb { r, g, b };
let style = Style::new().bold().with(color); let style = Style::new().bold().with(color);
let style_inv = Style::new().bold().black().on(color); let style_inv = Style::new().bold().black().on(color);
let perms_style_inv = Style::new().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 { let perms = if session.is_staff {
@ -158,20 +158,20 @@ fn render_row(
" " " "
}; };
let widget = if focused && list.state().selected() == Some(&session.session_id) { list_builder.add_sel(session.session_id.clone(), move |selected| {
let text = Styled::new_plain(owner) if focused && selected {
.then(name, style_inv) let text = Styled::new_plain(owner)
.then(perms, perms_style_inv); .then(name, style_inv)
Text::new(text) .then(perms, perms_style_inv);
.background() Text::new(text)
.with_style(style_inv) .background()
.boxed_async() .with_style(style_inv)
} else { .boxed_async()
let text = Styled::new_plain(owner) } else {
.then(&name, style) let text = Styled::new_plain(owner)
.then_plain(perms); .then(&name, style)
Text::new(text).boxed_async() .then_plain(perms);
}; Text::new(text).boxed_async()
}
list.add_sel(session.session_id.clone(), widget); });
} }

View file

@ -5,7 +5,7 @@ use crossterm::style::Stylize;
use toss::widgets::{BoxedAsync, Empty, Join2, Text}; use toss::widgets::{BoxedAsync, Empty, Join2, Text};
use toss::{Style, Styled, WidgetExt}; use toss::{Style, Styled, WidgetExt};
use super::widgets::ListState; use super::widgets::{ListBuilder, ListState};
use super::UiError; use super::UiError;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -136,12 +136,14 @@ impl KeyBindingsList {
.with_horizontal(0.5) .with_horizontal(0.5)
.with_vertical(0.0); .with_vertical(0.0);
let mut list = list_state.widget(); let mut list_builder = ListBuilder::new();
for row in self.0 { 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) .with_horizontal(1)
.border() .border()
.below(hint) .below(hint)

View file

@ -18,7 +18,7 @@ use crate::vault::Vault;
use super::euph::room::EuphRoom; use super::euph::room::EuphRoom;
use super::input::{key, InputEvent, KeyBindingsList}; use super::input::{key, InputEvent, KeyBindingsList};
use super::widgets::{List, ListState, Popup}; use super::widgets::{ListBuilder, ListState, Popup};
use super::{util, UiError, UiEvent}; use super::{util, UiError, UiEvent};
enum State { enum State {
@ -348,12 +348,12 @@ impl Rooms {
} }
async fn render_rows( async fn render_rows(
list: &mut List<'_, String, Text>, list_builder: &mut ListBuilder<'_, String, Text>,
euph_rooms: &HashMap<String, EuphRoom>, euph_rooms: &HashMap<String, EuphRoom>,
order: Order, order: Order,
) { ) {
if euph_rooms.is_empty() { if euph_rooms.is_empty() {
list.add_unsel(Text::new(( list_builder.add_unsel(Text::new((
"Press F1 for key bindings", "Press F1 for key bindings",
Style::new().grey().italic(), Style::new().grey().italic(),
))) )))
@ -367,16 +367,19 @@ impl Rooms {
} }
Self::sort_rooms(&mut rooms, order); Self::sort_rooms(&mut rooms, order);
for (name, state, unseen) in rooms { for (name, state, unseen) in rooms {
let style = if list.state().selected() == Some(name) { let name = name.clone();
Style::new().bold().black().on_white() let info = Self::format_room_info(state, unseen);
} else { list_builder.add_sel(name.clone(), move |selected| {
Style::new().bold().blue() let style = if selected {
}; Style::new().bold().black().on_white()
} else {
Style::new().bold().blue()
};
let text = Styled::new(format!("&{name}"), style) let text = Styled::new(format!("&{name}"), style).and_then(info);
.and_then(Self::format_room_info(state, unseen));
list.add_sel(name.clone(), Text::new(text)); Text::new(text)
});
} }
} }
@ -389,12 +392,12 @@ impl Rooms {
let heading_text = let heading_text =
Styled::new("Rooms", heading_style).then_plain(format!(" ({})", euph_rooms.len())); Styled::new("Rooms", heading_style).then_plain(format!(" ({})", euph_rooms.len()));
let mut list = list.widget(); let mut list_builder = ListBuilder::new();
Self::render_rows(&mut list, euph_rooms, order).await; Self::render_rows(&mut list_builder, euph_rooms, order).await;
Join2::vertical( Join2::vertical(
Text::new(heading_text).segment().with_fixed(true), Text::new(heading_text).segment().with_fixed(true),
list.segment(), list_builder.build(list).segment(),
) )
.boxed_async() .boxed_async()
} }

View file

@ -211,13 +211,6 @@ impl<Id: Clone> ListState<Id> {
self.move_cursor_to(new_cursor); self.move_cursor_to(new_cursor);
} }
} }
pub fn widget<W>(&mut self) -> List<'_, Id, W> {
List {
state: self,
rows: vec![],
}
}
} }
impl<Id: Clone + Eq> ListState<Id> { impl<Id: Clone + Eq> ListState<Id> {
@ -248,39 +241,61 @@ impl<Id: Clone + Eq> ListState<Id> {
} }
} }
struct Row<Id, W> { struct UnrenderedRow<'a, Id, W> {
id: Option<Id>, id: Option<Id>,
widget: W, widget: Box<dyn FnOnce(bool) -> W + 'a>,
} }
pub struct List<'a, Id, W> { pub struct ListBuilder<'a, Id, W> {
state: &'a mut ListState<Id>, rows: Vec<UnrenderedRow<'a, Id, W>>,
rows: Vec<Row<Id, W>>,
} }
impl<Id, W> List<'_, Id, W> { impl<'a, Id, W> ListBuilder<'a, Id, W> {
pub fn state(&self) -> &ListState<Id> { pub fn new() -> Self {
&self.state Self { rows: vec![] }
}
pub fn state_mut(&mut self) -> &mut ListState<Id> {
&mut self.state
} }
pub fn is_empty(&self) -> bool { pub fn is_empty(&self) -> bool {
self.rows.is_empty() self.rows.is_empty()
} }
pub fn add_unsel(&mut self, widget: W) { pub fn add_unsel(&mut self, widget: W)
self.rows.push(Row { id: None, widget }); where
} W: 'a,
{
pub fn add_sel(&mut self, id: Id, widget: W) { self.rows.push(UnrenderedRow {
self.rows.push(Row { id: None,
id: Some(id), widget: Box::new(|_| widget),
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<Id>) -> 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<Id>,
rows: Vec<W>,
} }
#[async_trait] #[async_trait]
@ -297,7 +312,7 @@ where
) -> Result<Size, E> { ) -> Result<Size, E> {
let mut width = 0; let mut width = 0;
for row in &self.rows { 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); width = width.max(size.width);
} }
let height = self.rows.len().try_into().unwrap_or(u16::MAX); 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> { async fn draw(self, frame: &mut Frame) -> Result<(), E> {
let size = frame.size(); 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.last_height = size.height;
self.state.fix_cursor();
for (y, row) in self for (y, row) in self
.rows .rows
@ -319,7 +332,7 @@ where
.enumerate() .enumerate()
{ {
frame.push(Pos::new(0, y as i32), Size::new(size.width, 1)); frame.push(Pos::new(0, y as i32), Size::new(size.width, 1));
row.widget.draw(frame).await?; row.draw(frame).await?;
frame.pop(); frame.pop();
} }