mirror of
https://github.com/ClementTsang/bottom.git
synced 2025-04-08 17:05:59 +02:00
Sorting
This commit is contained in:
parent
3e5dbb75fb
commit
211cfeb3ee
15
src/app.rs
15
src/app.rs
@ -29,7 +29,7 @@ use frozen_state::FrozenState;
|
||||
use crate::{
|
||||
canvas::Painter,
|
||||
constants,
|
||||
tuine::{Application, Element, Flex, ViewContext},
|
||||
tuine::{Application, Element, Flex, Status, ViewContext},
|
||||
units::data_units::DataUnit,
|
||||
Pid,
|
||||
};
|
||||
@ -267,7 +267,7 @@ impl Application for AppState {
|
||||
|
||||
fn global_event_handler(
|
||||
&mut self, event: crate::tuine::Event, messages: &mut Vec<Self::Message>,
|
||||
) {
|
||||
) -> Status {
|
||||
use crate::tuine::Event;
|
||||
use crossterm::event::{KeyCode, KeyModifiers};
|
||||
|
||||
@ -277,22 +277,27 @@ impl Application for AppState {
|
||||
match event.code {
|
||||
KeyCode::Char('q') | KeyCode::Char('Q') => {
|
||||
messages.push(AppMessages::Quit);
|
||||
Status::Captured
|
||||
}
|
||||
_ => {}
|
||||
_ => Status::Ignored,
|
||||
}
|
||||
} else if let KeyModifiers::CONTROL = event.modifiers {
|
||||
match event.code {
|
||||
KeyCode::Char('c') | KeyCode::Char('C') => {
|
||||
messages.push(AppMessages::Quit);
|
||||
Status::Captured
|
||||
}
|
||||
KeyCode::Char('r') | KeyCode::Char('R') => {
|
||||
messages.push(AppMessages::Reset);
|
||||
Status::Captured
|
||||
}
|
||||
_ => {}
|
||||
_ => Status::Ignored,
|
||||
}
|
||||
} else {
|
||||
Status::Ignored
|
||||
}
|
||||
}
|
||||
Event::Mouse(_event) => {}
|
||||
Event::Mouse(_event) => Status::Ignored,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ use tui::Terminal;
|
||||
|
||||
use super::{
|
||||
runtime::{self, RuntimeEvent},
|
||||
Element, Event, ViewContext,
|
||||
Element, Event, Status, ViewContext,
|
||||
};
|
||||
|
||||
/// An alias to the [`tui::backend::CrosstermBackend`] writing to [`std::io::Stdout`].
|
||||
@ -31,7 +31,9 @@ pub trait Application: Sized {
|
||||
/// *only* if it is not handled at all by it.
|
||||
///
|
||||
/// Defaults to not doing anything.
|
||||
fn global_event_handler(&mut self, event: Event, messages: &mut Vec<Self::Message>) {}
|
||||
fn global_event_handler(&mut self, event: Event, messages: &mut Vec<Self::Message>) -> Status {
|
||||
Status::Ignored
|
||||
}
|
||||
}
|
||||
|
||||
/// Launches some application with tuine. Note this will take over the calling thread.
|
||||
|
@ -1,73 +1,61 @@
|
||||
use std::{borrow::Cow, fmt::Display};
|
||||
|
||||
use enum_dispatch::enum_dispatch;
|
||||
use float_ord::FloatOrd;
|
||||
use tui::widgets::Cell;
|
||||
|
||||
#[enum_dispatch]
|
||||
pub trait Numeric {}
|
||||
impl Numeric for f64 {}
|
||||
impl Numeric for f32 {}
|
||||
impl Numeric for i64 {}
|
||||
impl Numeric for i32 {}
|
||||
impl Numeric for i16 {}
|
||||
impl Numeric for i8 {}
|
||||
impl Numeric for isize {}
|
||||
impl Numeric for u64 {}
|
||||
impl Numeric for u32 {}
|
||||
impl Numeric for u16 {}
|
||||
impl Numeric for u8 {}
|
||||
impl Numeric for usize {}
|
||||
pub trait DataCellValue {}
|
||||
|
||||
impl DataCellValue for FloatOrd<f64> {}
|
||||
impl DataCellValue for FloatOrd<f32> {}
|
||||
impl DataCellValue for i64 {}
|
||||
impl DataCellValue for i32 {}
|
||||
impl DataCellValue for i16 {}
|
||||
impl DataCellValue for i8 {}
|
||||
impl DataCellValue for isize {}
|
||||
impl DataCellValue for u64 {}
|
||||
impl DataCellValue for u32 {}
|
||||
impl DataCellValue for u16 {}
|
||||
impl DataCellValue for u8 {}
|
||||
impl DataCellValue for usize {}
|
||||
impl DataCellValue for Cow<'static, str> {}
|
||||
|
||||
#[allow(non_camel_case_types)]
|
||||
#[derive(Clone, Copy)]
|
||||
#[enum_dispatch(Numeric)]
|
||||
pub enum Number {
|
||||
f64,
|
||||
f32,
|
||||
i64,
|
||||
i32,
|
||||
i16,
|
||||
i8,
|
||||
isize,
|
||||
u64,
|
||||
u32,
|
||||
u16,
|
||||
u8,
|
||||
usize,
|
||||
}
|
||||
|
||||
impl Display for Number {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match &self {
|
||||
Number::f64(val) => write!(f, "{}", val),
|
||||
Number::f32(val) => write!(f, "{}", val),
|
||||
Number::i64(val) => write!(f, "{}", val),
|
||||
Number::i32(val) => write!(f, "{}", val),
|
||||
Number::i16(val) => write!(f, "{}", val),
|
||||
Number::i8(val) => write!(f, "{}", val),
|
||||
Number::isize(val) => write!(f, "{}", val),
|
||||
Number::u64(val) => write!(f, "{}", val),
|
||||
Number::u32(val) => write!(f, "{}", val),
|
||||
Number::u16(val) => write!(f, "{}", val),
|
||||
Number::u8(val) => write!(f, "{}", val),
|
||||
Number::usize(val) => write!(f, "{}", val),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||
#[enum_dispatch(DataCellValue)]
|
||||
pub enum DataCell {
|
||||
NumberCell(Number),
|
||||
String(Cow<'static, str>),
|
||||
f64(FloatOrd<f64>),
|
||||
f32(FloatOrd<f32>),
|
||||
i64(i64),
|
||||
i32(i32),
|
||||
i16(i16),
|
||||
i8(i8),
|
||||
isize(isize),
|
||||
u64(u64),
|
||||
u32(u32),
|
||||
u16(u16),
|
||||
u8(u8),
|
||||
usize(usize),
|
||||
Cow(Cow<'static, str>),
|
||||
}
|
||||
|
||||
impl DataCell {}
|
||||
|
||||
impl Display for DataCell {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
DataCell::NumberCell(n) => n.fmt(f),
|
||||
DataCell::String(d) => d.fmt(f),
|
||||
DataCell::f64(val) => val.0.fmt(f),
|
||||
DataCell::f32(val) => val.0.fmt(f),
|
||||
DataCell::i64(val) => val.fmt(f),
|
||||
DataCell::i32(val) => val.fmt(f),
|
||||
DataCell::i16(val) => val.fmt(f),
|
||||
DataCell::i8(val) => val.fmt(f),
|
||||
DataCell::isize(val) => val.fmt(f),
|
||||
DataCell::u64(val) => val.fmt(f),
|
||||
DataCell::u32(val) => val.fmt(f),
|
||||
DataCell::u16(val) => val.fmt(f),
|
||||
DataCell::u8(val) => val.fmt(f),
|
||||
DataCell::usize(val) => val.fmt(f),
|
||||
DataCell::Cow(val) => val.fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -78,20 +66,26 @@ impl From<DataCell> for Cell<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Number> for DataCell {
|
||||
fn from(num: Number) -> Self {
|
||||
DataCell::NumberCell(num)
|
||||
impl From<f64> for DataCell {
|
||||
fn from(num: f64) -> Self {
|
||||
DataCell::f64(FloatOrd(num))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<f32> for DataCell {
|
||||
fn from(num: f32) -> Self {
|
||||
DataCell::f32(FloatOrd(num))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for DataCell {
|
||||
fn from(s: String) -> Self {
|
||||
DataCell::String(s.into())
|
||||
DataCell::Cow(Cow::from(s))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&'static str> for DataCell {
|
||||
fn from(s: &'static str) -> Self {
|
||||
DataCell::String(s.into())
|
||||
DataCell::Cow(Cow::from(s))
|
||||
}
|
||||
}
|
||||
|
@ -2,21 +2,33 @@ use tui::{style::Style, widgets::Row};
|
||||
|
||||
use super::DataCell;
|
||||
|
||||
#[derive(Clone)]
|
||||
#[derive(Default, Clone)]
|
||||
pub struct DataRow {
|
||||
pub cells: Vec<DataCell>,
|
||||
pub style: Option<Style>,
|
||||
cells: Vec<DataCell>,
|
||||
style: Option<Style>,
|
||||
}
|
||||
|
||||
impl DataRow {
|
||||
pub fn new(cells: Vec<DataCell>) -> Self {
|
||||
Self { cells, style: None }
|
||||
pub fn new_with_vec<D: Into<DataCell>>(cells: Vec<D>) -> Self {
|
||||
Self {
|
||||
cells: cells.into_iter().map(Into::into).collect(),
|
||||
style: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cell<D: Into<DataCell>>(mut self, cell: D) -> Self {
|
||||
self.cells.push(cell.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn style(mut self, style: Option<Style>) -> Self {
|
||||
self.style = style;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn get(&self, index: usize) -> Option<&DataCell> {
|
||||
self.cells.get(index)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DataRow> for Row<'_> {
|
||||
|
@ -33,6 +33,11 @@ pub struct StyleSheet {
|
||||
table_header: Style,
|
||||
}
|
||||
|
||||
enum SortStatus {
|
||||
Unsortable,
|
||||
Sortable { column: usize },
|
||||
}
|
||||
|
||||
/// A sortable, scrollable table for text data.
|
||||
pub struct TextTable<Message> {
|
||||
key: Key,
|
||||
@ -42,7 +47,7 @@ pub struct TextTable<Message> {
|
||||
show_selected_entry: bool,
|
||||
rows: Vec<DataRow>,
|
||||
style_sheet: StyleSheet,
|
||||
sortable: bool,
|
||||
sortable: SortStatus,
|
||||
table_gap: u16,
|
||||
on_select: Option<Box<dyn Fn(usize) -> Message>>,
|
||||
on_selected_click: Option<Box<dyn Fn(usize) -> Message>>,
|
||||
@ -62,7 +67,7 @@ impl<Message> TextTable<Message> {
|
||||
show_selected_entry: true,
|
||||
rows: Vec::default(),
|
||||
style_sheet: StyleSheet::default(),
|
||||
sortable: false,
|
||||
sortable: SortStatus::Unsortable,
|
||||
table_gap: 0,
|
||||
on_select: None,
|
||||
on_selected_click: None,
|
||||
@ -74,10 +79,7 @@ impl<Message> TextTable<Message> {
|
||||
/// Defaults to displaying no data if not set.
|
||||
pub fn rows(mut self, rows: Vec<DataRow>) -> Self {
|
||||
self.rows = rows;
|
||||
|
||||
if self.sortable {
|
||||
self.sort_data();
|
||||
}
|
||||
self.try_sort_data();
|
||||
|
||||
self
|
||||
}
|
||||
@ -101,17 +103,33 @@ impl<Message> TextTable<Message> {
|
||||
|
||||
/// Whether the table should display as sortable.
|
||||
///
|
||||
/// Defaults to `false` if not set.
|
||||
/// Defaults to unsortable if not set.
|
||||
pub fn sortable(mut self, sortable: bool) -> Self {
|
||||
self.sortable = sortable;
|
||||
self.sortable = if sortable {
|
||||
SortStatus::Sortable { column: 0 }
|
||||
} else {
|
||||
SortStatus::Unsortable
|
||||
};
|
||||
|
||||
if self.sortable {
|
||||
self.sort_data();
|
||||
}
|
||||
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 = SortStatus::Sortable { column };
|
||||
|
||||
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`].
|
||||
///
|
||||
@ -131,8 +149,21 @@ impl<Message> TextTable<Message> {
|
||||
self
|
||||
}
|
||||
|
||||
fn sort_data(&mut self) {
|
||||
self.rows.sort_by(|a, b| todo!());
|
||||
fn try_sort_data(&mut self) {
|
||||
use std::cmp::Ordering;
|
||||
|
||||
if let SortStatus::Sortable { column } = self.sortable {
|
||||
// TODO: We can avoid some annoying checks vy 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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn update_column_widths(&mut self, bounds: Rect) {
|
||||
@ -193,6 +224,7 @@ impl<Message> TmpComponent<Message> for TextTable<Message> {
|
||||
{
|
||||
let rect = draw_ctx.rect();
|
||||
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.
|
||||
|
||||
self.table_gap = if !self.show_gap
|
||||
|| (self.rows.len() + 2 > rect.height.into() && rect.height < TABLE_GAP_HEIGHT_LIMIT)
|
||||
@ -220,6 +252,7 @@ impl<Message> TmpComponent<Message> for TextTable<Message> {
|
||||
let start = state.display_start_index(rect, scrollable_height as usize);
|
||||
let end = min(state.num_items(), start + scrollable_height as usize);
|
||||
|
||||
debug!("Start: {}, end: {}", start, end);
|
||||
self.rows.drain(start..end).into_iter().map(|row| {
|
||||
let r: Row<'_> = row.into();
|
||||
r
|
||||
@ -251,6 +284,7 @@ impl<Message> TmpComponent<Message> for TextTable<Message> {
|
||||
|
||||
let rect = draw_ctx.rect();
|
||||
let state = state_ctx.mut_state::<TextTableState>(self.key);
|
||||
state.set_num_items(self.rows.len());
|
||||
|
||||
match event {
|
||||
Event::Keyboard(key_event) => {
|
||||
@ -268,8 +302,13 @@ impl<Message> TmpComponent<Message> for TextTable<Message> {
|
||||
MouseEventKind::Down(MouseButton::Left) => {
|
||||
let y = mouse_event.row - rect.top();
|
||||
|
||||
if self.sortable && y == 0 {
|
||||
todo!() // Sort by the clicked column!
|
||||
if y == 0 {
|
||||
if let SortStatus::Sortable { column } = self.sortable {
|
||||
todo!() // Sort by the clicked column!
|
||||
// self.sort_data();
|
||||
} else {
|
||||
Status::Ignored
|
||||
}
|
||||
} else if y > self.table_gap {
|
||||
let visual_index = usize::from(y - self.table_gap);
|
||||
state.set_visual_index(visual_index)
|
||||
|
@ -1,4 +1,4 @@
|
||||
use crate::tuine::{Shortcut, TextTable, TmpComponent, ViewContext};
|
||||
use crate::tuine::{text_table::DataRow, Shortcut, TextTable, TmpComponent, ViewContext};
|
||||
|
||||
/// A [`TempTable`] is a text table that is meant to display temperature data.
|
||||
pub struct TempTable<Message> {
|
||||
@ -9,7 +9,15 @@ impl<Message> TempTable<Message> {
|
||||
#[track_caller]
|
||||
pub fn new(ctx: &mut ViewContext<'_>) -> Self {
|
||||
Self {
|
||||
inner: Shortcut::with_child(TextTable::new(ctx, vec!["Sensor", "Temp"])),
|
||||
inner: Shortcut::with_child(
|
||||
TextTable::new(ctx, vec!["Sensor", "Temp"])
|
||||
.rows(vec![
|
||||
DataRow::default().cell("A").cell(2),
|
||||
DataRow::default().cell("B").cell(3),
|
||||
DataRow::default().cell("C").cell(1),
|
||||
])
|
||||
.sort_column(1),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -41,15 +41,24 @@ where
|
||||
if let Ok(event) = receiver.recv() {
|
||||
match event {
|
||||
RuntimeEvent::UserInterface(event) => {
|
||||
on_event(
|
||||
match on_event(
|
||||
&mut application,
|
||||
&mut user_interface,
|
||||
&mut app_data,
|
||||
&mut layout,
|
||||
event,
|
||||
);
|
||||
user_interface = new_user_interface(&mut application, &mut app_data);
|
||||
draw(&mut user_interface, terminal, &mut app_data, &mut layout)?;
|
||||
) {
|
||||
Status::Captured => {
|
||||
// Hmm... is this really needed? Or is it fine to redraw once then do the termination check?
|
||||
if application.is_terminated() {
|
||||
break;
|
||||
}
|
||||
|
||||
user_interface = new_user_interface(&mut application, &mut app_data);
|
||||
draw(&mut user_interface, terminal, &mut app_data, &mut layout)?;
|
||||
}
|
||||
Status::Ignored => {}
|
||||
}
|
||||
}
|
||||
RuntimeEvent::Custom(message) => {
|
||||
application.update(message);
|
||||
@ -77,26 +86,29 @@ where
|
||||
fn on_event<A>(
|
||||
application: &mut A, user_interface: &mut Element<A::Message>, app_data: &mut AppData,
|
||||
layout: &mut LayoutNode, event: Event,
|
||||
) where
|
||||
) -> Status
|
||||
where
|
||||
A: Application + 'static,
|
||||
{
|
||||
let mut messages = vec![];
|
||||
let mut state_ctx = StateContext::new(&mut app_data.state_map);
|
||||
let draw_ctx = DrawContext::root(&layout);
|
||||
|
||||
match user_interface.on_event(&mut state_ctx, &draw_ctx, event, &mut messages) {
|
||||
Status::Captured => {
|
||||
// TODO: What to do on capture?
|
||||
}
|
||||
Status::Ignored => {
|
||||
application.global_event_handler(event, &mut messages);
|
||||
}
|
||||
}
|
||||
let event_handled =
|
||||
match user_interface.on_event(&mut state_ctx, &draw_ctx, event, &mut messages) {
|
||||
Status::Captured => {
|
||||
// TODO: What to do on capture?
|
||||
Status::Captured
|
||||
}
|
||||
Status::Ignored => application.global_event_handler(event, &mut messages),
|
||||
};
|
||||
|
||||
for msg in messages {
|
||||
debug!("Message: {:?}", msg); // FIXME: Remove this debug line!
|
||||
application.update(msg);
|
||||
}
|
||||
|
||||
event_handled
|
||||
}
|
||||
|
||||
/// Creates a new [`Element`] representing the root of the user interface.
|
||||
|
Loading…
x
Reference in New Issue
Block a user