tui: Implement generic table component
Signed-off-by: Erik Kundt <erik@zirkular.io>
This commit is contained in:
parent
ceb1f8e700
commit
85fad7aee0
|
|
@ -1,7 +1,7 @@
|
||||||
use tuirealm::command::{Cmd, CmdResult};
|
use tuirealm::command::{Cmd, CmdResult};
|
||||||
use tuirealm::props::{AttrValue, Attribute, Color, Props, Style};
|
use tuirealm::props::{AttrValue, Attribute, Color, Props, Style};
|
||||||
use tuirealm::tui::layout::Rect;
|
use tuirealm::tui::layout::Rect;
|
||||||
use tuirealm::tui::text::Span;
|
use tuirealm::tui::text::{Span, Text};
|
||||||
use tuirealm::{Frame, MockComponent, State};
|
use tuirealm::{Frame, MockComponent, State};
|
||||||
|
|
||||||
use crate::ui::widget::{Widget, WidgetComponent};
|
use crate::ui::widget::{Widget, WidgetComponent};
|
||||||
|
|
@ -64,3 +64,18 @@ impl From<&Widget<Label>> for Span<'_> {
|
||||||
Span::styled(content, Style::default())
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,21 @@
|
||||||
use tuirealm::command::{Cmd, CmdResult};
|
use radicle::Profile;
|
||||||
use tuirealm::props::{AttrValue, Attribute, Props};
|
use tuirealm::command::{Cmd, CmdResult, Direction};
|
||||||
use tuirealm::tui::layout::Rect;
|
use tuirealm::props::{
|
||||||
use tuirealm::{Frame, MockComponent, State};
|
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::components::label::Label;
|
||||||
use crate::ui::layout;
|
use crate::ui::layout;
|
||||||
|
use crate::ui::theme::Theme;
|
||||||
use crate::ui::widget::{Widget, WidgetComponent};
|
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.
|
/// A component that displays a labeled property.
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Property {
|
pub struct Property {
|
||||||
|
|
@ -56,7 +65,6 @@ impl WidgetComponent for Property {
|
||||||
|
|
||||||
/// A component that can display lists of labeled properties
|
/// A component that can display lists of labeled properties
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
#[allow(clippy::vec_box)]
|
|
||||||
pub struct PropertyList {
|
pub struct PropertyList {
|
||||||
properties: Vec<Widget<Property>>,
|
properties: Vec<Widget<Property>>,
|
||||||
}
|
}
|
||||||
|
|
@ -95,3 +103,145 @@ impl WidgetComponent for PropertyList {
|
||||||
CmdResult::None
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ pub struct Colors {
|
||||||
pub tabs_highlighted_fg: Color,
|
pub tabs_highlighted_fg: Color,
|
||||||
pub workspaces_info_fg: Color,
|
pub workspaces_info_fg: Color,
|
||||||
pub labeled_container_bg: Color,
|
pub labeled_container_bg: Color,
|
||||||
|
pub item_list_highlighted_bg: Color,
|
||||||
pub property_name_fg: Color,
|
pub property_name_fg: Color,
|
||||||
pub property_divider_fg: Color,
|
pub property_divider_fg: Color,
|
||||||
pub shortcut_short_fg: Color,
|
pub shortcut_short_fg: Color,
|
||||||
|
|
@ -52,6 +53,7 @@ pub fn default_dark() -> Theme {
|
||||||
tabs_highlighted_fg: Color::Rgb(38, 162, 105),
|
tabs_highlighted_fg: Color::Rgb(38, 162, 105),
|
||||||
workspaces_info_fg: Color::Rgb(220, 140, 40),
|
workspaces_info_fg: Color::Rgb(220, 140, 40),
|
||||||
labeled_container_bg: Color::Rgb(20, 20, 20),
|
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_name_fg: Color::Rgb(85, 85, 255),
|
||||||
property_divider_fg: Color::Rgb(10, 206, 209),
|
property_divider_fg: Color::Rgb(10, 206, 209),
|
||||||
shortcut_short_fg: Color::Rgb(100, 100, 100),
|
shortcut_short_fg: Color::Rgb(100, 100, 100),
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,11 @@ impl<T: WidgetComponent> Widget<T> {
|
||||||
self
|
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> {
|
pub fn to_boxed(self) -> Box<Self> {
|
||||||
Box::new(self)
|
Box::new(self)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue