diff --git a/src/app.rs b/src/app.rs index e9ab08695..e57d7ac0d 100644 --- a/src/app.rs +++ b/src/app.rs @@ -68,10 +68,10 @@ pub struct AppConfigFields { #[derive(TypedBuilder)] pub struct AppState { #[builder(default = false, setter(skip))] - awaiting_second_char: bool, + awaiting_second_char: bool, // TODO: Move out to input #[builder(default, setter(skip))] - second_char: Option, + second_char: Option, // TODO: Move out to input #[builder(default, setter(skip))] pub dd_err: Option, @@ -83,7 +83,7 @@ pub struct AppState { pub is_frozen: bool, #[builder(default = Instant::now(), setter(skip))] - last_key_press: Instant, + last_key_press: Instant, // TODO: Move out to input #[builder(default, setter(skip))] pub canvas_data: canvas::DisplayableData, @@ -129,11 +129,11 @@ pub struct AppState { } #[cfg(target_os = "windows")] -const MAX_SIGNAL: usize = 1; +const MAX_KILL_SIGNAL: usize = 1; #[cfg(target_os = "linux")] -const MAX_SIGNAL: usize = 64; +const MAX_KILL_SIGNAL: usize = 64; #[cfg(target_os = "macos")] -const MAX_SIGNAL: usize = 31; +const MAX_KILL_SIGNAL: usize = 31; impl AppState { pub fn reset(&mut self) { @@ -966,7 +966,7 @@ impl AppState { if self.delete_dialog_state.is_showing_dd { let mut new_signal = match self.delete_dialog_state.selected_signal { KillSignal::Cancel => 8, - KillSignal::Kill(signal) => min(signal + 8, MAX_SIGNAL), + KillSignal::Kill(signal) => min(signal + 8, MAX_KILL_SIGNAL), }; if new_signal > 31 && new_signal < 42 { new_signal += 2; @@ -2132,7 +2132,7 @@ impl AppState { .max_scroll_index .saturating_sub(1); } else if self.delete_dialog_state.is_showing_dd { - self.delete_dialog_state.selected_signal = KillSignal::Kill(MAX_SIGNAL); + self.delete_dialog_state.selected_signal = KillSignal::Kill(MAX_KILL_SIGNAL); } } diff --git a/src/app/layout_manager.rs b/src/app/layout_manager.rs index d4b9272d1..ef6837037 100644 --- a/src/app/layout_manager.rs +++ b/src/app/layout_manager.rs @@ -947,25 +947,7 @@ impl std::str::FromStr for BottomWidgetType { "empty" => Ok(BottomWidgetType::Empty), "battery" | "batt" => Ok(BottomWidgetType::Battery), _ => Err(BottomError::ConfigError(format!( - "\"{}\" is an invalid widget name. - -Supported widget names: -+--------------------------+ -| cpu | -+--------------------------+ -| mem, memory | -+--------------------------+ -| net, network | -+--------------------------+ -| proc, process, processes | -+--------------------------+ -| temp, temperature | -+--------------------------+ -| disk | -+--------------------------+ -| batt, battery | -+--------------------------+ - ", + "\"{}\" is an invalid widget name.", s ))), } diff --git a/src/app/widget_states/graph_state.rs b/src/app/widget_states/graph_state.rs deleted file mode 100644 index 6f3b55d48..000000000 --- a/src/app/widget_states/graph_state.rs +++ /dev/null @@ -1 +0,0 @@ -//! States for a graph widget. diff --git a/src/app/widget_states/mod.rs b/src/app/widget_states/mod.rs index dc6c02e74..2493da6e3 100644 --- a/src/app/widget_states/mod.rs +++ b/src/app/widget_states/mod.rs @@ -46,7 +46,7 @@ pub enum CursorDirection { } /// AppScrollWidgetState deals with fields for a scrollable app's current state. -#[derive(Default)] +#[derive(Debug, Default)] pub struct AppScrollWidgetState { pub current_scroll_position: usize, pub previous_scroll_position: usize, @@ -99,7 +99,7 @@ impl Default for AppHelpDialogState { } /// Meant for canvas operations involving table column widths. -#[derive(Default)] +#[derive(Debug, Default)] pub struct CanvasTableWidthState { pub desired_column_widths: Vec, pub calculated_column_widths: Vec, diff --git a/src/app/widget_states/table_state.rs b/src/app/widget_states/table_state.rs deleted file mode 100644 index 2e57e8f13..000000000 --- a/src/app/widget_states/table_state.rs +++ /dev/null @@ -1 +0,0 @@ -//! States for a table widget. diff --git a/src/bin/main.rs b/src/bin/main.rs index 48a757a03..4afea1739 100644 --- a/src/bin/main.rs +++ b/src/bin/main.rs @@ -4,7 +4,7 @@ #[macro_use] extern crate log; -use bottom::{canvas, constants::*, data_conversion::*, options::*, *}; +use bottom::{canvas, data_conversion::*, options::*, *}; use std::{ boxed::Box, @@ -61,6 +61,30 @@ fn main() -> Result<()> { get_color_scheme(&matches, &config)?, )?; + // Set up up tui and crossterm + let mut stdout_val = stdout(); + execute!(stdout_val, EnterAlternateScreen, EnableMouseCapture)?; + enable_raw_mode()?; + + let mut terminal = Terminal::new(CrosstermBackend::new(stdout_val))?; + terminal.clear()?; + terminal.hide_cursor()?; + + // Set panic hook + panic::set_hook(Box::new(|info| panic_hook(info))); + + // Set termination hook + let is_terminated = Arc::new(AtomicBool::new(false)); + { + let is_terminated = is_terminated.clone(); + ctrlc::set_handler(move || { + is_terminated.store(true, Ordering::SeqCst); + })?; + } + let mut first_pass = true; + + // ===== Start of actual thread creation and loop ===== + // Create termination mutex and cvar #[allow(clippy::mutex_atomic)] let thread_termination_lock = Arc::new(Mutex::new(false)); @@ -107,46 +131,34 @@ fn main() -> Result<()> { app.used_widgets.clone(), ); - // Set up up tui and crossterm - let mut stdout_val = stdout(); - execute!(stdout_val, EnterAlternateScreen, EnableMouseCapture)?; - enable_raw_mode()?; - - let mut terminal = Terminal::new(CrosstermBackend::new(stdout_val))?; - terminal.clear()?; - terminal.hide_cursor()?; - - // Set panic hook - panic::set_hook(Box::new(|info| panic_hook(info))); - - // Set termination hook - let is_terminated = Arc::new(AtomicBool::new(false)); - let ist_clone = is_terminated.clone(); - ctrlc::set_handler(move || { - ist_clone.store(true, Ordering::SeqCst); - })?; - let mut first_run = true; - while !is_terminated.load(Ordering::SeqCst) { - if let Ok(recv) = receiver.recv_timeout(Duration::from_millis(TICK_RATE_IN_MILLISECONDS)) { + if let Ok(recv) = receiver.recv() { match recv { BottomEvent::KeyInput(event) => { if handle_key_event_or_break(event, &mut app, &collection_thread_ctrl_sender) { break; } handle_force_redraws(&mut app); + + if try_drawing(&mut terminal, &mut app, &mut painter).is_err() { + break; + } } BottomEvent::MouseInput(event) => { handle_mouse_event(event, &mut app); handle_force_redraws(&mut app); + + if try_drawing(&mut terminal, &mut app, &mut painter).is_err() { + break; + } } BottomEvent::Update(data) => { app.data_collection.eat_data(data); // This thing is required as otherwise, some widgets can't draw correctly w/o // some data (or they need to be re-drawn). - if first_run { - first_run = false; + if first_pass { + first_pass = false; app.is_force_redraw = true; } @@ -221,25 +233,29 @@ fn main() -> Result<()> { convert_battery_harvest(&app.data_collection); } } + + if try_drawing(&mut terminal, &mut app, &mut painter).is_err() { + break; + } } BottomEvent::Clean => { app.data_collection .clean_data(constants::STALE_MAX_MILLISECONDS); } + BottomEvent::RequestRedraw => { + if try_drawing(&mut terminal, &mut app, &mut painter).is_err() { + break; + } + } } } - - // TODO: [OPT] Should not draw if no change (ie: scroll max) - try_drawing(&mut terminal, &mut app, &mut painter)?; } - // I think doing it in this order is safe... - *thread_termination_lock.lock().unwrap() = true; - thread_termination_cvar.notify_all(); - cleanup_terminal(&mut terminal)?; + // ===== End of actual thread creation and loop ===== + Ok(()) } diff --git a/src/canvas/dialogs/dd_dialog.rs b/src/canvas/dialogs/dd_dialog.rs index 359653c91..8a187e8fd 100644 --- a/src/canvas/dialogs/dd_dialog.rs +++ b/src/canvas/dialogs/dd_dialog.rs @@ -24,7 +24,8 @@ pub trait KillDialog { ); fn draw_dd_dialog( - &self, f: &mut Frame<'_, B>, dd_text: Option>, app_state: &mut AppState, draw_loc: Rect, + &self, f: &mut Frame<'_, B>, dd_text: Option>, app_state: &mut AppState, + draw_loc: Rect, ) -> bool; } @@ -318,7 +319,8 @@ impl KillDialog for Painter { } fn draw_dd_dialog( - &self, f: &mut Frame<'_, B>, dd_text: Option>, app_state: &mut AppState, draw_loc: Rect, + &self, f: &mut Frame<'_, B>, dd_text: Option>, app_state: &mut AppState, + draw_loc: Rect, ) -> bool { if let Some(dd_text) = dd_text { let dd_title = if app_state.dd_err.is_some() { diff --git a/src/canvas/widgets/process_table.rs b/src/canvas/element/process_table.rs similarity index 100% rename from src/canvas/widgets/process_table.rs rename to src/canvas/element/process_table.rs diff --git a/src/canvas/widgets/basic_table_arrows.rs b/src/canvas/elements/basic_table_arrows.rs similarity index 100% rename from src/canvas/widgets/basic_table_arrows.rs rename to src/canvas/elements/basic_table_arrows.rs diff --git a/src/canvas/widgets/battery_display.rs b/src/canvas/elements/battery_display.rs similarity index 100% rename from src/canvas/widgets/battery_display.rs rename to src/canvas/elements/battery_display.rs diff --git a/src/canvas/widgets/cpu_basic.rs b/src/canvas/elements/cpu_basic.rs similarity index 100% rename from src/canvas/widgets/cpu_basic.rs rename to src/canvas/elements/cpu_basic.rs diff --git a/src/canvas/widgets/cpu_graph.rs b/src/canvas/elements/cpu_graph.rs similarity index 100% rename from src/canvas/widgets/cpu_graph.rs rename to src/canvas/elements/cpu_graph.rs diff --git a/src/canvas/widgets/disk_table.rs b/src/canvas/elements/disk_table.rs similarity index 98% rename from src/canvas/widgets/disk_table.rs rename to src/canvas/elements/disk_table.rs index c48683208..6a79c5b69 100644 --- a/src/canvas/widgets/disk_table.rs +++ b/src/canvas/elements/disk_table.rs @@ -29,15 +29,15 @@ static DISK_HEADERS_LENS: Lazy> = Lazy::new(|| { pub trait DiskTableWidget { fn draw_disk_table( - &self, f: &mut Frame<'_, B>, app_state: &mut app::AppState, draw_loc: Rect, draw_border: bool, - widget_id: u64, + &self, f: &mut Frame<'_, B>, app_state: &mut app::AppState, draw_loc: Rect, + draw_border: bool, widget_id: u64, ); } impl DiskTableWidget for Painter { fn draw_disk_table( - &self, f: &mut Frame<'_, B>, app_state: &mut app::AppState, draw_loc: Rect, draw_border: bool, - widget_id: u64, + &self, f: &mut Frame<'_, B>, app_state: &mut app::AppState, draw_loc: Rect, + draw_border: bool, widget_id: u64, ) { let recalculate_column_widths = app_state.should_get_widget_bounds(); if let Some(disk_widget_state) = app_state.disk_state.widget_states.get_mut(&widget_id) { diff --git a/src/canvas/elements/element.rs b/src/canvas/elements/element.rs new file mode 100644 index 000000000..d00ef1552 --- /dev/null +++ b/src/canvas/elements/element.rs @@ -0,0 +1,48 @@ +#![allow(dead_code)] + +use tui::{backend::Backend, layout::Rect, Frame}; + +use crate::{app::AppState, canvas::canvas_colours::CanvasColours}; + +/// A single point. +#[derive(Copy, Clone)] +pub struct Point { + pub x: u16, + pub y: u16, +} + +/// The top-left and bottom-right corners of a [`Element`]. +#[derive(Copy, Clone)] +pub enum ElementBounds { + Unset, + Points { + top_left_corner: Point, + bottom_right_corner: Point, + }, +} + +/// A basic [`Element`] trait, all drawn components must implement this. +pub trait Element { + /// The type of data that is expected for the [`Element`]. + + /// The main drawing function. + fn draw( + &mut self, f: &mut Frame<'_, B>, app_state: &AppState, draw_loc: Rect, + style: &CanvasColours, + ) -> anyhow::Result<()>; + + /// Recalculates the click bounds. + fn recalculate_click_bounds(&mut self); + + /// A function to determine the main widget click bounds. + fn click_bounds(&self) -> ElementBounds; + + /// Returns whether am [`Element`] is selected. + fn is_selected(&self) -> bool; + + /// Marks an [`Element`] as selected. + fn select(&mut self); + + /// Marks an [`Element`] as unselected. + fn unselect(&mut self); +} diff --git a/src/canvas/widgets/mem_basic.rs b/src/canvas/elements/mem_basic.rs similarity index 100% rename from src/canvas/widgets/mem_basic.rs rename to src/canvas/elements/mem_basic.rs diff --git a/src/canvas/widgets/mem_graph.rs b/src/canvas/elements/mem_graph.rs similarity index 100% rename from src/canvas/widgets/mem_graph.rs rename to src/canvas/elements/mem_graph.rs diff --git a/src/canvas/widgets.rs b/src/canvas/elements/mod.rs similarity index 73% rename from src/canvas/widgets.rs rename to src/canvas/elements/mod.rs index a76b45912..8394737d1 100644 --- a/src/canvas/widgets.rs +++ b/src/canvas/elements/mod.rs @@ -21,3 +21,15 @@ pub use network_basic::NetworkBasicWidget; pub use network_graph::NetworkGraphWidget; pub use process_table::ProcessTableWidget; pub use temp_table::TempTableWidget; + +pub mod element; +pub use element::Element; + +pub mod scrollable_table; +pub use scrollable_table::ScrollableTable; + +pub mod scroll_sort_table; +pub use scroll_sort_table::ScrollSortTable; + +pub mod time_graph; +pub use time_graph::TimeGraph; diff --git a/src/canvas/widgets/network_basic.rs b/src/canvas/elements/network_basic.rs similarity index 100% rename from src/canvas/widgets/network_basic.rs rename to src/canvas/elements/network_basic.rs diff --git a/src/canvas/widgets/network_graph.rs b/src/canvas/elements/network_graph.rs similarity index 100% rename from src/canvas/widgets/network_graph.rs rename to src/canvas/elements/network_graph.rs diff --git a/src/canvas/elements/process_table.rs b/src/canvas/elements/process_table.rs new file mode 100644 index 000000000..190a216e0 --- /dev/null +++ b/src/canvas/elements/process_table.rs @@ -0,0 +1,911 @@ +use crate::{ + app::AppState, + canvas::{ + drawing_utils::{get_column_widths, get_search_start_position, get_start_position}, + Painter, + }, + constants::*, +}; + +use tui::{ + backend::Backend, + layout::{Alignment, Constraint, Direction, Layout, Rect}, + terminal::Frame, + text::{Span, Spans, Text}, + widgets::{Block, Borders, Paragraph, Row, Table}, +}; + +use unicode_segmentation::{GraphemeIndices, UnicodeSegmentation}; +use unicode_width::UnicodeWidthStr; + +use once_cell::sync::Lazy; + +static PROCESS_HEADERS_HARD_WIDTH_NO_GROUP: Lazy>> = Lazy::new(|| { + vec![ + Some(7), + None, + Some(8), + Some(8), + Some(8), + Some(8), + Some(7), + Some(8), + #[cfg(target_family = "unix")] + None, + None, + ] +}); +static PROCESS_HEADERS_HARD_WIDTH_GROUPED: Lazy>> = Lazy::new(|| { + vec![ + Some(7), + None, + Some(8), + Some(8), + Some(8), + Some(8), + Some(7), + Some(8), + ] +}); + +static PROCESS_HEADERS_SOFT_WIDTH_MAX_GROUPED_COMMAND: Lazy>> = + Lazy::new(|| vec![None, Some(0.7), None, None, None, None, None, None]); +static PROCESS_HEADERS_SOFT_WIDTH_MAX_GROUPED_ELSE: Lazy>> = + Lazy::new(|| vec![None, Some(0.3), None, None, None, None, None, None]); + +static PROCESS_HEADERS_SOFT_WIDTH_MAX_NO_GROUP_COMMAND: Lazy>> = Lazy::new(|| { + vec![ + None, + Some(0.7), + None, + None, + None, + None, + None, + None, + #[cfg(target_family = "unix")] + Some(0.05), + Some(0.2), + ] +}); +static PROCESS_HEADERS_SOFT_WIDTH_MAX_NO_GROUP_TREE: Lazy>> = Lazy::new(|| { + vec![ + None, + Some(0.5), + None, + None, + None, + None, + None, + None, + #[cfg(target_family = "unix")] + Some(0.05), + Some(0.2), + ] +}); +static PROCESS_HEADERS_SOFT_WIDTH_MAX_NO_GROUP_ELSE: Lazy>> = Lazy::new(|| { + vec![ + None, + Some(0.3), + None, + None, + None, + None, + None, + None, + #[cfg(target_family = "unix")] + Some(0.05), + Some(0.2), + ] +}); + +pub trait ProcessTableWidget { + /// Draws and handles all process-related drawing. Use this. + /// - `widget_id` here represents the widget ID of the process widget itself! + fn draw_process_features( + &self, f: &mut Frame<'_, B>, app_state: &mut AppState, draw_loc: Rect, draw_border: bool, + widget_id: u64, + ); + + /// Draws the process sort box. + /// - `widget_id` represents the widget ID of the process widget itself. + /// + /// This should not be directly called. + fn draw_processes_table( + &self, f: &mut Frame<'_, B>, app_state: &mut AppState, draw_loc: Rect, draw_border: bool, + widget_id: u64, + ); + + /// Draws the process search field. + /// - `widget_id` represents the widget ID of the search box itself --- NOT the process widget + /// state that is stored. + /// + /// This should not be directly called. + fn draw_search_field( + &self, f: &mut Frame<'_, B>, app_state: &mut AppState, draw_loc: Rect, draw_border: bool, + widget_id: u64, + ); + + /// Draws the process sort box. + /// - `widget_id` represents the widget ID of the sort box itself --- NOT the process widget + /// state that is stored. + /// + /// This should not be directly called. + fn draw_process_sort( + &self, f: &mut Frame<'_, B>, app_state: &mut AppState, draw_loc: Rect, draw_border: bool, + widget_id: u64, + ); +} + +impl ProcessTableWidget for Painter { + fn draw_process_features( + &self, f: &mut Frame<'_, B>, app_state: &mut AppState, draw_loc: Rect, draw_border: bool, + widget_id: u64, + ) { + if let Some(process_widget_state) = app_state.proc_state.widget_states.get(&widget_id) { + let search_height = if draw_border { 5 } else { 3 }; + let is_sort_open = process_widget_state.is_sort_open; + let header_len = process_widget_state.columns.longest_header_len; + + let mut proc_draw_loc = draw_loc; + if process_widget_state.is_search_enabled() { + let processes_chunk = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Min(0), Constraint::Length(search_height)]) + .split(draw_loc); + proc_draw_loc = processes_chunk[0]; + + self.draw_search_field( + f, + app_state, + processes_chunk[1], + draw_border, + widget_id + 1, + ); + } + + if is_sort_open { + let processes_chunk = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Length(header_len + 4), Constraint::Min(0)]) + .split(proc_draw_loc); + proc_draw_loc = processes_chunk[1]; + + self.draw_process_sort( + f, + app_state, + processes_chunk[0], + draw_border, + widget_id + 2, + ); + } + + self.draw_processes_table(f, app_state, proc_draw_loc, draw_border, widget_id); + } + } + + fn draw_processes_table( + &self, f: &mut Frame<'_, B>, app_state: &mut AppState, draw_loc: Rect, draw_border: bool, + widget_id: u64, + ) { + let should_get_widget_bounds = app_state.should_get_widget_bounds(); + if let Some(proc_widget_state) = app_state.proc_state.widget_states.get_mut(&widget_id) { + let recalculate_column_widths = + should_get_widget_bounds || proc_widget_state.requires_redraw; + if proc_widget_state.requires_redraw { + proc_widget_state.requires_redraw = false; + } + + let is_on_widget = widget_id == app_state.current_widget.widget_id; + let margined_draw_loc = Layout::default() + .constraints([Constraint::Percentage(100)]) + .horizontal_margin(if is_on_widget || draw_border { 0 } else { 1 }) + .direction(Direction::Horizontal) + .split(draw_loc)[0]; + + let (border_style, highlight_style) = if is_on_widget { + ( + self.colours.highlighted_border_style, + self.colours.currently_selected_text_style, + ) + } else { + (self.colours.border_style, self.colours.text_style) + }; + + let title_base = if app_state.app_config_fields.show_table_scroll_position { + if let Some(finalized_process_data) = app_state + .canvas_data + .finalized_process_data_map + .get(&widget_id) + { + let title = format!( + " Processes ({} of {}) ", + proc_widget_state + .scroll_state + .current_scroll_position + .saturating_add(1), + finalized_process_data.len() + ); + + if title.len() <= draw_loc.width as usize { + title + } else { + " Processes ".to_string() + } + } else { + " Processes ".to_string() + } + } else { + " Processes ".to_string() + }; + + let title = if app_state.is_expanded + && !proc_widget_state + .process_search_state + .search_state + .is_enabled + && !proc_widget_state.is_sort_open + { + const ESCAPE_ENDING: &str = "── Esc to go back "; + + let (chosen_title_base, expanded_title_base) = { + let temp_title_base = format!("{}{}", title_base, ESCAPE_ENDING); + + if temp_title_base.len() > draw_loc.width as usize { + ( + " Processes ".to_string(), + format!("{}{}", " Processes ".to_string(), ESCAPE_ENDING), + ) + } else { + (title_base, temp_title_base) + } + }; + + Spans::from(vec![ + Span::styled(chosen_title_base, self.colours.widget_title_style), + Span::styled( + format!( + "─{}─ Esc to go back ", + "─".repeat( + usize::from(draw_loc.width).saturating_sub( + UnicodeSegmentation::graphemes( + expanded_title_base.as_str(), + true + ) + .count() + + 2 + ) + ) + ), + border_style, + ), + ]) + } else { + Spans::from(Span::styled(title_base, self.colours.widget_title_style)) + }; + + let process_block = if draw_border { + Block::default() + .title(title) + .borders(Borders::ALL) + .border_style(border_style) + } else if is_on_widget { + Block::default() + .borders(*SIDE_BORDERS) + .border_style(self.colours.highlighted_border_style) + } else { + Block::default().borders(Borders::NONE) + }; + + if let Some(process_data) = &app_state + .canvas_data + .stringified_process_data_map + .get(&widget_id) + { + let table_gap = if draw_loc.height < TABLE_GAP_HEIGHT_LIMIT { + 0 + } else { + app_state.app_config_fields.table_gap + }; + let position = get_start_position( + usize::from( + (draw_loc.height + (1 - table_gap)) + .saturating_sub(self.table_height_offset), + ), + &proc_widget_state.scroll_state.scroll_direction, + &mut proc_widget_state.scroll_state.previous_scroll_position, + proc_widget_state.scroll_state.current_scroll_position, + app_state.is_force_redraw, + ); + + // Sanity check + let start_position = if position >= process_data.len() { + process_data.len().saturating_sub(1) + } else { + position + }; + + let sliced_vec = &process_data[start_position..]; + let processed_sliced_vec = sliced_vec.iter().map(|(data, disabled)| { + ( + data.iter() + .map(|(entry, _alternative)| entry) + .collect::>(), + disabled, + ) + }); + + let proc_table_state = &mut proc_widget_state.scroll_state.table_state; + proc_table_state.select(Some( + proc_widget_state + .scroll_state + .current_scroll_position + .saturating_sub(start_position), + )); + + // Draw! + let process_headers = proc_widget_state.columns.get_column_headers( + &proc_widget_state.process_sorting_type, + proc_widget_state.is_process_sort_descending, + ); + + // Calculate widths + // FIXME: See if we can move this into the recalculate block? I want to move column widths into the column widths + let hard_widths = if proc_widget_state.is_grouped { + &*PROCESS_HEADERS_HARD_WIDTH_GROUPED + } else { + &*PROCESS_HEADERS_HARD_WIDTH_NO_GROUP + }; + + if recalculate_column_widths { + let mut column_widths = process_headers + .iter() + .map(|entry| UnicodeWidthStr::width(entry.as_str()) as u16) + .collect::>(); + + let soft_widths_min = column_widths + .iter() + .map(|width| Some(*width)) + .collect::>(); + + proc_widget_state.table_width_state.desired_column_widths = { + for (row, _disabled) in processed_sliced_vec.clone() { + for (col, entry) in row.iter().enumerate() { + if let Some(col_width) = column_widths.get_mut(col) { + let grapheme_len = UnicodeWidthStr::width(entry.as_str()); + if grapheme_len as u16 > *col_width { + *col_width = grapheme_len as u16; + } + } + } + } + column_widths + }; + + proc_widget_state.table_width_state.desired_column_widths = proc_widget_state + .table_width_state + .desired_column_widths + .iter() + .zip(hard_widths) + .map(|(current, hard)| { + if let Some(hard) = hard { + if *hard > *current { + *hard + } else { + *current + } + } else { + *current + } + }) + .collect::>(); + + let soft_widths_max = if proc_widget_state.is_grouped { + // Note grouped trees are not a thing. + + if proc_widget_state.is_using_command { + &*PROCESS_HEADERS_SOFT_WIDTH_MAX_GROUPED_COMMAND + } else { + &*PROCESS_HEADERS_SOFT_WIDTH_MAX_GROUPED_ELSE + } + } else if proc_widget_state.is_using_command { + &*PROCESS_HEADERS_SOFT_WIDTH_MAX_NO_GROUP_COMMAND + } else if proc_widget_state.is_tree_mode { + &*PROCESS_HEADERS_SOFT_WIDTH_MAX_NO_GROUP_TREE + } else { + &*PROCESS_HEADERS_SOFT_WIDTH_MAX_NO_GROUP_ELSE + }; + + proc_widget_state.table_width_state.calculated_column_widths = + get_column_widths( + draw_loc.width, + &hard_widths, + &soft_widths_min, + soft_widths_max, + &(proc_widget_state + .table_width_state + .desired_column_widths + .iter() + .map(|width| Some(*width)) + .collect::>()), + true, + ); + + // debug!( + // "DCW: {:?}", + // proc_widget_state.table_width_state.desired_column_widths + // ); + // debug!( + // "CCW: {:?}", + // proc_widget_state.table_width_state.calculated_column_widths + // ); + } + + let dcw = &proc_widget_state.table_width_state.desired_column_widths; + let ccw = &proc_widget_state.table_width_state.calculated_column_widths; + + let process_rows = sliced_vec.iter().map(|(data, disabled)| { + let truncated_data = data.iter().zip(hard_widths).enumerate().map( + |(itx, ((entry, alternative), width))| { + if let (Some(desired_col_width), Some(calculated_col_width)) = + (dcw.get(itx), ccw.get(itx)) + { + if width.is_none() { + if *desired_col_width > *calculated_col_width + && *calculated_col_width > 0 + { + let graphemes = + UnicodeSegmentation::graphemes(entry.as_str(), true) + .collect::>(); + + if let Some(alternative) = alternative { + Text::raw(alternative) + } else if graphemes.len() > *calculated_col_width as usize + && *calculated_col_width > 1 + { + // Truncate with ellipsis + let first_n = graphemes + [..(*calculated_col_width as usize - 1)] + .concat(); + Text::raw(format!("{}…", first_n)) + } else { + Text::raw(entry) + } + } else { + Text::raw(entry) + } + } else { + Text::raw(entry) + } + } else { + Text::raw(entry) + } + }, + ); + + if *disabled { + Row::new(truncated_data).style(self.colours.disabled_text_style) + } else { + Row::new(truncated_data) + } + }); + + f.render_stateful_widget( + Table::new(process_rows) + .header( + Row::new(process_headers) + .style(self.colours.table_header_style) + .bottom_margin(table_gap), + ) + .block(process_block) + .highlight_style(highlight_style) + .style(self.colours.text_style) + .widths( + &(proc_widget_state + .table_width_state + .calculated_column_widths + .iter() + .map(|calculated_width| { + Constraint::Length(*calculated_width as u16) + }) + .collect::>()), + ), + margined_draw_loc, + proc_table_state, + ); + } else { + f.render_widget(process_block, margined_draw_loc); + } + + // Check if we need to update columnar bounds... + if recalculate_column_widths + || proc_widget_state.columns.column_header_x_locs.is_none() + || proc_widget_state.columns.column_header_y_loc.is_none() + { + // y location is just the y location of the widget + border size (1 normally, 0 in basic) + proc_widget_state.columns.column_header_y_loc = + Some(draw_loc.y + if draw_border { 1 } else { 0 }); + + // x location is determined using the x locations of the widget; just offset from the left bound + // as appropriate, and use the right bound as limiter. + + let mut current_x_left = draw_loc.x + 1; + let max_x_right = draw_loc.x + draw_loc.width - 1; + + let mut x_locs = vec![]; + + for width in proc_widget_state + .table_width_state + .calculated_column_widths + .iter() + { + let right_bound = current_x_left + width; + + if right_bound < max_x_right { + x_locs.push((current_x_left, right_bound)); + current_x_left = right_bound + 1; + } else { + x_locs.push((current_x_left, max_x_right)); + break; + } + } + + proc_widget_state.columns.column_header_x_locs = Some(x_locs); + } + + if app_state.should_get_widget_bounds() { + // Update draw loc in widget map + if let Some(widget) = app_state.widget_map.get_mut(&widget_id) { + widget.top_left_corner = Some((margined_draw_loc.x, margined_draw_loc.y)); + widget.bottom_right_corner = Some(( + margined_draw_loc.x + margined_draw_loc.width, + margined_draw_loc.y + margined_draw_loc.height, + )); + } + } + } + } + + fn draw_search_field( + &self, f: &mut Frame<'_, B>, app_state: &mut AppState, draw_loc: Rect, draw_border: bool, + widget_id: u64, + ) { + fn build_query<'a>( + is_on_widget: bool, grapheme_indices: GraphemeIndices<'a>, start_position: usize, + cursor_position: usize, query: &str, currently_selected_text_style: tui::style::Style, + text_style: tui::style::Style, + ) -> Vec> { + let mut current_grapheme_posn = 0; + + if is_on_widget { + let mut res = grapheme_indices + .filter_map(|grapheme| { + current_grapheme_posn += UnicodeWidthStr::width(grapheme.1); + + if current_grapheme_posn <= start_position { + None + } else { + let styled = if grapheme.0 == cursor_position { + Span::styled(grapheme.1, currently_selected_text_style) + } else { + Span::styled(grapheme.1, text_style) + }; + Some(styled) + } + }) + .collect::>(); + + if cursor_position == query.len() { + res.push(Span::styled(" ", currently_selected_text_style)) + } + + res + } else { + // This is easier - we just need to get a range of graphemes, rather than + // dealing with possibly inserting a cursor (as none is shown!) + + vec![Span::styled(query.to_string(), text_style)] + } + } + + // TODO: Make the cursor scroll back if there's space! + if let Some(proc_widget_state) = + app_state.proc_state.widget_states.get_mut(&(widget_id - 1)) + { + let is_on_widget = widget_id == app_state.current_widget.widget_id; + let num_columns = usize::from(draw_loc.width); + let search_title = "> "; + + let num_chars_for_text = search_title.len(); + let cursor_position = proc_widget_state.get_search_cursor_position(); + let current_cursor_position = proc_widget_state.get_char_cursor_position(); + + let start_position: usize = get_search_start_position( + num_columns - num_chars_for_text - 5, + &proc_widget_state + .process_search_state + .search_state + .cursor_direction, + &mut proc_widget_state + .process_search_state + .search_state + .cursor_bar, + current_cursor_position, + app_state.is_force_redraw, + ); + + let query = proc_widget_state.get_current_search_query().as_str(); + let grapheme_indices = UnicodeSegmentation::grapheme_indices(query, true); + + // TODO: [CURSOR] blank cursor if not selected + // TODO: [CURSOR] blinking cursor? + let query_with_cursor = build_query( + is_on_widget, + grapheme_indices, + start_position, + cursor_position, + query, + self.colours.currently_selected_text_style, + self.colours.text_style, + ); + + let mut search_text = vec![Spans::from({ + let mut search_vec = vec![Span::styled( + search_title, + if is_on_widget { + self.colours.table_header_style + } else { + self.colours.text_style + }, + )]; + search_vec.extend(query_with_cursor); + + search_vec + })]; + + // Text options shamelessly stolen from VS Code. + let case_style = if !proc_widget_state.process_search_state.is_ignoring_case { + self.colours.currently_selected_text_style + } else { + self.colours.text_style + }; + + let whole_word_style = if proc_widget_state + .process_search_state + .is_searching_whole_word + { + self.colours.currently_selected_text_style + } else { + self.colours.text_style + }; + + let regex_style = if proc_widget_state + .process_search_state + .is_searching_with_regex + { + self.colours.currently_selected_text_style + } else { + self.colours.text_style + }; + + // FIXME: [MOUSE] Mouse support for these in search + // FIXME: [MOVEMENT] Movement support for these in search + let option_text = Spans::from(vec![ + Span::styled( + format!("Case({})", if self.is_mac_os { "F1" } else { "Alt+C" }), + case_style, + ), + Span::raw(" "), + Span::styled( + format!("Whole({})", if self.is_mac_os { "F2" } else { "Alt+W" }), + whole_word_style, + ), + Span::raw(" "), + Span::styled( + format!("Regex({})", if self.is_mac_os { "F3" } else { "Alt+R" }), + regex_style, + ), + ]); + + search_text.push(Spans::from(Span::styled( + if let Some(err) = &proc_widget_state + .process_search_state + .search_state + .error_message + { + err.as_str() + } else { + "" + }, + self.colours.invalid_query_style, + ))); + search_text.push(option_text); + + let current_border_style = if proc_widget_state + .process_search_state + .search_state + .is_invalid_search + { + self.colours.invalid_query_style + } else if is_on_widget { + self.colours.highlighted_border_style + } else { + self.colours.border_style + }; + + let title = Span::styled( + if draw_border { + const TITLE_BASE: &str = " Esc to close "; + let repeat_num = + usize::from(draw_loc.width).saturating_sub(TITLE_BASE.chars().count() + 2); + format!("{} Esc to close ", "─".repeat(repeat_num)) + } else { + String::new() + }, + current_border_style, + ); + + let process_search_block = if draw_border { + Block::default() + .title(title) + .borders(Borders::ALL) + .border_style(current_border_style) + } else if is_on_widget { + Block::default() + .borders(*SIDE_BORDERS) + .border_style(current_border_style) + } else { + Block::default().borders(Borders::NONE) + }; + + let margined_draw_loc = Layout::default() + .constraints([Constraint::Percentage(100)]) + .horizontal_margin(if is_on_widget || draw_border { 0 } else { 1 }) + .direction(Direction::Horizontal) + .split(draw_loc)[0]; + + f.render_widget( + Paragraph::new(search_text) + .block(process_search_block) + .style(self.colours.text_style) + .alignment(Alignment::Left), + margined_draw_loc, + ); + + if app_state.should_get_widget_bounds() { + // Update draw loc in widget map + if let Some(widget) = app_state.widget_map.get_mut(&widget_id) { + widget.top_left_corner = Some((margined_draw_loc.x, margined_draw_loc.y)); + widget.bottom_right_corner = Some(( + margined_draw_loc.x + margined_draw_loc.width, + margined_draw_loc.y + margined_draw_loc.height, + )); + } + } + } + } + + fn draw_process_sort( + &self, f: &mut Frame<'_, B>, app_state: &mut AppState, draw_loc: Rect, draw_border: bool, + widget_id: u64, + ) { + let is_on_widget = widget_id == app_state.current_widget.widget_id; + + if let Some(proc_widget_state) = + app_state.proc_state.widget_states.get_mut(&(widget_id - 2)) + { + let current_scroll_position = proc_widget_state.columns.current_scroll_position; + let sort_string = proc_widget_state + .columns + .ordered_columns + .iter() + .filter(|column_type| { + proc_widget_state + .columns + .column_mapping + .get(&column_type) + .unwrap() + .enabled + }) + .map(|column_type| column_type.to_string()) + .collect::>(); + + let table_gap = if draw_loc.height < TABLE_GAP_HEIGHT_LIMIT { + 0 + } else { + app_state.app_config_fields.table_gap + }; + let position = get_start_position( + usize::from( + (draw_loc.height + (1 - table_gap)).saturating_sub(self.table_height_offset), + ), + &proc_widget_state.columns.scroll_direction, + &mut proc_widget_state.columns.previous_scroll_position, + current_scroll_position, + app_state.is_force_redraw, + ); + + // Sanity check + let start_position = if position >= sort_string.len() { + sort_string.len().saturating_sub(1) + } else { + position + }; + + let sliced_vec = &sort_string[start_position..]; + + let sort_options = sliced_vec + .iter() + .map(|column| Row::new(vec![column.as_str()])); + + let column_state = &mut proc_widget_state.columns.column_state; + column_state.select(Some( + proc_widget_state + .columns + .current_scroll_position + .saturating_sub(start_position), + )); + let current_border_style = if proc_widget_state + .process_search_state + .search_state + .is_invalid_search + { + self.colours.invalid_query_style + } else if is_on_widget { + self.colours.highlighted_border_style + } else { + self.colours.border_style + }; + + let process_sort_block = if draw_border { + Block::default() + .borders(Borders::ALL) + .border_style(current_border_style) + } else if is_on_widget { + Block::default() + .borders(*SIDE_BORDERS) + .border_style(current_border_style) + } else { + Block::default().borders(Borders::NONE) + }; + + let highlight_style = if is_on_widget { + self.colours.currently_selected_text_style + } else { + self.colours.text_style + }; + + let margined_draw_loc = Layout::default() + .constraints([Constraint::Percentage(100)]) + .horizontal_margin(if is_on_widget || draw_border { 0 } else { 1 }) + .direction(Direction::Horizontal) + .split(draw_loc)[0]; + + f.render_stateful_widget( + Table::new(sort_options) + .header( + Row::new(vec!["Sort By"]) + .style(self.colours.table_header_style) + .bottom_margin(table_gap), + ) + .block(process_sort_block) + .highlight_style(highlight_style) + .style(self.colours.text_style) + .widths(&[Constraint::Percentage(100)]), + margined_draw_loc, + column_state, + ); + + if app_state.should_get_widget_bounds() { + // Update draw loc in widget map + if let Some(widget) = app_state.widget_map.get_mut(&widget_id) { + widget.top_left_corner = Some((margined_draw_loc.x, margined_draw_loc.y)); + widget.bottom_right_corner = Some(( + margined_draw_loc.x + margined_draw_loc.width, + margined_draw_loc.y + margined_draw_loc.height, + )); + } + } + } + } +} diff --git a/src/canvas/elements/scroll_sort_table.rs b/src/canvas/elements/scroll_sort_table.rs new file mode 100644 index 000000000..549bde458 --- /dev/null +++ b/src/canvas/elements/scroll_sort_table.rs @@ -0,0 +1,35 @@ +#![allow(dead_code)] + +//! Code for a generic table element with scroll and sort support. + +use crate::app::{AppScrollWidgetState, CanvasTableWidthState}; + +use super::element::ElementBounds; + +#[derive(Debug, Default)] +pub struct State { + scroll: AppScrollWidgetState, + width: CanvasTableWidthState, +} + +/// A [`ScrollSortTable`] is a stateful generic table element with scroll and sort support. +pub struct ScrollSortTable { + state: State, + bounds: ElementBounds, +} + +impl ScrollSortTable { + /// Function for incrementing the scroll. + fn increment_scroll(&mut self) {} + + /// Function for decrementing the scroll. + fn decrement_scroll(&mut self) {} + + pub fn on_down(&mut self) { + self.increment_scroll(); + } + + pub fn on_up(&mut self) { + self.decrement_scroll(); + } +} diff --git a/src/canvas/elements/scrollable_table.rs b/src/canvas/elements/scrollable_table.rs new file mode 100644 index 000000000..29ac3d4e3 --- /dev/null +++ b/src/canvas/elements/scrollable_table.rs @@ -0,0 +1,49 @@ +#![allow(dead_code)] +#![allow(unused_variables)] + +use tui::widgets::TableState; + +use super::element::{Element, ElementBounds}; + +/// The state for a [`ScrollableTable`]. +struct ScrollableTableState { + tui_state: TableState, +} + +/// A [`ScrollableTable`] is a stateful table [`Element`] with scrolling support. +pub struct ScrollableTable { + bounds: ElementBounds, + selected: bool, + state: ScrollableTableState, +} + +impl ScrollableTable {} + +impl Element for ScrollableTable { + fn draw( + &mut self, f: &mut tui::Frame<'_, B>, app_state: &crate::app::AppState, + draw_loc: tui::layout::Rect, style: &crate::canvas::canvas_colours::CanvasColours, + ) -> anyhow::Result<()> { + Ok(()) + } + + fn recalculate_click_bounds(&mut self) { + todo!() + } + + fn click_bounds(&self) -> super::element::ElementBounds { + self.bounds + } + + fn is_selected(&self) -> bool { + self.selected + } + + fn select(&mut self) { + self.selected = true; + } + + fn unselect(&mut self) { + self.selected = false; + } +} diff --git a/src/canvas/widgets/temp_table.rs b/src/canvas/elements/temp_table.rs similarity index 98% rename from src/canvas/widgets/temp_table.rs rename to src/canvas/elements/temp_table.rs index bc5ba485c..eaa9ea2a1 100644 --- a/src/canvas/widgets/temp_table.rs +++ b/src/canvas/elements/temp_table.rs @@ -29,15 +29,15 @@ static TEMP_HEADERS_LENS: Lazy> = Lazy::new(|| { pub trait TempTableWidget { fn draw_temp_table( - &self, f: &mut Frame<'_, B>, app_state: &mut app::AppState, draw_loc: Rect, draw_border: bool, - widget_id: u64, + &self, f: &mut Frame<'_, B>, app_state: &mut app::AppState, draw_loc: Rect, + draw_border: bool, widget_id: u64, ); } impl TempTableWidget for Painter { fn draw_temp_table( - &self, f: &mut Frame<'_, B>, app_state: &mut app::AppState, draw_loc: Rect, draw_border: bool, - widget_id: u64, + &self, f: &mut Frame<'_, B>, app_state: &mut app::AppState, draw_loc: Rect, + draw_border: bool, widget_id: u64, ) { let recalculate_column_widths = app_state.should_get_widget_bounds(); if let Some(temp_widget_state) = app_state.temp_state.widget_states.get_mut(&widget_id) { diff --git a/src/canvas/elements/time_graph.rs b/src/canvas/elements/time_graph.rs new file mode 100644 index 000000000..644cfe277 --- /dev/null +++ b/src/canvas/elements/time_graph.rs @@ -0,0 +1,255 @@ +#![allow(dead_code)] + +use std::{borrow::Cow, time::Instant}; + +use tui::{ + backend::Backend, + layout::{Constraint, Rect}, + style::Style, + symbols::Marker, + text::{Span, Spans}, + widgets::{Axis, Block, Borders, Chart, Dataset}, + Frame, +}; +use unicode_segmentation::UnicodeSegmentation; + +use crate::{ + app::AppState, + canvas::{canvas_colours::CanvasColours, drawing_utils::interpolate_points}, + constants::{AUTOHIDE_TIMEOUT_MILLISECONDS, TIME_LABEL_HEIGHT_LIMIT}, +}; + +use super::element::{Element, ElementBounds}; + +/// Struct representing the state of a [`TimeGraph`]. +#[derive(Default)] +struct TimeGraphState { + pub current_max_time_ms: u32, + pub autohide_timer: Option, +} + +/// A stateful graph widget graphing between a time x-axis and some y-axis, supporting time zooming. +pub struct TimeGraph<'d> { + state: TimeGraphState, + bounds: ElementBounds, + name: Cow<'static, str>, + legend_constraints: (Constraint, Constraint), + selected: bool, + data: &'d [(Cow<'static, str>, Style, Vec<(f64, f64)>)], + y_axis_legend: Axis<'d>, + marker: Marker, +} + +impl<'d> TimeGraph<'d> { + /// Creates a new [`TimeGraph`]. + pub fn new( + data: &'d [(Cow<'static, str>, Style, Vec<(f64, f64)>)], legend_bounds: &[f64; 2], + labels: Vec>, + ) -> Self { + Self { + state: TimeGraphState::default(), + bounds: ElementBounds::Unset, + name: Cow::default(), + legend_constraints: (Constraint::Ratio(1, 1), Constraint::Ratio(3, 4)), + selected: false, + data, + y_axis_legend: Axis::default().bounds(*legend_bounds).labels(labels), + marker: Marker::Braille, + } + } + + /// Sets the legend status for a [`TimeGraph`]. + pub fn enable_legend(mut self, enable_legend: bool) -> Self { + if enable_legend { + self.legend_constraints = (Constraint::Ratio(1, 1), Constraint::Ratio(3, 4)); + } else { + self.legend_constraints = (Constraint::Ratio(0, 1), Constraint::Ratio(0, 1)); + } + + self + } + + /// Sets the marker type for a [`TimeGraph`]. + pub fn marker(mut self, use_dot: bool) -> Self { + self.marker = if use_dot { + Marker::Dot + } else { + Marker::Braille + }; + + self + } +} + +impl<'d> Element for TimeGraph<'d> { + fn draw( + &mut self, f: &mut Frame<'_, B>, app_state: &AppState, draw_loc: Rect, + style: &CanvasColours, + ) -> anyhow::Result<()> { + let time_start = -(f64::from(self.state.current_max_time_ms)); + let display_time_labels = vec![ + Span::styled( + format!("{}s", self.state.current_max_time_ms / 1000), + style.graph_style, + ), + Span::styled("0s".to_string(), style.graph_style), + ]; + + let x_axis = if app_state.app_config_fields.hide_time + || (app_state.app_config_fields.autohide_time && self.state.autohide_timer.is_none()) + { + Axis::default().bounds([time_start, 0.0]) + } else if let Some(time) = self.state.autohide_timer { + if std::time::Instant::now().duration_since(time).as_millis() + < AUTOHIDE_TIMEOUT_MILLISECONDS as u128 + { + Axis::default() + .bounds([time_start, 0.0]) + .style(style.graph_style) + .labels(display_time_labels) + } else { + self.state.autohide_timer = None; + Axis::default().bounds([time_start, 0.0]) + } + } else if draw_loc.height < TIME_LABEL_HEIGHT_LIMIT { + Axis::default().bounds([time_start, 0.0]) + } else { + Axis::default() + .bounds([time_start, 0.0]) + .style(style.graph_style) + .labels(display_time_labels) + }; + + let y_axis = self.y_axis_legend.clone(); // Not sure how else to do this right now. + let border_style = if self.selected { + style.highlighted_border_style + } else { + style.border_style + }; + + let title = if app_state.is_expanded { + Spans::from(vec![ + Span::styled(format!(" {} ", self.name), style.widget_title_style), + Span::styled( + format!( + "─{}─ Esc to go back ", + "─".repeat( + usize::from(draw_loc.width).saturating_sub( + UnicodeSegmentation::graphemes( + format!(" {} ── Esc to go back", self.name).as_str(), + true + ) + .count() + + 2 + ) + ) + ), + border_style, + ), + ]) + } else { + Spans::from(Span::styled( + format!(" {} ", self.name), + style.widget_title_style, + )) + }; + + // We unfortunately must store the data at least once, otherwise we get issues with local referencing. + let processed_data = self + .data + .iter() + .map(|(name, style, dataset)| { + // Match time + interpolate; we assume all the datasets are sorted. + + if let Some(end_pos) = dataset.iter().position(|(time, _data)| *time >= time_start) + { + if end_pos > 0 { + // We can interpolate. + + let old = dataset[end_pos - 1]; + let new = dataset[end_pos]; + + let interpolated_point = interpolate_points(&old, &new, time_start); + + ( + (name, *style, &dataset[end_pos..]), + Some([(time_start, interpolated_point), new]), + ) + } else { + ((name, *style, &dataset[end_pos..]), None) + } + } else { + // No need to interpolate, just return the entire thing. + ((name, *style, &dataset[..]), None) + } + }) + .collect::>(); + + let datasets = processed_data + .iter() + .map(|((name, style, cut_data), interpolated_data)| { + if let Some(interpolated_data) = interpolated_data { + vec![ + Dataset::default() + .data(cut_data) + .style(*style) + .name(name.as_ref()) + .marker(self.marker) + .graph_type(tui::widgets::GraphType::Line), + Dataset::default() + .data(interpolated_data.as_ref()) + .style(*style) + .marker(self.marker) + .graph_type(tui::widgets::GraphType::Line), + ] + } else { + vec![Dataset::default() + .data(cut_data) + .style(*style) + .name(name.as_ref()) + .marker(self.marker) + .graph_type(tui::widgets::GraphType::Line)] + } + }) + .flatten() + .collect(); + + f.render_widget( + Chart::new(datasets) + .block( + Block::default() + .title(title) + .borders(Borders::ALL) + .border_style(if self.selected { + style.highlighted_border_style + } else { + style.border_style + }), + ) + .x_axis(x_axis) + .y_axis(y_axis) + .hidden_legend_constraints(self.legend_constraints), + draw_loc, + ); + + Ok(()) + } + + fn recalculate_click_bounds(&mut self) {} + + fn click_bounds(&self) -> super::element::ElementBounds { + self.bounds + } + + fn is_selected(&self) -> bool { + self.selected + } + + fn select(&mut self) { + self.selected = true; + } + + fn unselect(&mut self) { + self.selected = false; + } +} diff --git a/src/canvas.rs b/src/canvas/mod.rs similarity index 99% rename from src/canvas.rs rename to src/canvas/mod.rs index d96b78d95..44700ea7a 100644 --- a/src/canvas.rs +++ b/src/canvas/mod.rs @@ -9,11 +9,9 @@ use tui::{ Frame, Terminal, }; -// use ordered_float::OrderedFloat; - use canvas_colours::*; use dialogs::*; -use widgets::*; +use elements::*; use crate::{ app::{ @@ -32,7 +30,7 @@ use crate::{ mod canvas_colours; mod dialogs; mod drawing_utils; -mod widgets; +mod elements; /// Point is of time, data type Point = (f64, f64); diff --git a/src/constants.rs b/src/constants.rs index f85904bb5..bd6cfd4aa 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -11,9 +11,8 @@ pub const STALE_MAX_MILLISECONDS: u64 = 600 * 1000; // Keep 10 minutes of data. pub const DEFAULT_TIME_MILLISECONDS: u64 = 60 * 1000; // Defaults to 1 min. pub const STALE_MIN_MILLISECONDS: u64 = 30 * 1000; // Lowest is 30 seconds pub const TIME_CHANGE_MILLISECONDS: u64 = 15 * 1000; // How much to increment each time -pub const AUTOHIDE_TIMEOUT_MILLISECONDS: u64 = 5000; // 5 seconds to autohide +pub const AUTOHIDE_TIMEOUT_MILLISECONDS: u128 = 5000; // 5 seconds to autohide -pub const TICK_RATE_IN_MILLISECONDS: u64 = 200; // How fast the screen refreshes pub const DEFAULT_REFRESH_RATE_IN_MILLISECONDS: u64 = 1000; pub const MAX_KEY_TIMEOUT_IN_MILLISECONDS: u64 = 1000; diff --git a/src/lib.rs b/src/lib.rs index 17a9f924d..c17ec0986 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -61,6 +61,7 @@ pub enum BottomEvent { MouseInput(J), Update(Box), Clean, + RequestRedraw, } #[derive(Debug)] @@ -609,19 +610,27 @@ pub fn create_input_thread( if let Ok(poll) = poll(Duration::from_millis(20)) { if poll { if let Ok(event) = read() { - if let Event::Key(key) = event { - if Instant::now().duration_since(keyboard_timer).as_millis() >= 20 { - if sender.send(BottomEvent::KeyInput(key)).is_err() { - break; + match event { + Event::Key(key) => { + if Instant::now().duration_since(keyboard_timer).as_millis() >= 20 { + if sender.send(BottomEvent::KeyInput(key)).is_err() { + break; + } + keyboard_timer = Instant::now(); } - keyboard_timer = Instant::now(); } - } else if let Event::Mouse(mouse) = event { - if Instant::now().duration_since(mouse_timer).as_millis() >= 20 { - if sender.send(BottomEvent::MouseInput(mouse)).is_err() { - break; + Event::Mouse(mouse) => { + if Instant::now().duration_since(mouse_timer).as_millis() >= 20 { + if sender.send(BottomEvent::MouseInput(mouse)).is_err() { + break; + } + mouse_timer = Instant::now(); } - mouse_timer = Instant::now(); + } + Event::Resize(_, _) => { + // if sender.send(BottomEvent::RequestRedraw).is_err() { + // break; + // } } } }