mirror of
https://github.com/ClementTsang/bottom.git
synced 2025-07-27 15:44:17 +02:00
Stateful
This commit is contained in:
parent
54ec5ff0c9
commit
8c3a44deba
@ -240,21 +240,22 @@ impl Application for AppState {
|
|||||||
|
|
||||||
fn view<'b>(&mut self, ctx: &mut ViewContext<'_>) -> Element<Self::Message> {
|
fn view<'b>(&mut self, ctx: &mut ViewContext<'_>) -> Element<Self::Message> {
|
||||||
use crate::tuine::FlexElement;
|
use crate::tuine::FlexElement;
|
||||||
|
use crate::tuine::StatefulTemplate;
|
||||||
use crate::tuine::TempTable;
|
use crate::tuine::TempTable;
|
||||||
use crate::tuine::TextTable;
|
use crate::tuine::TextTableBuilder;
|
||||||
|
|
||||||
Flex::column()
|
Flex::column()
|
||||||
.with_flex_child(
|
.with_flex_child(
|
||||||
Flex::row_with_children(vec![
|
Flex::row_with_children(vec![
|
||||||
FlexElement::new(TempTable::new(ctx)),
|
FlexElement::new(TempTable::new(ctx)),
|
||||||
FlexElement::new(TextTable::new(ctx, vec!["D", "E", "F"])),
|
FlexElement::new(TextTableBuilder::new(vec!["D", "E", "F"]).build(ctx)),
|
||||||
]),
|
]),
|
||||||
1,
|
1,
|
||||||
)
|
)
|
||||||
.with_flex_child(
|
.with_flex_child(
|
||||||
Flex::row_with_children(vec![
|
Flex::row_with_children(vec![
|
||||||
FlexElement::new(TextTable::new(ctx, vec!["G", "H", "I", "J"])),
|
FlexElement::new(TextTableBuilder::new(vec!["G", "H", "I", "J"]).build(ctx)),
|
||||||
FlexElement::new(TextTable::new(ctx, vec!["K", "L", "M", "N"])),
|
FlexElement::new(TextTableBuilder::new(vec!["K", "L", "M", "N"]).build(ctx)),
|
||||||
]),
|
]),
|
||||||
2,
|
2,
|
||||||
)
|
)
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
pub mod text_table;
|
pub mod text_table;
|
||||||
pub use text_table::{TextColumn, TextColumnConstraint, TextTable};
|
pub use text_table::{TextColumn, TextColumnConstraint, TextTable, TextTableBuilder};
|
||||||
|
|
||||||
pub mod shortcut;
|
pub mod shortcut;
|
||||||
pub use shortcut::Shortcut;
|
pub use shortcut::Shortcut;
|
||||||
|
156
src/tuine/component/base/text_table/builder.rs
Normal file
156
src/tuine/component/base/text_table/builder.rs
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
use std::{borrow::Cow, panic::Location};
|
||||||
|
|
||||||
|
use crate::tuine::{StatefulTemplate, ViewContext};
|
||||||
|
|
||||||
|
use super::{DataRow, SortType, StyleSheet, TextColumn, TextTable, TextTableState};
|
||||||
|
|
||||||
|
pub struct TextTableBuilder<Message> {
|
||||||
|
column_widths: Vec<u16>,
|
||||||
|
columns: Vec<TextColumn>,
|
||||||
|
show_gap: bool,
|
||||||
|
show_selected_entry: bool,
|
||||||
|
rows: Vec<DataRow>,
|
||||||
|
style_sheet: StyleSheet,
|
||||||
|
sort: SortType,
|
||||||
|
table_gap: u16,
|
||||||
|
on_select: Option<Box<dyn Fn(usize) -> Message>>,
|
||||||
|
on_selected_click: Option<Box<dyn Fn(usize) -> Message>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Message> TextTableBuilder<Message> {
|
||||||
|
pub fn new<S: Into<Cow<'static, str>>>(columns: Vec<S>) -> Self {
|
||||||
|
Self {
|
||||||
|
column_widths: vec![0; columns.len()],
|
||||||
|
columns: columns
|
||||||
|
.into_iter()
|
||||||
|
.map(|name| TextColumn::new(name))
|
||||||
|
.collect(),
|
||||||
|
show_gap: true,
|
||||||
|
show_selected_entry: true,
|
||||||
|
rows: Vec::default(),
|
||||||
|
style_sheet: StyleSheet::default(),
|
||||||
|
sort: SortType::Unsortable,
|
||||||
|
table_gap: 0,
|
||||||
|
on_select: None,
|
||||||
|
on_selected_click: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the row to display in the table.
|
||||||
|
///
|
||||||
|
/// Defaults to displaying no data if not set.
|
||||||
|
pub fn rows(mut self, rows: Vec<DataRow>) -> Self {
|
||||||
|
self.rows = rows;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a new row.
|
||||||
|
pub fn row(mut self, row: DataRow) -> Self {
|
||||||
|
self.rows.push(row);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether to try to show a gap between the table headers and data.
|
||||||
|
/// Note that if there isn't enough room, the gap will still be hidden.
|
||||||
|
///
|
||||||
|
/// Defaults to `true` if not set.
|
||||||
|
pub fn show_gap(mut self, show_gap: bool) -> Self {
|
||||||
|
self.show_gap = show_gap;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether to highlight the selected entry.
|
||||||
|
///
|
||||||
|
/// Defaults to `true` if not set.
|
||||||
|
pub fn show_selected_entry(mut self, show_selected_entry: bool) -> Self {
|
||||||
|
self.show_selected_entry = show_selected_entry;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// How the table should sort data on first initialization, if at all.
|
||||||
|
///
|
||||||
|
/// Defaults to [`SortType::Unsortable`] if not set.
|
||||||
|
pub fn default_sort(mut self, sort: SortType) -> Self {
|
||||||
|
self.sort = sort;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// What to do when selecting an entry. Expects a boxed function that takes in
|
||||||
|
/// the currently selected index and returns a [`Message`].
|
||||||
|
///
|
||||||
|
/// Defaults to `None` if not set.
|
||||||
|
pub fn on_select(mut self, on_select: Option<Box<dyn Fn(usize) -> Message>>) -> Self {
|
||||||
|
self.on_select = on_select;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// What to do when clicking on an entry that is already selected.
|
||||||
|
///
|
||||||
|
/// Defaults to `None` if not set.
|
||||||
|
pub fn on_selected_click(
|
||||||
|
mut self, on_selected_click: Option<Box<dyn Fn(usize) -> Message>>,
|
||||||
|
) -> Self {
|
||||||
|
self.on_selected_click = on_selected_click;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn try_sort_data(&mut self, sort_type: SortType) {
|
||||||
|
use std::cmp::Ordering;
|
||||||
|
|
||||||
|
// TODO: We can avoid some annoying checks by using const generics - this is waiting on
|
||||||
|
// the const_generics_defaults feature, landing in 1.59, however!
|
||||||
|
|
||||||
|
fn sort_cmp(column: usize, a: &DataRow, b: &DataRow) -> Ordering {
|
||||||
|
match (a.get(column), b.get(column)) {
|
||||||
|
(Some(a), Some(b)) => a.cmp(b),
|
||||||
|
(Some(_a), None) => Ordering::Greater,
|
||||||
|
(None, Some(_b)) => Ordering::Less,
|
||||||
|
(None, None) => Ordering::Equal,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match sort_type {
|
||||||
|
SortType::Ascending(column) => {
|
||||||
|
self.rows.sort_by(|a, b| sort_cmp(column, a, b));
|
||||||
|
}
|
||||||
|
SortType::Descending(column) => {
|
||||||
|
self.rows.sort_by(|a, b| sort_cmp(column, a, b));
|
||||||
|
self.rows.reverse();
|
||||||
|
}
|
||||||
|
SortType::Unsortable => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Message> StatefulTemplate<Message> for TextTableBuilder<Message> {
|
||||||
|
type Component = TextTable<Message>;
|
||||||
|
type ComponentState = TextTableState;
|
||||||
|
|
||||||
|
#[track_caller]
|
||||||
|
fn build(mut self, ctx: &mut ViewContext<'_>) -> Self::Component {
|
||||||
|
let sort = self.sort;
|
||||||
|
let (key, state) = ctx.register_and_mut_state_with_default::<_, Self::ComponentState, _>(
|
||||||
|
Location::caller(),
|
||||||
|
|| TextTableState {
|
||||||
|
scroll: Default::default(),
|
||||||
|
sort,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
state.scroll.set_num_items(self.rows.len());
|
||||||
|
self.try_sort_data(state.sort);
|
||||||
|
|
||||||
|
TextTable {
|
||||||
|
key,
|
||||||
|
column_widths: self.column_widths,
|
||||||
|
columns: self.columns,
|
||||||
|
show_gap: self.show_gap,
|
||||||
|
show_selected_entry: self.show_selected_entry,
|
||||||
|
rows: self.rows,
|
||||||
|
style_sheet: self.style_sheet,
|
||||||
|
table_gap: self.table_gap,
|
||||||
|
on_select: self.on_select,
|
||||||
|
on_selected_click: self.on_selected_click,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -2,7 +2,7 @@ pub mod table_column;
|
|||||||
pub use self::table_column::{TextColumn, TextColumnConstraint};
|
pub use self::table_column::{TextColumn, TextColumnConstraint};
|
||||||
|
|
||||||
mod table_scroll_state;
|
mod table_scroll_state;
|
||||||
use self::table_scroll_state::ScrollState as TextTableState;
|
use self::table_scroll_state::ScrollState;
|
||||||
|
|
||||||
pub mod data_row;
|
pub mod data_row;
|
||||||
pub use data_row::DataRow;
|
pub use data_row::DataRow;
|
||||||
@ -10,7 +10,13 @@ pub use data_row::DataRow;
|
|||||||
pub mod data_cell;
|
pub mod data_cell;
|
||||||
pub use data_cell::DataCell;
|
pub use data_cell::DataCell;
|
||||||
|
|
||||||
use std::{borrow::Cow, cmp::min, panic::Location};
|
pub mod builder;
|
||||||
|
pub use builder::TextTableBuilder;
|
||||||
|
|
||||||
|
pub mod sort_type;
|
||||||
|
pub use sort_type::SortType;
|
||||||
|
|
||||||
|
use std::cmp::min;
|
||||||
|
|
||||||
use tui::{
|
use tui::{
|
||||||
backend::Backend,
|
backend::Backend,
|
||||||
@ -23,7 +29,7 @@ use unicode_segmentation::UnicodeSegmentation;
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
constants::TABLE_GAP_HEIGHT_LIMIT,
|
constants::TABLE_GAP_HEIGHT_LIMIT,
|
||||||
tuine::{DrawContext, Event, Key, StateContext, Status, TmpComponent, ViewContext},
|
tuine::{DrawContext, Event, Key, StateContext, Status, TmpComponent},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default)]
|
#[derive(Clone, Debug, Default)]
|
||||||
@ -33,9 +39,10 @@ pub struct StyleSheet {
|
|||||||
table_header: Style,
|
table_header: Style,
|
||||||
}
|
}
|
||||||
|
|
||||||
enum SortStatus {
|
#[derive(PartialEq, Default)]
|
||||||
Unsortable,
|
pub struct TextTableState {
|
||||||
Sortable { column: usize, reverse: bool },
|
scroll: ScrollState,
|
||||||
|
sort: SortType,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A sortable, scrollable table for text data.
|
/// A sortable, scrollable table for text data.
|
||||||
@ -47,152 +54,12 @@ pub struct TextTable<Message> {
|
|||||||
show_selected_entry: bool,
|
show_selected_entry: bool,
|
||||||
rows: Vec<DataRow>,
|
rows: Vec<DataRow>,
|
||||||
style_sheet: StyleSheet,
|
style_sheet: StyleSheet,
|
||||||
sortable: SortStatus, // FIXME: Should this be stored in state?
|
|
||||||
table_gap: u16,
|
table_gap: u16,
|
||||||
on_select: Option<Box<dyn Fn(usize) -> Message>>,
|
on_select: Option<Box<dyn Fn(usize) -> Message>>,
|
||||||
on_selected_click: Option<Box<dyn Fn(usize) -> Message>>,
|
on_selected_click: Option<Box<dyn Fn(usize) -> Message>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<Message> TextTable<Message> {
|
impl<Message> TextTable<Message> {
|
||||||
#[track_caller]
|
|
||||||
pub fn new<S: Into<Cow<'static, str>>>(ctx: &mut ViewContext<'_>, columns: Vec<S>) -> Self {
|
|
||||||
Self {
|
|
||||||
key: ctx.register_component(Location::caller()),
|
|
||||||
column_widths: vec![0; columns.len()],
|
|
||||||
columns: columns
|
|
||||||
.into_iter()
|
|
||||||
.map(|name| TextColumn::new(name))
|
|
||||||
.collect(),
|
|
||||||
show_gap: true,
|
|
||||||
show_selected_entry: true,
|
|
||||||
rows: Vec::default(),
|
|
||||||
style_sheet: StyleSheet::default(),
|
|
||||||
sortable: SortStatus::Unsortable,
|
|
||||||
table_gap: 0,
|
|
||||||
on_select: None,
|
|
||||||
on_selected_click: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets the row to display in the table.
|
|
||||||
///
|
|
||||||
/// Defaults to displaying no data if not set.
|
|
||||||
pub fn rows(mut self, rows: Vec<DataRow>) -> Self {
|
|
||||||
self.rows = rows;
|
|
||||||
self.try_sort_data();
|
|
||||||
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Adds a new row.
|
|
||||||
pub fn row(mut self, row: DataRow) -> Self {
|
|
||||||
self.rows.push(row);
|
|
||||||
self.try_sort_data();
|
|
||||||
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Whether to try to show a gap between the table headers and data.
|
|
||||||
/// Note that if there isn't enough room, the gap will still be hidden.
|
|
||||||
///
|
|
||||||
/// Defaults to `true` if not set.
|
|
||||||
pub fn show_gap(mut self, show_gap: bool) -> Self {
|
|
||||||
self.show_gap = show_gap;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Whether to highlight the selected entry.
|
|
||||||
///
|
|
||||||
/// Defaults to `true` if not set.
|
|
||||||
pub fn show_selected_entry(mut self, show_selected_entry: bool) -> Self {
|
|
||||||
self.show_selected_entry = show_selected_entry;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Whether the table should display as sortable.
|
|
||||||
///
|
|
||||||
/// Defaults to unsortable if not set.
|
|
||||||
pub fn sortable(mut self, sortable: bool) -> Self {
|
|
||||||
self.sortable = if sortable {
|
|
||||||
SortStatus::Sortable {
|
|
||||||
column: 0,
|
|
||||||
reverse: false,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
SortStatus::Unsortable
|
|
||||||
};
|
|
||||||
self.try_sort_data();
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Calling this enables sorting, and sets the sort column to `column`.
|
|
||||||
pub fn sort_column(mut self, column: usize) -> Self {
|
|
||||||
self.sortable = match self.sortable {
|
|
||||||
SortStatus::Unsortable => SortStatus::Sortable {
|
|
||||||
column,
|
|
||||||
reverse: false,
|
|
||||||
},
|
|
||||||
SortStatus::Sortable { column: _, reverse } => SortStatus::Sortable { column, reverse },
|
|
||||||
};
|
|
||||||
self.try_sort_data();
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Calling this enables sorting, and sets the reverse status to `reverse`.
|
|
||||||
pub fn sort_reverse(mut self, reverse: bool) -> Self {
|
|
||||||
self.sortable = match self.sortable {
|
|
||||||
SortStatus::Unsortable => SortStatus::Sortable { column: 0, reverse },
|
|
||||||
SortStatus::Sortable { column, reverse: _ } => SortStatus::Sortable { column, reverse },
|
|
||||||
};
|
|
||||||
self.try_sort_data();
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns whether the table is currently sortable.
|
|
||||||
pub fn is_sortable(&self) -> bool {
|
|
||||||
matches!(self.sortable, SortStatus::Sortable { .. })
|
|
||||||
}
|
|
||||||
|
|
||||||
/// What to do when selecting an entry. Expects a boxed function that takes in
|
|
||||||
/// the currently selected index and returns a [`Message`].
|
|
||||||
///
|
|
||||||
/// Defaults to `None` if not set.
|
|
||||||
pub fn on_select(mut self, on_select: Option<Box<dyn Fn(usize) -> Message>>) -> Self {
|
|
||||||
self.on_select = on_select;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// What to do when clicking on an entry that is already selected.
|
|
||||||
///
|
|
||||||
/// Defaults to `None` if not set.
|
|
||||||
pub fn on_selected_click(
|
|
||||||
mut self, on_selected_click: Option<Box<dyn Fn(usize) -> Message>>,
|
|
||||||
) -> Self {
|
|
||||||
self.on_selected_click = on_selected_click;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
fn try_sort_data(&mut self) {
|
|
||||||
use std::cmp::Ordering;
|
|
||||||
|
|
||||||
if let SortStatus::Sortable { column, reverse } = self.sortable {
|
|
||||||
// TODO: We can avoid some annoying checks by using const generics - this is waiting on
|
|
||||||
// the const_generics_defaults feature, landing in 1.59, however!
|
|
||||||
|
|
||||||
self.rows
|
|
||||||
.sort_by(|a, b| match (a.get(column), b.get(column)) {
|
|
||||||
(Some(a), Some(b)) => a.cmp(b),
|
|
||||||
(Some(_a), None) => Ordering::Greater,
|
|
||||||
(None, Some(_b)) => Ordering::Less,
|
|
||||||
(None, None) => Ordering::Equal,
|
|
||||||
});
|
|
||||||
|
|
||||||
if reverse {
|
|
||||||
self.rows.reverse();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn update_column_widths(&mut self, bounds: Rect) {
|
fn update_column_widths(&mut self, bounds: Rect) {
|
||||||
let total_width = bounds.width;
|
let total_width = bounds.width;
|
||||||
let mut width_remaining = bounds.width;
|
let mut width_remaining = bounds.width;
|
||||||
@ -251,7 +118,7 @@ impl<Message> TmpComponent<Message> for TextTable<Message> {
|
|||||||
{
|
{
|
||||||
let rect = draw_ctx.rect();
|
let rect = draw_ctx.rect();
|
||||||
let state = state_ctx.mut_state::<TextTableState>(self.key);
|
let state = state_ctx.mut_state::<TextTableState>(self.key);
|
||||||
state.set_num_items(self.rows.len()); // FIXME: Not a fan of this system like this - should be easier to do.
|
state.scroll.set_num_items(self.rows.len()); // FIXME: Not a fan of this system like this - should be easier to do.
|
||||||
|
|
||||||
self.table_gap = if !self.show_gap
|
self.table_gap = if !self.show_gap
|
||||||
|| (self.rows.len() + 2 > rect.height.into() && rect.height < TABLE_GAP_HEIGHT_LIMIT)
|
|| (self.rows.len() + 2 > rect.height.into() && rect.height < TABLE_GAP_HEIGHT_LIMIT)
|
||||||
@ -276,8 +143,10 @@ impl<Message> TmpComponent<Message> for TextTable<Message> {
|
|||||||
// as well as truncating some entries based on available width.
|
// as well as truncating some entries based on available width.
|
||||||
let data_slice = {
|
let data_slice = {
|
||||||
// Note: `get_list_start` already ensures `start` is within the bounds of the number of items, so no need to check!
|
// Note: `get_list_start` already ensures `start` is within the bounds of the number of items, so no need to check!
|
||||||
let start = state.display_start_index(rect, scrollable_height as usize);
|
let start = state
|
||||||
let end = min(state.num_items(), start + scrollable_height as usize);
|
.scroll
|
||||||
|
.display_start_index(rect, scrollable_height as usize);
|
||||||
|
let end = min(state.scroll.num_items(), start + scrollable_height as usize);
|
||||||
|
|
||||||
debug!("Start: {}, end: {}", start, end);
|
debug!("Start: {}, end: {}", start, end);
|
||||||
self.rows.drain(start..end).into_iter().map(|row| {
|
self.rows.drain(start..end).into_iter().map(|row| {
|
||||||
@ -299,7 +168,7 @@ impl<Message> TmpComponent<Message> for TextTable<Message> {
|
|||||||
table = table.highlight_style(self.style_sheet.selected_text);
|
table = table.highlight_style(self.style_sheet.selected_text);
|
||||||
}
|
}
|
||||||
|
|
||||||
frame.render_stateful_widget(table.widths(&widths), rect, state.tui_state());
|
frame.render_stateful_widget(table.widths(&widths), rect, state.scroll.tui_state());
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_event(
|
fn on_event(
|
||||||
@ -311,7 +180,6 @@ impl<Message> TmpComponent<Message> for TextTable<Message> {
|
|||||||
|
|
||||||
let rect = draw_ctx.rect();
|
let rect = draw_ctx.rect();
|
||||||
let state = state_ctx.mut_state::<TextTableState>(self.key);
|
let state = state_ctx.mut_state::<TextTableState>(self.key);
|
||||||
state.set_num_items(self.rows.len());
|
|
||||||
|
|
||||||
match event {
|
match event {
|
||||||
Event::Keyboard(key_event) => {
|
Event::Keyboard(key_event) => {
|
||||||
@ -330,30 +198,37 @@ impl<Message> TmpComponent<Message> for TextTable<Message> {
|
|||||||
let y = mouse_event.row - rect.top();
|
let y = mouse_event.row - rect.top();
|
||||||
|
|
||||||
if y == 0 {
|
if y == 0 {
|
||||||
if let SortStatus::Sortable { column, reverse } = self.sortable {
|
match state.sort {
|
||||||
todo!() // Sort by the clicked column! If already using column, reverse!
|
SortType::Unsortable => Status::Ignored,
|
||||||
// self.sort_data();
|
SortType::Ascending(column) => {
|
||||||
} else {
|
// Sort by the clicked column! If already using column, reverse!
|
||||||
Status::Ignored
|
// self.sort_data();
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
SortType::Descending(column) => {
|
||||||
|
// Sort by the clicked column! If already using column, reverse!
|
||||||
|
// self.sort_data();
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if y > self.table_gap {
|
} else if y > self.table_gap {
|
||||||
let visual_index = usize::from(y - self.table_gap);
|
let visual_index = usize::from(y - self.table_gap);
|
||||||
state.set_visual_index(visual_index)
|
state.scroll.set_visual_index(visual_index)
|
||||||
} else {
|
} else {
|
||||||
Status::Ignored
|
Status::Ignored
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
MouseEventKind::ScrollDown => {
|
MouseEventKind::ScrollDown => {
|
||||||
let status = state.move_down(1);
|
let status = state.scroll.move_down(1);
|
||||||
if let Some(on_select) = &self.on_select {
|
if let Some(on_select) = &self.on_select {
|
||||||
messages.push(on_select(state.current_index()));
|
messages.push(on_select(state.scroll.current_index()));
|
||||||
}
|
}
|
||||||
status
|
status
|
||||||
}
|
}
|
||||||
MouseEventKind::ScrollUp => {
|
MouseEventKind::ScrollUp => {
|
||||||
let status = state.move_up(1);
|
let status = state.scroll.move_up(1);
|
||||||
if let Some(on_select) = &self.on_select {
|
if let Some(on_select) = &self.on_select {
|
||||||
messages.push(on_select(state.current_index()));
|
messages.push(on_select(state.scroll.current_index()));
|
||||||
}
|
}
|
||||||
status
|
status
|
||||||
}
|
}
|
||||||
@ -369,7 +244,10 @@ impl<Message> TmpComponent<Message> for TextTable<Message> {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::tuine::{StateMap, ViewContext};
|
use crate::tuine::{
|
||||||
|
text_table::{SortType, TextTableBuilder},
|
||||||
|
StateMap, StatefulTemplate, ViewContext,
|
||||||
|
};
|
||||||
|
|
||||||
use super::{DataRow, TextTable};
|
use super::{DataRow, TextTable};
|
||||||
|
|
||||||
@ -390,9 +268,10 @@ mod tests {
|
|||||||
let index = 1;
|
let index = 1;
|
||||||
|
|
||||||
let mut map = StateMap::default();
|
let mut map = StateMap::default();
|
||||||
let table: TextTable<Message> = TextTable::new(&mut ctx(&mut map), vec!["Sensor", "Temp"])
|
let table: TextTable<Message> = TextTableBuilder::new(vec!["Sensor", "Temp"])
|
||||||
.sort_column(index)
|
.default_sort(SortType::Ascending(index))
|
||||||
.rows(rows);
|
.rows(rows)
|
||||||
|
.build(&mut ctx(&mut map));
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
table.rows.len(),
|
table.rows.len(),
|
||||||
@ -422,10 +301,11 @@ mod tests {
|
|||||||
let new_index = 0;
|
let new_index = 0;
|
||||||
|
|
||||||
let mut map = StateMap::default();
|
let mut map = StateMap::default();
|
||||||
let table: TextTable<Message> = TextTable::new(&mut ctx(&mut map), vec!["Sensor", "Temp"])
|
let table: TextTable<Message> = TextTableBuilder::new(vec!["Sensor", "Temp"])
|
||||||
.sort_column(index)
|
.default_sort(SortType::Ascending(index))
|
||||||
.rows(rows)
|
.rows(rows)
|
||||||
.sort_column(new_index);
|
.default_sort(SortType::Ascending(new_index))
|
||||||
|
.build(&mut ctx(&mut map));
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
table.rows.len(),
|
table.rows.len(),
|
||||||
@ -454,10 +334,10 @@ mod tests {
|
|||||||
let index = 1;
|
let index = 1;
|
||||||
|
|
||||||
let mut map = StateMap::default();
|
let mut map = StateMap::default();
|
||||||
let table: TextTable<Message> = TextTable::new(&mut ctx(&mut map), vec!["Sensor", "Temp"])
|
let table: TextTable<Message> = TextTableBuilder::new(vec!["Sensor", "Temp"])
|
||||||
.sort_column(index)
|
.default_sort(SortType::Descending(index))
|
||||||
.sort_reverse(true)
|
.rows(rows)
|
||||||
.rows(rows);
|
.build(&mut ctx(&mut map));
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
table.rows.len(),
|
table.rows.len(),
|
||||||
@ -486,10 +366,11 @@ mod tests {
|
|||||||
let index = 1;
|
let index = 1;
|
||||||
|
|
||||||
let mut map = StateMap::default();
|
let mut map = StateMap::default();
|
||||||
let table: TextTable<Message> = TextTable::new(&mut ctx(&mut map), vec!["Sensor", "Temp"])
|
let table: TextTable<Message> = TextTableBuilder::new(vec!["Sensor", "Temp"])
|
||||||
.rows(rows)
|
.rows(rows)
|
||||||
.sort_column(index)
|
.default_sort(SortType::Ascending(index))
|
||||||
.row(DataRow::default().cell("X").cell(0));
|
.row(DataRow::default().cell("X").cell(0))
|
||||||
|
.build(&mut ctx(&mut map));
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
table.rows.len(),
|
table.rows.len(),
|
||||||
@ -519,9 +400,10 @@ mod tests {
|
|||||||
let row_length = original_rows.len();
|
let row_length = original_rows.len();
|
||||||
|
|
||||||
let mut map = StateMap::default();
|
let mut map = StateMap::default();
|
||||||
let table: TextTable<Message> = TextTable::new(&mut ctx(&mut map), vec!["Sensor", "Temp"])
|
let table: TextTable<Message> = TextTableBuilder::new(vec!["Sensor", "Temp"])
|
||||||
.rows(rows)
|
.rows(rows)
|
||||||
.row(original_rows[3].clone());
|
.row(original_rows[3].clone())
|
||||||
|
.build(&mut ctx(&mut map));
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
table.rows.len(),
|
table.rows.len(),
|
||||||
|
12
src/tuine/component/base/text_table/sort_type.rs
Normal file
12
src/tuine/component/base/text_table/sort_type.rs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
#[derive(Clone, Copy, PartialEq)]
|
||||||
|
pub enum SortType {
|
||||||
|
Unsortable,
|
||||||
|
Ascending(usize),
|
||||||
|
Descending(usize),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for SortType {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::Unsortable
|
||||||
|
}
|
||||||
|
}
|
@ -4,6 +4,9 @@ pub use base::*;
|
|||||||
pub mod widget;
|
pub mod widget;
|
||||||
pub use widget::*;
|
pub use widget::*;
|
||||||
|
|
||||||
|
pub mod stateful;
|
||||||
|
pub use stateful::*;
|
||||||
|
|
||||||
use enum_dispatch::enum_dispatch;
|
use enum_dispatch::enum_dispatch;
|
||||||
use tui::Frame;
|
use tui::Frame;
|
||||||
|
|
||||||
|
14
src/tuine/component/stateful.rs
Normal file
14
src/tuine/component/stateful.rs
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
use crate::tuine::{State, ViewContext};
|
||||||
|
|
||||||
|
use super::TmpComponent;
|
||||||
|
|
||||||
|
/// A [`StatefulTemplate`] is a builder-style pattern for building a stateful
|
||||||
|
/// [`Component`].
|
||||||
|
///
|
||||||
|
/// Inspired by Flutter's StatefulWidget interface.
|
||||||
|
pub trait StatefulTemplate<Message> {
|
||||||
|
type Component: TmpComponent<Message>;
|
||||||
|
type ComponentState: State;
|
||||||
|
|
||||||
|
fn build(self, ctx: &mut ViewContext<'_>) -> Self::Component;
|
||||||
|
}
|
@ -1,4 +1,7 @@
|
|||||||
use crate::tuine::{text_table::DataRow, Shortcut, TextTable, TmpComponent, ViewContext};
|
use crate::tuine::{
|
||||||
|
text_table::{DataRow, SortType, TextTableBuilder},
|
||||||
|
Shortcut, StatefulTemplate, TextTable, TmpComponent, ViewContext,
|
||||||
|
};
|
||||||
|
|
||||||
/// A [`TempTable`] is a text table that is meant to display temperature data.
|
/// A [`TempTable`] is a text table that is meant to display temperature data.
|
||||||
pub struct TempTable<Message> {
|
pub struct TempTable<Message> {
|
||||||
@ -10,13 +13,14 @@ impl<Message> TempTable<Message> {
|
|||||||
pub fn new(ctx: &mut ViewContext<'_>) -> Self {
|
pub fn new(ctx: &mut ViewContext<'_>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
inner: Shortcut::with_child(
|
inner: Shortcut::with_child(
|
||||||
TextTable::new(ctx, vec!["Sensor", "Temp"])
|
TextTableBuilder::new(vec!["Sensor", "Temp"])
|
||||||
.rows(vec![
|
.rows(vec![
|
||||||
DataRow::default().cell("A").cell(2),
|
DataRow::default().cell("A").cell(2),
|
||||||
DataRow::default().cell("B").cell(3),
|
DataRow::default().cell("B").cell(3),
|
||||||
DataRow::default().cell("C").cell(1),
|
DataRow::default().cell("C").cell(1),
|
||||||
])
|
])
|
||||||
.sort_column(1),
|
.default_sort(SortType::Ascending(1))
|
||||||
|
.build(ctx),
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,4 +16,16 @@ impl<'a> StateContext<'a> {
|
|||||||
pub fn mut_state<S: State + Default + 'static>(&mut self, key: Key) -> &mut S {
|
pub fn mut_state<S: State + Default + 'static>(&mut self, key: Key) -> &mut S {
|
||||||
self.state_map.mut_state::<S>(key)
|
self.state_map.mut_state::<S>(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn state_with_default<S: State + 'static, F: FnOnce() -> S>(
|
||||||
|
&mut self, key: Key, default: F,
|
||||||
|
) -> &S {
|
||||||
|
self.state_map.state_with_default::<S, F>(key, default)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn mut_state_with_default<S: State + 'static, F: FnOnce() -> S>(
|
||||||
|
&mut self, key: Key, default: F,
|
||||||
|
) -> &mut S {
|
||||||
|
self.state_map.mut_state_with_default::<S, F>(key, default)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,34 +3,44 @@ use rustc_hash::FxHashMap;
|
|||||||
use crate::tuine::{Key, State};
|
use crate::tuine::{Key, State};
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct StateMap(FxHashMap<Key, (Box<dyn State>, bool)>);
|
pub struct StateMap(FxHashMap<Key, Box<dyn State>>);
|
||||||
|
|
||||||
impl StateMap {
|
impl StateMap {
|
||||||
pub fn state<S: State + Default + 'static>(&mut self, key: Key) -> &S {
|
pub fn state<S: State + Default + 'static>(&mut self, key: Key) -> &S {
|
||||||
let state = self
|
let state = self.0.entry(key).or_insert_with(|| Box::new(S::default()));
|
||||||
.0
|
|
||||||
.entry(key)
|
|
||||||
.or_insert_with(|| (Box::new(S::default()), true));
|
|
||||||
|
|
||||||
state.1 = true;
|
|
||||||
|
|
||||||
state
|
state
|
||||||
.0
|
|
||||||
.as_any()
|
.as_any()
|
||||||
.downcast_ref()
|
.downcast_ref()
|
||||||
.expect("Successful downcast of state.")
|
.expect("Successful downcast of state.")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn mut_state<S: State + Default + 'static>(&mut self, key: Key) -> &mut S {
|
pub fn mut_state<S: State + Default + 'static>(&mut self, key: Key) -> &mut S {
|
||||||
let state = self
|
let state = self.0.entry(key).or_insert_with(|| Box::new(S::default()));
|
||||||
.0
|
|
||||||
.entry(key)
|
state
|
||||||
.or_insert_with(|| (Box::new(S::default()), true));
|
.as_mut_any()
|
||||||
|
.downcast_mut()
|
||||||
state.1 = true;
|
.expect("Successful downcast of state.")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn state_with_default<S: State + 'static, F: FnOnce() -> S>(
|
||||||
|
&mut self, key: Key, default: F,
|
||||||
|
) -> &S {
|
||||||
|
let state = self.0.entry(key).or_insert_with(|| Box::new(default()));
|
||||||
|
|
||||||
|
state
|
||||||
|
.as_any()
|
||||||
|
.downcast_ref()
|
||||||
|
.expect("Successful downcast of state.")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn mut_state_with_default<S: State + 'static, F: FnOnce() -> S>(
|
||||||
|
&mut self, key: Key, default: F,
|
||||||
|
) -> &mut S {
|
||||||
|
let state = self.0.entry(key).or_insert_with(|| Box::new(default()));
|
||||||
|
|
||||||
state
|
state
|
||||||
.0
|
|
||||||
.as_mut_any()
|
.as_mut_any()
|
||||||
.downcast_mut()
|
.downcast_mut()
|
||||||
.expect("Successful downcast of state.")
|
.expect("Successful downcast of state.")
|
||||||
|
@ -27,4 +27,48 @@ impl<'a> ViewContext<'a> {
|
|||||||
pub fn mut_state<S: State + Default + 'static>(&mut self, key: Key) -> &mut S {
|
pub fn mut_state<S: State + Default + 'static>(&mut self, key: Key) -> &mut S {
|
||||||
self.state_context.mut_state(key)
|
self.state_context.mut_state(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn register_and_state<C: Into<Caller>, S: State + Default + 'static>(
|
||||||
|
&mut self, caller: C,
|
||||||
|
) -> (Key, &S) {
|
||||||
|
self.key_counter += 1;
|
||||||
|
let key = Key::new(caller.into(), self.key_counter);
|
||||||
|
|
||||||
|
(key, self.state(key))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn register_and_mut_state<C: Into<Caller>, S: State + Default + 'static>(
|
||||||
|
&mut self, caller: C,
|
||||||
|
) -> (Key, &mut S) {
|
||||||
|
self.key_counter += 1;
|
||||||
|
let key = Key::new(caller.into(), self.key_counter);
|
||||||
|
|
||||||
|
(key, self.mut_state(key))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn register_and_state_with_default<
|
||||||
|
C: Into<Caller>,
|
||||||
|
S: State + 'static,
|
||||||
|
F: FnOnce() -> S,
|
||||||
|
>(
|
||||||
|
&mut self, caller: C, default: F,
|
||||||
|
) -> (Key, &S) {
|
||||||
|
self.key_counter += 1;
|
||||||
|
let key = Key::new(caller.into(), self.key_counter);
|
||||||
|
|
||||||
|
(key, self.state_context.state_with_default(key, default))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn register_and_mut_state_with_default<
|
||||||
|
C: Into<Caller>,
|
||||||
|
S: State + 'static,
|
||||||
|
F: FnOnce() -> S,
|
||||||
|
>(
|
||||||
|
&mut self, caller: C, default: F,
|
||||||
|
) -> (Key, &mut S) {
|
||||||
|
self.key_counter += 1;
|
||||||
|
let key = Key::new(caller.into(), self.key_counter);
|
||||||
|
|
||||||
|
(key, self.state_context.mut_state_with_default(key, default))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user