From e657fec2c04a1adabc7e28ff28c1292bb4c282e8 Mon Sep 17 00:00:00 2001 From: ClementTsang Date: Tue, 17 Aug 2021 23:44:17 -0400 Subject: [PATCH] refactor: Create new main widgets --- Cargo.lock | 13 ++++ Cargo.toml | 1 + src/app/event.rs | 19 ++++- src/app/widgets/base/scrollable.rs | 24 ++++-- src/app/widgets/base/text_table.rs | 48 ++++++------ src/app/widgets/base/time_graph.rs | 19 ++++- src/app/widgets/cpu.rs | 95 ++++++++++++++++++++++- src/app/widgets/disk.rs | 50 ++++++++++++- src/app/widgets/mem.rs | 40 ++++++++++ src/app/widgets/mod.rs | 37 ++++++--- src/app/widgets/net.rs | 116 +++++++++++++++++++++++++++++ src/app/widgets/temp.rs | 50 ++++++++++++- 12 files changed, 464 insertions(+), 48 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 63998907..e890a692 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -244,6 +244,7 @@ dependencies = [ "crossterm", "ctrlc", "dirs", + "enum_dispatch", "fern", "futures", "futures-timer", @@ -525,6 +526,18 @@ version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" +[[package]] +name = "enum_dispatch" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd53b3fde38a39a06b2e66dc282f3e86191e53bd04cc499929c15742beae3df8" +dependencies = [ + "once_cell", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "event-listener" version = "2.5.1" diff --git a/Cargo.toml b/Cargo.toml index eedb7c45..4554ab27 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,6 +43,7 @@ ctrlc = { version = "3.1.9", features = ["termination"] } clap = "2.33" cfg-if = "1.0" dirs = "3.0.2" +enum_dispatch = "0.3.7" futures = "0.3.14" futures-timer = "3.0.2" fxhash = "0.2.1" diff --git a/src/app/event.rs b/src/app/event.rs index 33806673..79efd9f5 100644 --- a/src/app/event.rs +++ b/src/app/event.rs @@ -7,14 +7,20 @@ pub enum EventResult { } enum MultiKeyState { + /// Currently not waiting on any next input. Idle, + + /// Waiting for the next input, with a given trigger [`Instant`]. Waiting { + /// When it was triggered. trigger_instant: Instant, + + /// What part of the pattern it is at. checked_index: usize, }, } -/// The possible outcomes of calling [`MultiKey::input`] on a [`MultiKey`]. +/// The possible outcomes of calling [`MultiKey::input`]. pub enum MultiKeyResult { /// Returned when a character was *accepted*, but has not completed the sequence required. Accepted, @@ -34,6 +40,7 @@ pub struct MultiKey { } impl MultiKey { + /// Creates a new [`MultiKey`] with a given pattern and timeout. pub fn register(pattern: Vec, timeout: Duration) -> Self { Self { state: MultiKeyState::Idle, @@ -42,10 +49,18 @@ impl MultiKey { } } - pub fn reset(&mut self) { + /// Resets to an idle state. + fn reset(&mut self) { self.state = MultiKeyState::Idle; } + /// Handles a char input and returns the current status of the [`MultiKey`] after, which is one of: + /// - Accepting the char and moving to the next state + /// - Completing the multi-key pattern + /// - Rejecting it + /// + /// Note that if a [`MultiKey`] only "times out" upon calling this - if it has timed out, it will first reset + /// before trying to check the char. pub fn input(&mut self, c: char) -> MultiKeyResult { match &mut self.state { MultiKeyState::Idle => { diff --git a/src/app/widgets/base/scrollable.rs b/src/app/widgets/base/scrollable.rs index b3e68a6c..675dc558 100644 --- a/src/app/widgets/base/scrollable.rs +++ b/src/app/widgets/base/scrollable.rs @@ -1,7 +1,7 @@ use std::time::Duration; use crossterm::event::{KeyEvent, KeyModifiers, MouseButton, MouseEvent}; -use tui::widgets::TableState; +use tui::{layout::Rect, widgets::TableState}; use crate::app::{ event::{EventResult, MultiKey, MultiKeyResult}, @@ -13,7 +13,8 @@ pub enum ScrollDirection { Down, } -/// A "scrollable" [`Widget`] component. Intended for use as part of another [`Widget]]. +/// A "scrollable" [`Widget`] component. Intended for use as part of another [`Widget`] - as such, it does +/// not have any bounds or the like. pub struct Scrollable { current_index: usize, previous_index: usize, @@ -22,6 +23,8 @@ pub struct Scrollable { tui_state: TableState, gg_manager: MultiKey, + + bounds: Rect, } impl Scrollable { @@ -34,6 +37,7 @@ impl Scrollable { num_items, tui_state: TableState::default(), gg_manager: MultiKey::register(vec!['g', 'g'], Duration::from_millis(400)), + bounds: Rect::default(), } } @@ -127,7 +131,7 @@ impl Scrollable { } impl Widget for Scrollable { - type UpdateState = usize; + type UpdateData = usize; fn handle_key_event(&mut self, event: KeyEvent) -> EventResult { use crossterm::event::KeyCode::{Char, Down, Up}; @@ -151,18 +155,18 @@ impl Widget for Scrollable { } } - fn handle_mouse_event(&mut self, event: MouseEvent, _x: u16, y: u16) -> EventResult { + fn handle_mouse_event(&mut self, event: MouseEvent) -> EventResult { match event.kind { crossterm::event::MouseEventKind::Down(MouseButton::Left) => { // This requires a bit of fancy calculation. The main trick is remembering that // we are using a *visual* index here - not what is the actual index! Luckily, we keep track of that // inside our linked copy of TableState! - // + // Note that y is assumed to be *relative*; // we assume that y starts at where the list starts (and there are no gaps or whatever). + let y = usize::from(event.row - self.bounds.top()); if let Some(selected) = self.tui_state.selected() { - let y = y as usize; if y > selected { let offset = y - selected; return self.move_down(offset); @@ -191,4 +195,12 @@ impl Widget for Scrollable { self.previous_index = new_num_items - 1; } } + + fn bounds(&self) -> Rect { + self.bounds + } + + fn set_bounds(&mut self, new_bounds: Rect) { + self.bounds = new_bounds; + } } diff --git a/src/app/widgets/base/text_table.rs b/src/app/widgets/base/text_table.rs index 885f25b4..3e113568 100644 --- a/src/app/widgets/base/text_table.rs +++ b/src/app/widgets/base/text_table.rs @@ -1,9 +1,7 @@ +use crossterm::event::{KeyEvent, MouseEvent}; use tui::layout::Rect; -use crate::{ - app::{event::EventResult, Scrollable, Widget}, - constants::TABLE_GAP_HEIGHT_LIMIT, -}; +use crate::app::{event::EventResult, Scrollable, Widget}; struct Column { name: &'static str, @@ -18,7 +16,7 @@ struct Column { impl Column {} /// The [`Widget::UpdateState`] of a [`TextTable`]. -pub struct TextTableUpdateState { +pub struct TextTableUpdateData { num_items: Option, columns: Option>, } @@ -35,7 +33,7 @@ pub struct TextTable { show_gap: bool, /// The bounding box of the [`TextTable`]. - bounds: Rect, // TODO: I kinda want to remove this... + bounds: Rect, // TODO: Consider moving bounds to something else??? /// Which index we're sorting by. sort_index: usize, @@ -91,26 +89,20 @@ impl TextTable { pub fn column_names(&self) -> Vec<&'static str> { self.columns.iter().map(|column| column.name).collect() } - - fn is_drawing_gap(&self) -> bool { - if !self.show_gap { - false - } else { - self.bounds.height >= TABLE_GAP_HEIGHT_LIMIT - } - } } impl Widget for TextTable { - type UpdateState = TextTableUpdateState; + type UpdateData = TextTableUpdateData; - fn handle_key_event(&mut self, event: crossterm::event::KeyEvent) -> EventResult { + fn handle_key_event(&mut self, event: KeyEvent) -> EventResult { self.scrollable.handle_key_event(event) } - fn handle_mouse_event( - &mut self, event: crossterm::event::MouseEvent, x: u16, y: u16, - ) -> EventResult { + fn handle_mouse_event(&mut self, event: MouseEvent) -> EventResult { + // Note these are representing RELATIVE coordinates! + let x = event.column - self.bounds.left(); + let y = event.row - self.bounds.top(); + if y == 0 { for (index, column) in self.columns.iter().enumerate() { let (start, end) = column.x_bounds; @@ -120,23 +112,29 @@ impl Widget for TextTable { } EventResult::Continue - } else if self.is_drawing_gap() { - self.scrollable.handle_mouse_event(event, x, y - 1) } else { - self.scrollable.handle_mouse_event(event, x, y - 2) + self.scrollable.handle_mouse_event(event) } } - fn update(&mut self, update_state: Self::UpdateState) { - if let Some(num_items) = update_state.num_items { + fn update(&mut self, update_data: Self::UpdateData) { + if let Some(num_items) = update_data.num_items { self.scrollable.update(num_items); } - if let Some(columns) = update_state.columns { + if let Some(columns) = update_data.columns { self.columns = columns; if self.columns.len() <= self.sort_index { self.sort_index = self.columns.len() - 1; } } } + + fn bounds(&self) -> Rect { + self.bounds + } + + fn set_bounds(&mut self, new_bounds: Rect) { + self.bounds = new_bounds; + } } diff --git a/src/app/widgets/base/time_graph.rs b/src/app/widgets/base/time_graph.rs index 981aca15..26adfcb4 100644 --- a/src/app/widgets/base/time_graph.rs +++ b/src/app/widgets/base/time_graph.rs @@ -1,14 +1,17 @@ use std::time::{Duration, Instant}; use crossterm::event::{KeyEvent, KeyModifiers, MouseEvent}; +use tui::layout::Rect; use crate::app::{event::EventResult, Widget}; +#[derive(Clone)] pub enum AutohideTimerState { Hidden, Running(Instant), } +#[derive(Clone)] pub enum AutohideTimer { Disabled, Enabled { @@ -58,6 +61,8 @@ pub struct TimeGraph { min_duration: u64, max_duration: u64, time_interval: u64, + + bounds: Rect, } impl TimeGraph { @@ -72,9 +77,11 @@ impl TimeGraph { min_duration, max_duration, time_interval, + bounds: Rect::default(), } } + /// Handles a char `c`. fn handle_char(&mut self, c: char) -> EventResult { match c { '-' => self.zoom_out(), @@ -132,7 +139,7 @@ impl TimeGraph { } impl Widget for TimeGraph { - type UpdateState = (); + type UpdateData = (); fn handle_key_event(&mut self, event: KeyEvent) -> EventResult { use crossterm::event::KeyCode::Char; @@ -147,11 +154,19 @@ impl Widget for TimeGraph { } } - fn handle_mouse_event(&mut self, event: MouseEvent, _x: u16, _y: u16) -> EventResult { + fn handle_mouse_event(&mut self, event: MouseEvent) -> EventResult { match event.kind { crossterm::event::MouseEventKind::ScrollDown => self.zoom_out(), crossterm::event::MouseEventKind::ScrollUp => self.zoom_in(), _ => EventResult::Continue, } } + + fn bounds(&self) -> Rect { + self.bounds + } + + fn set_bounds(&mut self, new_bounds: Rect) { + self.bounds = new_bounds; + } } diff --git a/src/app/widgets/cpu.rs b/src/app/widgets/cpu.rs index c155cf84..48c97dbc 100644 --- a/src/app/widgets/cpu.rs +++ b/src/app/widgets/cpu.rs @@ -1,6 +1,14 @@ use std::{collections::HashMap, time::Instant}; -use super::{AppScrollWidgetState, CanvasTableWidthState}; +use crossterm::event::{KeyEvent, MouseEvent}; +use tui::layout::Rect; + +use crate::app::event::EventResult; + +use super::{ + does_point_intersect_rect, text_table::TextTableUpdateData, AppScrollWidgetState, + CanvasTableWidthState, TextTable, TimeGraph, Widget, +}; pub struct CpuWidgetState { pub current_display_time: u64, @@ -45,3 +53,88 @@ impl CpuState { self.widget_states.get(&widget_id) } } + +enum CpuGraphSelection { + Graph, + Legend, + None, +} + +/// Whether the [`CpuGraph`]'s legend is placed on the left or right. +pub enum CpuGraphLegendPosition { + Left, + Right, +} + +pub struct CpuGraphUpdateData { + pub legend_data: Option, +} + +/// A widget designed to show CPU usage via a graph, along with a side legend implemented as a [`TextTable`]. +pub struct CpuGraph { + graph: TimeGraph, + legend: TextTable, + pub legend_position: CpuGraphLegendPosition, + + bounds: Rect, + selected: CpuGraphSelection, +} + +impl CpuGraph { + /// Creates a new [`CpuGraph`]. + pub fn new( + graph: TimeGraph, legend: TextTable, legend_position: CpuGraphLegendPosition, + ) -> Self { + Self { + graph, + legend, + legend_position, + bounds: Rect::default(), + selected: CpuGraphSelection::None, + } + } +} + +impl Widget for CpuGraph { + type UpdateData = CpuGraphUpdateData; + + fn handle_key_event(&mut self, event: KeyEvent) -> EventResult { + match self.selected { + CpuGraphSelection::Graph => self.graph.handle_key_event(event), + CpuGraphSelection::Legend => self.legend.handle_key_event(event), + CpuGraphSelection::None => EventResult::Continue, + } + } + + fn handle_mouse_event(&mut self, event: MouseEvent) -> EventResult { + // Check where we clicked (and switch the selected field if required) and fire the handler from there. + // Note we assume that the + + let global_x = event.column; + let global_y = event.row; + + if does_point_intersect_rect(global_x, global_y, self.graph.bounds()) { + self.selected = CpuGraphSelection::Graph; + self.graph.handle_mouse_event(event) + } else if does_point_intersect_rect(global_x, global_y, self.legend.bounds()) { + self.selected = CpuGraphSelection::Legend; + self.legend.handle_mouse_event(event) + } else { + EventResult::Continue + } + } + + fn update(&mut self, update_data: Self::UpdateData) { + if let Some(legend_data) = update_data.legend_data { + self.legend.update(legend_data); + } + } + + fn bounds(&self) -> Rect { + self.bounds + } + + fn set_bounds(&mut self, new_bounds: Rect) { + self.bounds = new_bounds; + } +} diff --git a/src/app/widgets/disk.rs b/src/app/widgets/disk.rs index 8528668c..9142dac0 100644 --- a/src/app/widgets/disk.rs +++ b/src/app/widgets/disk.rs @@ -1,6 +1,14 @@ use std::collections::HashMap; -use super::{AppScrollWidgetState, CanvasTableWidthState}; +use crossterm::event::{KeyEvent, MouseEvent}; +use tui::layout::Rect; + +use crate::app::event::EventResult; + +use super::{ + text_table::TextTableUpdateData, AppScrollWidgetState, CanvasTableWidthState, TextTable, + Widget, +}; pub struct DiskWidgetState { pub scroll_state: AppScrollWidgetState, @@ -33,3 +41,43 @@ impl DiskState { self.widget_states.get(&widget_id) } } + +/// A table displaying disk data. Essentially a wrapper around a [`TextTable`]. +pub struct DiskTable { + table: TextTable, + bounds: Rect, +} + +impl DiskTable { + /// Creates a new [`DiskTable`]. + pub fn new(table: TextTable) -> Self { + Self { + table, + bounds: Rect::default(), + } + } +} + +impl Widget for DiskTable { + type UpdateData = TextTableUpdateData; + + fn handle_key_event(&mut self, event: KeyEvent) -> EventResult { + self.table.handle_key_event(event) + } + + fn handle_mouse_event(&mut self, event: MouseEvent) -> EventResult { + self.table.handle_mouse_event(event) + } + + fn update(&mut self, update_data: Self::UpdateData) { + self.table.update(update_data); + } + + fn bounds(&self) -> Rect { + self.bounds + } + + fn set_bounds(&mut self, new_bounds: Rect) { + self.bounds = new_bounds; + } +} diff --git a/src/app/widgets/mem.rs b/src/app/widgets/mem.rs index 1e6ce5f7..9409b94c 100644 --- a/src/app/widgets/mem.rs +++ b/src/app/widgets/mem.rs @@ -1,5 +1,12 @@ use std::{collections::HashMap, time::Instant}; +use crossterm::event::{KeyEvent, MouseEvent}; +use tui::layout::Rect; + +use crate::app::event::EventResult; + +use super::{TimeGraph, Widget}; + pub struct MemWidgetState { pub current_display_time: u64, pub autohide_timer: Option, @@ -35,3 +42,36 @@ impl MemState { self.widget_states.get(&widget_id) } } + +/// A widget that deals with displaying memory usage on a [`TimeGraph`]. Basically just a wrapper +/// around [`TimeGraph`] as of now. +pub struct MemGraph { + graph: TimeGraph, +} + +impl MemGraph { + /// Creates a new [`MemGraph`]. + pub fn new(graph: TimeGraph) -> Self { + Self { graph } + } +} + +impl Widget for MemGraph { + type UpdateData = (); + + fn handle_key_event(&mut self, event: KeyEvent) -> EventResult { + self.graph.handle_key_event(event) + } + + fn handle_mouse_event(&mut self, event: MouseEvent) -> EventResult { + self.graph.handle_mouse_event(event) + } + + fn bounds(&self) -> Rect { + self.graph.bounds() + } + + fn set_bounds(&mut self, new_bounds: Rect) { + self.graph.set_bounds(new_bounds); + } +} diff --git a/src/app/widgets/mod.rs b/src/app/widgets/mod.rs index ae675df3..e7f63b45 100644 --- a/src/app/widgets/mod.rs +++ b/src/app/widgets/mod.rs @@ -1,6 +1,7 @@ use std::time::Instant; use crossterm::event::{KeyEvent, MouseEvent}; +use enum_dispatch::enum_dispatch; use tui::{layout::Rect, widgets::TableState}; use crate::{ @@ -32,9 +33,10 @@ pub use self::battery::*; pub mod temp; pub use temp::*; +#[enum_dispatch] #[allow(unused_variables)] pub trait Widget { - type UpdateState; + type UpdateData; /// Handles a [`KeyEvent`]. /// @@ -43,21 +45,36 @@ pub trait Widget { EventResult::Continue } - /// Handles a [`MouseEvent`]. `x` and `y` represent *relative* mouse coordinates to the [`Widget`] - those should - /// be used as opposed to the coordinates in the `event` unless you need absolute coordinates for some reason! + /// Handles a [`MouseEvent`]. /// /// Defaults to returning [`EventResult::Continue`], indicating nothing should be done. - fn handle_mouse_event(&mut self, event: MouseEvent, x: u16, y: u16) -> EventResult { + fn handle_mouse_event(&mut self, event: MouseEvent) -> EventResult { EventResult::Continue } - /// Updates a [`Widget`]. Defaults to doing nothing. - fn update(&mut self, update_state: Self::UpdateState) {} + /// Updates a [`Widget`] with new data from some state outside of its control. Defaults to doing nothing. + fn update(&mut self, update_data: Self::UpdateData) {} - /// Returns a [`Widget`]'s bounding box, if possible. Defaults to returning [`None`]. - fn bounding_box(&self) -> Option { - None - } + /// Returns a [`Widget`]'s bounding box. Note that these are defined in *global*, *absolute* + /// coordinates. + fn bounds(&self) -> Rect; + + /// Updates a [`Widget`]s bounding box. + fn set_bounds(&mut self, new_bounds: Rect); +} + +#[enum_dispatch(BottomWidget)] +enum BottomWidget { + MemGraph, + TempTable, + DiskTable, + CpuGraph, + NetGraph, + OldNetGraph, +} + +pub fn does_point_intersect_rect(x: u16, y: u16, rect: Rect) -> bool { + x >= rect.left() && x <= rect.right() && y >= rect.top() && y <= rect.bottom() } #[derive(Debug)] diff --git a/src/app/widgets/net.rs b/src/app/widgets/net.rs index 85c1281e..5865858c 100644 --- a/src/app/widgets/net.rs +++ b/src/app/widgets/net.rs @@ -1,5 +1,9 @@ use std::{collections::HashMap, time::Instant}; +use tui::layout::Rect; + +use super::{TimeGraph, Widget}; + pub struct NetWidgetState { pub current_display_time: u64, pub autohide_timer: Option, @@ -50,3 +54,115 @@ impl NetState { self.widget_states.get(&widget_id) } } + +struct NetGraphCache { + max_range: f64, + labels: Vec, + time_start: f64, +} + +enum NetGraphCacheState { + Uncached, + Cached(NetGraphCache), +} + +/// A widget denoting network usage via a graph. This version is self-contained within a single [`TimeGraph`]; +/// if you need the older one that splits into two sections, use [`OldNetGraph`], which is built on a [`NetGraph`]. +/// +/// As of now, this is essentially just a wrapper around a [`TimeGraph`]. +pub struct NetGraph { + graph: TimeGraph, + + // Cached details; probably want to move at some point... + draw_cache: NetGraphCacheState, +} + +impl NetGraph { + /// Creates a new [`NetGraph`]. + pub fn new(graph: TimeGraph) -> Self { + Self { + graph, + draw_cache: NetGraphCacheState::Uncached, + } + } + + pub fn set_cache(&mut self, max_range: f64, labels: Vec, time_start: f64) { + self.draw_cache = NetGraphCacheState::Cached(NetGraphCache { + max_range, + labels, + time_start, + }) + } + + pub fn is_cached(&self) -> bool { + match self.draw_cache { + NetGraphCacheState::Uncached => false, + NetGraphCacheState::Cached(_) => true, + } + } +} + +impl Widget for NetGraph { + type UpdateData = (); + + fn bounds(&self) -> Rect { + self.graph.bounds() + } + + fn set_bounds(&mut self, new_bounds: Rect) { + self.graph.set_bounds(new_bounds); + } + + fn handle_key_event( + &mut self, event: crossterm::event::KeyEvent, + ) -> crate::app::event::EventResult { + self.graph.handle_key_event(event) + } + + fn handle_mouse_event( + &mut self, event: crossterm::event::MouseEvent, + ) -> crate::app::event::EventResult { + self.graph.handle_mouse_event(event) + } +} + +/// A widget denoting network usage via a graph and a separate, single row table. This is built on [`NetGraph`], +/// and the main difference is that it also contains a bounding box for the graph + text. +pub struct OldNetGraph { + graph: NetGraph, + bounds: Rect, +} + +impl OldNetGraph { + /// Creates a new [`OldNetGraph`]. + pub fn new(graph: NetGraph) -> Self { + Self { + graph, + bounds: Rect::default(), + } + } +} + +impl Widget for OldNetGraph { + type UpdateData = (); + + fn bounds(&self) -> Rect { + self.bounds + } + + fn set_bounds(&mut self, new_bounds: Rect) { + self.bounds = new_bounds; + } + + fn handle_key_event( + &mut self, event: crossterm::event::KeyEvent, + ) -> crate::app::event::EventResult { + self.graph.handle_key_event(event) + } + + fn handle_mouse_event( + &mut self, event: crossterm::event::MouseEvent, + ) -> crate::app::event::EventResult { + self.graph.handle_mouse_event(event) + } +} diff --git a/src/app/widgets/temp.rs b/src/app/widgets/temp.rs index e8bd73f4..5259cfe8 100644 --- a/src/app/widgets/temp.rs +++ b/src/app/widgets/temp.rs @@ -1,6 +1,14 @@ use std::collections::HashMap; -use super::{AppScrollWidgetState, CanvasTableWidthState}; +use crossterm::event::{KeyEvent, MouseEvent}; +use tui::layout::Rect; + +use crate::app::event::EventResult; + +use super::{ + text_table::TextTableUpdateData, AppScrollWidgetState, CanvasTableWidthState, TextTable, + Widget, +}; pub struct TempWidgetState { pub scroll_state: AppScrollWidgetState, @@ -33,3 +41,43 @@ impl TempState { self.widget_states.get(&widget_id) } } + +/// A table displaying disk data. Essentially a wrapper around a [`TextTable`]. +pub struct TempTable { + table: TextTable, + bounds: Rect, +} + +impl TempTable { + /// Creates a new [`TempTable`]. + pub fn new(table: TextTable) -> Self { + Self { + table, + bounds: Rect::default(), + } + } +} + +impl Widget for TempTable { + type UpdateData = TextTableUpdateData; + + fn handle_key_event(&mut self, event: KeyEvent) -> EventResult { + self.table.handle_key_event(event) + } + + fn handle_mouse_event(&mut self, event: MouseEvent) -> EventResult { + self.table.handle_mouse_event(event) + } + + fn update(&mut self, update_data: Self::UpdateData) { + self.table.update(update_data); + } + + fn bounds(&self) -> Rect { + self.bounds + } + + fn set_bounds(&mut self, new_bounds: Rect) { + self.bounds = new_bounds; + } +}