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 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 {

View file

@ -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()
}
});
}

View file

@ -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)

View file

@ -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<String, EuphRoom>,
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()
}

View file

@ -211,13 +211,6 @@ impl<Id: Clone> ListState<Id> {
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> {
@ -248,39 +241,61 @@ impl<Id: Clone + Eq> ListState<Id> {
}
}
struct Row<Id, W> {
struct UnrenderedRow<'a, Id, W> {
id: Option<Id>,
widget: W,
widget: Box<dyn FnOnce(bool) -> W + 'a>,
}
pub struct List<'a, Id, W> {
state: &'a mut ListState<Id>,
rows: Vec<Row<Id, W>>,
pub struct ListBuilder<'a, Id, W> {
rows: Vec<UnrenderedRow<'a, Id, W>>,
}
impl<Id, W> List<'_, Id, W> {
pub fn state(&self) -> &ListState<Id> {
&self.state
}
pub fn state_mut(&mut self) -> &mut ListState<Id> {
&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<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]
@ -297,7 +312,7 @@ where
) -> Result<Size, E> {
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();
}