Skip to content

Commit

Permalink
feat: rewrite list widget
Browse files Browse the repository at this point in the history
  • Loading branch information
rvigo committed Jun 8, 2024
1 parent 7820edc commit da079d7
Show file tree
Hide file tree
Showing 8 changed files with 234 additions and 90 deletions.
3 changes: 1 addition & 2 deletions cl-gui/src/context/commands_context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,13 @@ use super::{
namespace_context::{NamespaceContext, DEFAULT_NAMESPACE},
Selectable,
};
use crate::{Fuzzy, State};
use crate::{state::ListState, Fuzzy, State};
use anyhow::Result;
use cl_core::{resource::FileService, Command, CommandMap, CommandVec, CommandVecExt, Commands};
use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher};
use itertools::Itertools;
use log::debug;
use std::{cmp::Reverse, thread, time::Duration};
use tui::widgets::ListState;

/// Groups all `Command`'s related stuff
pub struct CommandsContext {
Expand Down
14 changes: 5 additions & 9 deletions cl-gui/src/screen/main_screen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ use crate::{
default_block, display_widget, popup, render,
terminal::{TerminalSize, TerminalSizeExt},
widget::{
list::List,
popup::{HelpPopup, RenderPopup},
statusbar::{Help, Info},
AliasListWidget, DisplayWidget,
DisplayWidget,
},
State, DEFAULT_SELECTED_COLOR,
};
Expand Down Expand Up @@ -44,7 +45,7 @@ impl Screen for MainScreen {
let tags_str = &selected_command.tags_as_string();
let description_str = &selected_command.description();

let commands = AliasListWidget::new(&filtered_commands, command_state);
let commands = List::new(&filtered_commands, command_state);
let tabs = create_namespaces_menu_widget(namespaces, selected_namespace);

let command = display_widget!("Command", command_str, true, should_highlight, &query);
Expand Down Expand Up @@ -122,7 +123,7 @@ fn render_form_medium(
frame: &mut Frame,
tabs: Tabs,
command: DisplayWidget,
commands: AliasListWidget,
commands: List,
namespace: DisplayWidget,
tags: DisplayWidget,
description: DisplayWidget,
Expand Down Expand Up @@ -165,12 +166,7 @@ fn render_form_medium(
);
}

fn render_form_small(
frame: &mut Frame,
tabs: Tabs,
commands: AliasListWidget,
command: DisplayWidget,
) {
fn render_form_small(frame: &mut Frame, tabs: Tabs, commands: List, command: DisplayWidget) {
let constraints = [Constraint::Length(3), Constraint::Min(5)];
let chunks = Layout::default()
.direction(Direction::Vertical)
Expand Down
18 changes: 0 additions & 18 deletions cl-gui/src/state/alias_list_state.rs

This file was deleted.

22 changes: 22 additions & 0 deletions cl-gui/src/state/list_state.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
use super::State;

#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub struct ListState {
pub offset: usize,
pub selected: Option<usize>,
}

impl State for ListState {
type Output = Option<usize>;

fn selected(&self) -> Self::Output {
self.selected
}

fn select(&mut self, index: Self::Output) {
self.selected = index;
if index.is_none() {
self.offset = 0;
}
}
}
3 changes: 2 additions & 1 deletion cl-gui/src/state/mod.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
mod alias_list_state;
mod clipboard_state;
mod field_state;
mod list_state;
mod namespaces_state;

pub use clipboard_state::ClipboardState;
pub use field_state::FieldState;
pub use list_state::ListState;
pub use namespaces_state::NamespacesState;

pub trait State {
Expand Down
58 changes: 0 additions & 58 deletions cl-gui/src/widget/alias_list.rs

This file was deleted.

202 changes: 202 additions & 0 deletions cl-gui/src/widget/list.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
use crate::{default_block, state::ListState, DEFAULT_SELECTED_COLOR};
use cl_core::CommandVec;
use tui::{
buffer::Buffer,
layout::{Corner, Rect},
style::{Color, Modifier, Style, Styled},
text::Line,
widgets::{Block, HighlightSpacing, Widget},
};
use unicode_width::UnicodeWidthStr;

#[derive(Debug, Clone, Eq, PartialEq, Hash, Default)]
pub struct List<'a> {
block: Option<Block<'a>>,
items: Vec<String>,
style: Style,
start_corner: Corner,
highlight_style: Style,
highlight_symbol: Option<&'a str>,
repeat_highlight_symbol: bool,
highlight_spacing: HighlightSpacing,
state: ListState,
}

impl<'a> List<'a> {
pub fn new(commands: &CommandVec, state: ListState) -> List<'a> {
let items: Vec<String> = commands.iter().map(|c| c.alias.to_owned()).collect();

List {
block: None,
style: Style::default(),
items,
start_corner: Corner::TopLeft,
highlight_style: Style::default()
.fg(Color::Black)
.bg(DEFAULT_SELECTED_COLOR)
.add_modifier(Modifier::BOLD),

highlight_symbol: Some("> "),
repeat_highlight_symbol: false,
highlight_spacing: HighlightSpacing::default(),
state,
}
}

pub fn style(mut self, style: Style) -> List<'a> {
self.style = style;
self
}

fn get_items_bounds(
&self,
selected: Option<usize>,
offset: usize,
max_height: usize,
) -> (usize, usize) {
let offset = offset.min(self.items.len().saturating_sub(1));
let mut start = offset;
let mut end = offset;
let mut height = 0;
for item in self.items.iter().skip(offset) {
if height + item.height() > max_height {
break;
}
height += item.height();
end += 1;
}

let selected = selected.unwrap_or(0).min(self.items.len() - 1);
while selected >= end {
height = height.saturating_add(self.items[end].height());
end += 1;
while height > max_height {
height = height.saturating_sub(self.items[start].height());
start += 1;
}
}
while selected < start {
start -= 1;
height = height.saturating_add(self.items[start].height());
while height > max_height {
end -= 1;
height = height.saturating_sub(self.items[end].height());
}
}
(start, end)
}
}

impl<'a> Widget for List<'a> {
fn render(mut self, area: Rect, buf: &mut Buffer) {
buf.set_style(area, self.style);
let list_area = match self.block.take() {
Some(b) => {
let inner_area = b.inner(area);
b.render(area, buf);
inner_area
}
None => {
let b = default_block!("Aliases");
let inner_area = b.inner(area);
b.render(area, buf);
inner_area
}
};

if list_area.width < 1 || list_area.height < 1 {
return;
}

if self.items.is_empty() {
return;
}
let list_height = list_area.height as usize;

let (start, end) =
self.get_items_bounds(self.state.selected, self.state.offset, list_height);
self.state.offset = start;

let highlight_symbol = self.highlight_symbol.unwrap_or("");
let blank_symbol = " ".repeat(highlight_symbol.width());

let mut current_height = 0;
let selection_spacing = self
.highlight_spacing
.should_add(self.state.selected.is_some());
for (i, item) in self
.items
.iter_mut()
.enumerate()
.skip(self.state.offset)
.take(end - start)
{
let (x, y) = if self.start_corner == Corner::BottomLeft {
current_height += 1;
(list_area.left(), list_area.bottom() - current_height)
} else {
let pos = (list_area.left(), list_area.top() + current_height);
current_height += item.height() as u16;
pos
};
let area = Rect {
x,
y,
width: list_area.width,
height: item.height() as u16,
};
let item_style = self.style;
buf.set_style(area, item_style);

let is_selected = self.state.selected.map_or(false, |s| s == i);
for (j, line) in item.lines().enumerate() {
// if the item is selected, we need to display the highlight symbol:
// - either for the first line of the item only,
// - or for each line of the item if the appropriate option is set
let symbol = if is_selected && (j == 0 || self.repeat_highlight_symbol) {
highlight_symbol
} else {
&blank_symbol
};
let (elem_x, max_element_width) = if selection_spacing {
let (elem_x, _) = buf.set_stringn(
x,
y + j as u16,
symbol,
list_area.width as usize,
item_style,
);
(elem_x, (list_area.width - (elem_x - x)))
} else {
(x, list_area.width)
};
buf.set_line(elem_x, y + j as u16, &Line::from(line), max_element_width);
}
if is_selected {
buf.set_style(area, self.highlight_style);
}
}
}
}

impl<'a> Styled for List<'a> {
type Item = List<'a>;

fn style(&self) -> Style {
self.style
}

fn set_style(self, style: Style) -> Self::Item {
self.style(style)
}
}

trait StringExt {
fn height(&self) -> usize;
}

impl StringExt for String {
fn height(&self) -> usize {
self.lines().count()
}
}
4 changes: 2 additions & 2 deletions cl-gui/src/widget/mod.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
mod alias_list;
mod base_widget;
mod display;
mod highlight;
pub mod list;
mod macros;

pub mod popup;
pub mod statusbar;
pub mod text_field;

pub use alias_list::AliasListWidget;
pub use base_widget::BaseWidget;
pub use display::DisplayWidget;
pub use text_field::TextField;
Expand Down

0 comments on commit da079d7

Please sign in to comment.