Fix list cursor being invisible until first redraw
This commit is contained in:
parent
3f18b76c7d
commit
07b761e0f9
5 changed files with 129 additions and 107 deletions
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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);
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue