tui: Implement generic table component

Signed-off-by: Erik Kundt <erik@zirkular.io>
This commit is contained in:
Erik Kundt 2023-03-24 13:39:39 +01:00
parent ceb1f8e700
commit 85fad7aee0
4 changed files with 178 additions and 6 deletions

View File

@ -1,7 +1,7 @@
use tuirealm::command::{Cmd, CmdResult};
use tuirealm::props::{AttrValue, Attribute, Color, Props, Style};
use tuirealm::tui::layout::Rect;
use tuirealm::tui::text::Span;
use tuirealm::tui::text::{Span, Text};
use tuirealm::{Frame, MockComponent, State};
use crate::ui::widget::{Widget, WidgetComponent};
@ -64,3 +64,18 @@ impl From<&Widget<Label>> for Span<'_> {
Span::styled(content, Style::default())
}
}
impl From<&Widget<Label>> for Text<'_> {
fn from(label: &Widget<Label>) -> Self {
let content = label
.query(Attribute::Content)
.unwrap_or(AttrValue::String(String::default()))
.unwrap_string();
let foreground = label
.query(Attribute::Foreground)
.unwrap_or(AttrValue::Color(Color::Reset))
.unwrap_color();
Text::styled(content, Style::default().fg(foreground))
}
}

View File

@ -1,12 +1,21 @@
use tuirealm::command::{Cmd, CmdResult};
use tuirealm::props::{AttrValue, Attribute, Props};
use tuirealm::tui::layout::Rect;
use tuirealm::{Frame, MockComponent, State};
use radicle::Profile;
use tuirealm::command::{Cmd, CmdResult, Direction};
use tuirealm::props::{
AttrValue, Attribute, Color, PropPayload, PropValue, Props, Style, TextModifiers, TextSpan,
};
use tuirealm::tui::layout::{Constraint, Rect};
use tuirealm::tui::widgets::{Cell, Row, TableState};
use tuirealm::{Frame, MockComponent, State, StateValue};
use crate::ui::components::label::Label;
use crate::ui::layout;
use crate::ui::theme::Theme;
use crate::ui::widget::{Widget, WidgetComponent};
pub trait List {
fn row(&self, theme: &Theme, profile: &Profile) -> Vec<TextSpan>;
}
/// A component that displays a labeled property.
#[derive(Clone)]
pub struct Property {
@ -56,7 +65,6 @@ impl WidgetComponent for Property {
/// A component that can display lists of labeled properties
#[derive(Default)]
#[allow(clippy::vec_box)]
pub struct PropertyList {
properties: Vec<Widget<Property>>,
}
@ -95,3 +103,145 @@ impl WidgetComponent for PropertyList {
CmdResult::None
}
}
pub struct Table {
state: TableState,
}
impl Default for Table {
fn default() -> Self {
let mut state = TableState::default();
state.select(Some(0));
Self { state }
}
}
impl Table {
fn select_previous(&mut self) {
let index = match self.state.selected() {
Some(selected) if selected == 0 => 0,
Some(selected) => selected.saturating_sub(1),
None => 0,
};
self.state.select(Some(index));
}
fn select_next(&mut self, len: usize) {
let index = match self.state.selected() {
Some(selected) if selected >= len.saturating_sub(1) => len.saturating_sub(1),
Some(selected) => selected.saturating_add(1),
None => 0,
};
self.state.select(Some(index));
}
fn header<'a>(spans: Vec<PropValue>) -> Row<'a> {
Row::new(
spans
.iter()
.map(|span| {
Cell::from(span.clone().unwrap_text_span().content)
.style(Style::default().add_modifier(TextModifiers::BOLD))
})
.collect::<Vec<_>>(),
)
}
fn rows<'a>(spans: Vec<Vec<TextSpan>>) -> Vec<Row<'a>> {
spans
.iter()
.map(|spans| {
let cells = spans.iter().map(|span| {
let style = Style::default().fg(span.fg);
Cell::from(span.content.clone()).style(style)
});
Row::new(cells).height(1)
})
.collect::<Vec<Row>>()
}
fn widths(widths: Vec<PropValue>) -> Vec<Constraint> {
widths
.iter()
.map(|prop| Constraint::Percentage(prop.clone().unwrap_u16()))
.collect()
}
}
impl WidgetComponent for Table {
fn view(&mut self, properties: &Props, frame: &mut Frame, area: Rect) {
let content = properties
.get_or(Attribute::Content, AttrValue::Table(vec![]))
.unwrap_table();
let background = properties
.get_or(Attribute::Background, AttrValue::Color(Color::Reset))
.unwrap_color();
let highlight = properties
.get_or(Attribute::HighlightedColor, AttrValue::Color(Color::Reset))
.unwrap_color();
let header = properties
.get_or(
Attribute::Custom("header"),
AttrValue::Payload(PropPayload::Vec(vec![])),
)
.unwrap_payload()
.unwrap_vec();
let widths = properties
.get_or(
Attribute::Custom("widths"),
AttrValue::Payload(PropPayload::Vec(vec![])),
)
.unwrap_payload()
.unwrap_vec();
let header = Self::header(header);
let rows = Self::rows(content);
let widths = Self::widths(widths);
let table = tuirealm::tui::widgets::Table::new(rows)
.highlight_style(Style::default().bg(highlight))
.style(Style::default().bg(background))
.column_spacing(3u16)
.header(header)
.widths(&widths);
frame.render_stateful_widget(table, area, &mut self.state);
}
fn state(&self) -> State {
State::None
}
fn perform(&mut self, properties: &Props, cmd: Cmd) -> CmdResult {
let content = properties
.get_or(Attribute::Content, AttrValue::Table(vec![]))
.unwrap_table();
match cmd {
Cmd::Move(Direction::Up) => {
self.select_previous();
if let Some(selected) = self.state.selected() {
CmdResult::Changed(State::One(StateValue::Usize(selected)))
} else {
CmdResult::None
}
}
Cmd::Move(Direction::Down) => {
self.select_next(content.len());
if let Some(selected) = self.state.selected() {
CmdResult::Changed(State::One(StateValue::Usize(selected)))
} else {
CmdResult::None
}
}
Cmd::Submit => {
if let Some(selected) = self.state.selected() {
CmdResult::Submit(State::One(StateValue::Usize(selected)))
} else {
CmdResult::None
}
}
_ => CmdResult::None,
}
}
}

View File

@ -7,6 +7,7 @@ pub struct Colors {
pub tabs_highlighted_fg: Color,
pub workspaces_info_fg: Color,
pub labeled_container_bg: Color,
pub item_list_highlighted_bg: Color,
pub property_name_fg: Color,
pub property_divider_fg: Color,
pub shortcut_short_fg: Color,
@ -52,6 +53,7 @@ pub fn default_dark() -> Theme {
tabs_highlighted_fg: Color::Rgb(38, 162, 105),
workspaces_info_fg: Color::Rgb(220, 140, 40),
labeled_container_bg: Color::Rgb(20, 20, 20),
item_list_highlighted_bg: Color::Rgb(40, 40, 40),
property_name_fg: Color::Rgb(85, 85, 255),
property_divider_fg: Color::Rgb(10, 206, 209),
shortcut_short_fg: Color::Rgb(100, 100, 100),

View File

@ -67,6 +67,11 @@ impl<T: WidgetComponent> Widget<T> {
self
}
pub fn custom(mut self, key: &'static str, value: AttrValue) -> Self {
self.attr(Attribute::Custom(key), value);
self
}
pub fn to_boxed(self) -> Box<Self> {
Box::new(self)
}