From 3b5774117f7a7aa5e99f6553e989688f4362e8e2 Mon Sep 17 00:00:00 2001 From: Clement Tsang <34804052+ClementTsang@users.noreply.github.com> Date: Sat, 31 Dec 2022 05:51:59 -0500 Subject: [PATCH] bug: fix search scrolling with wider Unicode characters. (#938) This should help make search scrolling more reliable larger Unicode characters like CJK/flag characters. --- CHANGELOG.md | 1 + src/app.rs | 165 +++++-------------- src/app/query.rs | 2 +- src/app/states.rs | 244 +++++++++++++++++++++++++++- src/canvas/drawing_utils.rs | 39 ----- src/canvas/widgets/process_table.rs | 102 ++++++------ src/components/data_table/state.rs | 21 +-- src/utils/gen_util.rs | 36 +++- src/widgets/process_table.rs | 77 +-------- 9 files changed, 372 insertions(+), 315 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e6060a1..f661d680 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [#805](https://github.com/ClementTsang/bottom/pull/805): Fix bottom keeping devices awake in certain scenarios. - [#825](https://github.com/ClementTsang/bottom/pull/825): Use alternative method of getting parent PID in some cases on macOS devices to avoid needing root access. - [#916](https://github.com/ClementTsang/bottom/pull/916): Fix possible gaps with widget layout spacing. +- [#938](https://github.com/ClementTsang/bottom/pull/938): Fix search scrolling with wider Unicode characters. ## Changes diff --git a/src/app.rs b/src/app.rs index a2bffe95..78a64b00 100644 --- a/src/app.rs +++ b/src/app.rs @@ -11,7 +11,6 @@ use layout_manager::*; pub use states::*; use typed_builder::*; use unicode_segmentation::{GraphemeCursor, UnicodeSegmentation}; -use unicode_width::{UnicodeWidthChar, UnicodeWidthStr}; use crate::widgets::{ProcWidget, ProcWidgetMode}; use crate::{ @@ -32,8 +31,6 @@ pub mod states; use frozen_state::FrozenState; -const MAX_SEARCH_LENGTH: usize = 200; // FIXME: Remove this limit, it's unnecessary. - #[derive(Debug, Clone)] pub enum AxisScaling { Log, @@ -504,23 +501,21 @@ impl App { { if is_in_search_widget { if proc_widget_state.proc_search.search_state.is_enabled - && proc_widget_state.get_search_cursor_position() + && proc_widget_state.cursor_char_index() < proc_widget_state .proc_search .search_state .current_search_query .len() { - let current_cursor = proc_widget_state.get_search_cursor_position(); - proc_widget_state - .search_walk_forward(proc_widget_state.get_search_cursor_position()); + let current_cursor = proc_widget_state.cursor_char_index(); + proc_widget_state.search_walk_forward(); - let _removed_chars: String = proc_widget_state + let _ = proc_widget_state .proc_search .search_state .current_search_query - .drain(current_cursor..proc_widget_state.get_search_cursor_position()) - .collect(); + .drain(current_cursor..proc_widget_state.cursor_char_index()); proc_widget_state.proc_search.search_state.grapheme_cursor = GraphemeCursor::new( @@ -552,22 +547,21 @@ impl App { { if is_in_search_widget && proc_widget_state.proc_search.search_state.is_enabled - && proc_widget_state.get_search_cursor_position() > 0 + && proc_widget_state.cursor_char_index() > 0 { - let current_cursor = proc_widget_state.get_search_cursor_position(); - proc_widget_state - .search_walk_back(proc_widget_state.get_search_cursor_position()); + let current_cursor = proc_widget_state.cursor_char_index(); + proc_widget_state.search_walk_back(); - let removed_chars: String = proc_widget_state + // Remove the indices in between. + let _ = proc_widget_state .proc_search .search_state .current_search_query - .drain(proc_widget_state.get_search_cursor_position()..current_cursor) - .collect(); + .drain(proc_widget_state.cursor_char_index()..current_cursor); proc_widget_state.proc_search.search_state.grapheme_cursor = GraphemeCursor::new( - proc_widget_state.get_search_cursor_position(), + proc_widget_state.cursor_char_index(), proc_widget_state .proc_search .search_state @@ -576,11 +570,6 @@ impl App { true, ); - proc_widget_state - .proc_search - .search_state - .char_cursor_position -= UnicodeWidthStr::width(removed_chars.as_str()); - proc_widget_state.proc_search.search_state.cursor_direction = CursorDirection::Left; @@ -684,19 +673,9 @@ impl App { .get_mut_widget_state(self.current_widget.widget_id - 1) { if is_in_search_widget { - let prev_cursor = proc_widget_state.get_search_cursor_position(); - proc_widget_state - .search_walk_back(proc_widget_state.get_search_cursor_position()); - if proc_widget_state.get_search_cursor_position() < prev_cursor { - let str_slice = &proc_widget_state - .proc_search - .search_state - .current_search_query - [proc_widget_state.get_search_cursor_position()..prev_cursor]; - proc_widget_state - .proc_search - .search_state - .char_cursor_position -= UnicodeWidthStr::width(str_slice); + let prev_cursor = proc_widget_state.cursor_char_index(); + proc_widget_state.search_walk_back(); + if proc_widget_state.cursor_char_index() < prev_cursor { proc_widget_state.proc_search.search_state.cursor_direction = CursorDirection::Left; } @@ -753,20 +732,9 @@ impl App { .get_mut_widget_state(self.current_widget.widget_id - 1) { if is_in_search_widget { - let prev_cursor = proc_widget_state.get_search_cursor_position(); - proc_widget_state.search_walk_forward( - proc_widget_state.get_search_cursor_position(), - ); - if proc_widget_state.get_search_cursor_position() > prev_cursor { - let str_slice = &proc_widget_state - .proc_search - .search_state - .current_search_query - [prev_cursor..proc_widget_state.get_search_cursor_position()]; - proc_widget_state - .proc_search - .search_state - .char_cursor_position += UnicodeWidthStr::width(str_slice); + let prev_cursor = proc_widget_state.cursor_char_index(); + proc_widget_state.search_walk_forward(); + if proc_widget_state.cursor_char_index() > prev_cursor { proc_widget_state.proc_search.search_state.cursor_direction = CursorDirection::Right; } @@ -932,10 +900,7 @@ impl App { .len(), true, ); - proc_widget_state - .proc_search - .search_state - .char_cursor_position = 0; + proc_widget_state.proc_search.search_state.cursor_direction = CursorDirection::Left; } @@ -954,30 +919,14 @@ impl App { .get_mut(&(self.current_widget.widget_id - 1)) { if is_in_search_widget { - proc_widget_state.proc_search.search_state.grapheme_cursor = - GraphemeCursor::new( - proc_widget_state - .proc_search - .search_state - .current_search_query - .len(), - proc_widget_state - .proc_search - .search_state - .current_search_query - .len(), - true, - ); - proc_widget_state + let query_len = proc_widget_state .proc_search .search_state - .char_cursor_position = UnicodeWidthStr::width( - proc_widget_state - .proc_search - .search_state - .current_search_query - .as_str(), - ); + .current_search_query + .len(); + + proc_widget_state.proc_search.search_state.grapheme_cursor = + GraphemeCursor::new(query_len, query_len, true); proc_widget_state.proc_search.search_state.cursor_direction = CursorDirection::Right; } @@ -1008,11 +957,11 @@ impl App { // Traverse backwards from the current cursor location until you hit non-whitespace characters, // then continue to traverse (and delete) backwards until you hit a whitespace character. Halt. - // So... first, let's get our current cursor position using graphemes... - let end_index = proc_widget_state.get_char_cursor_position(); + // So... first, let's get our current cursor position in terms of char indices. + let end_index = proc_widget_state.cursor_char_index(); // Then, let's crawl backwards until we hit our location, and store the "head"... - let query = proc_widget_state.get_current_search_query(); + let query = proc_widget_state.current_search_query(); let mut start_index = 0; let mut saw_non_whitespace = false; @@ -1032,12 +981,11 @@ impl App { } } - let removed_chars: String = proc_widget_state + let _ = proc_widget_state .proc_search .search_state .current_search_query - .drain(start_index..end_index) - .collect(); + .drain(start_index..end_index); proc_widget_state.proc_search.search_state.grapheme_cursor = GraphemeCursor::new( start_index, @@ -1049,11 +997,6 @@ impl App { true, ); - proc_widget_state - .proc_search - .search_state - .char_cursor_position -= UnicodeWidthStr::width(removed_chars.as_str()); - proc_widget_state.proc_search.search_state.cursor_direction = CursorDirection::Left; proc_widget_state.update_query(); @@ -1113,25 +1056,16 @@ impl App { .widget_states .get_mut(&(self.current_widget.widget_id - 1)) { - if is_in_search_widget - && proc_widget_state.is_search_enabled() - && UnicodeWidthStr::width( - proc_widget_state - .proc_search - .search_state - .current_search_query - .as_str(), - ) <= MAX_SEARCH_LENGTH - { + if is_in_search_widget && proc_widget_state.is_search_enabled() { proc_widget_state .proc_search .search_state .current_search_query - .insert(proc_widget_state.get_search_cursor_position(), caught_char); + .insert(proc_widget_state.cursor_char_index(), caught_char); proc_widget_state.proc_search.search_state.grapheme_cursor = GraphemeCursor::new( - proc_widget_state.get_search_cursor_position(), + proc_widget_state.cursor_char_index(), proc_widget_state .proc_search .search_state @@ -1139,14 +1073,7 @@ impl App { .len(), true, ); - proc_widget_state - .search_walk_forward(proc_widget_state.get_search_cursor_position()); - - proc_widget_state - .proc_search - .search_state - .char_cursor_position += - UnicodeWidthChar::width(caught_char).unwrap_or(0); + proc_widget_state.search_walk_forward(); proc_widget_state.update_query(); proc_widget_state.proc_search.search_state.cursor_direction = @@ -2724,22 +2651,10 @@ impl App { .widget_states .get_mut(&(self.current_widget.widget_id - 1)) { - let curr_width = UnicodeWidthStr::width( - proc_widget_state - .proc_search - .search_state - .current_search_query - .as_str(), - ); - let paste_width = UnicodeWidthStr::width(paste.as_str()); let num_runes = UnicodeSegmentation::graphemes(paste.as_str(), true).count(); - if is_in_search_widget - && proc_widget_state.is_search_enabled() - && curr_width + paste_width <= MAX_SEARCH_LENGTH - { - let paste_char_width = paste.len(); - let left_bound = proc_widget_state.get_search_cursor_position(); + if is_in_search_widget && proc_widget_state.is_search_enabled() { + let left_bound = proc_widget_state.cursor_char_index(); let curr_query = &mut proc_widget_state .proc_search @@ -2752,15 +2667,9 @@ impl App { GraphemeCursor::new(left_bound, curr_query.len(), true); for _ in 0..num_runes { - let cursor = proc_widget_state.get_search_cursor_position(); - proc_widget_state.search_walk_forward(cursor); + proc_widget_state.search_walk_forward(); } - proc_widget_state - .proc_search - .search_state - .char_cursor_position += paste_char_width; - proc_widget_state.update_query(); proc_widget_state.proc_search.search_state.cursor_direction = CursorDirection::Right; diff --git a/src/app/query.rs b/src/app/query.rs index 0eca1b75..a894577e 100644 --- a/src/app/query.rs +++ b/src/app/query.rs @@ -253,7 +253,7 @@ pub fn parse_query( if content == "=" { // Check next string if possible if let Some(queue_next) = query.pop_front() { - // TODO: [Query, ???] Need to consider the following cases: + // TODO: [Query] Need to consider the following cases: // - (test) // - (test // - test) diff --git a/src/app/states.rs b/src/app/states.rs index a24a52f1..e4141e4f 100644 --- a/src/app/states.rs +++ b/src/app/states.rs @@ -1,10 +1,12 @@ -use std::{collections::HashMap, time::Instant}; +use std::{collections::HashMap, ops::Range, time::Instant}; -use unicode_segmentation::GraphemeCursor; +use indexmap::IndexMap; +use unicode_segmentation::{GraphemeCursor, GraphemeIncomplete, UnicodeSegmentation}; use crate::{ app::{layout_manager::BottomWidgetType, query::*}, constants, + utils::gen_util::str_width, widgets::{ BatteryWidgetState, CpuWidgetState, DiskTableWidget, MemWidgetState, NetWidgetState, ProcWidget, TempWidgetState, @@ -71,10 +73,11 @@ pub struct AppSearchState { pub is_invalid_search: bool, pub grapheme_cursor: GraphemeCursor, pub cursor_direction: CursorDirection, - pub cursor_bar: usize, - /// This represents the position in terms of CHARACTERS, not graphemes - pub char_cursor_position: usize, - /// The query + + pub display_start_char_index: usize, + pub size_mappings: IndexMap>, + + /// The query. TODO: Merge this as one enum. pub query: Option, pub error_message: Option, } @@ -88,8 +91,8 @@ impl Default for AppSearchState { is_blank_search: true, grapheme_cursor: GraphemeCursor::new(0, 0, true), cursor_direction: CursorDirection::Right, - cursor_bar: 0, - char_cursor_position: 0, + display_start_char_index: 0, + size_mappings: IndexMap::default(), query: None, error_message: None, } @@ -109,6 +112,142 @@ impl AppSearchState { pub fn is_invalid_or_blank_search(&self) -> bool { self.is_blank_search || self.is_invalid_search } + + /// Sets the starting grapheme index to draw from. + pub fn get_start_position(&mut self, available_width: usize, is_force_redraw: bool) { + // Remember - the number of columns != the number of grapheme slots/sizes, you + // cannot use index to determine this reliably! + + let start_index = if is_force_redraw { + 0 + } else { + self.display_start_char_index + }; + let cursor_index = self.grapheme_cursor.cur_cursor(); + + if let Some(start_range) = self.size_mappings.get(&start_index) { + let cursor_range = self + .size_mappings + .get(&cursor_index) + .cloned() + .unwrap_or_else(|| { + self.size_mappings + .last() + .map(|(_, r)| r.end..(r.end + 1)) + .unwrap_or(start_range.end..(start_range.end + 1)) + }); + + // Cases to handle in both cases: + // - The current start index can show the cursor's word. + // - The current start index cannot show the cursor's word. + // + // What differs is how we "scroll" based on the cursor movement direction. + + self.display_start_char_index = match self.cursor_direction { + CursorDirection::Right => { + if start_range.start + available_width >= cursor_range.end { + // Use the current index. + start_index + } else if cursor_range.end >= available_width { + // If the current position is past the last visible element, skip until we see it. + + let mut index = 0; + for i in 0..(cursor_index + 1) { + if let Some(r) = self.size_mappings.get(&i) { + if r.start + available_width >= cursor_range.end { + index = i; + break; + } + } + } + + index + } else { + 0 + } + } + CursorDirection::Left => { + if cursor_range.start < start_range.end { + let mut index = 0; + for i in cursor_index..(self.current_search_query.len()) { + if let Some(r) = self.size_mappings.get(&i) { + if r.start + available_width >= cursor_range.end { + index = i; + break; + } + } + } + index + } else { + start_index + } + } + }; + } else { + // If we fail here somehow, just reset to 0 index + scroll left. + self.display_start_char_index = 0; + self.cursor_direction = CursorDirection::Left; + }; + } + + pub(crate) fn walk_forward(&mut self) { + // TODO: Add tests for this. + let start_position = self.grapheme_cursor.cur_cursor(); + let chunk = &self.current_search_query[start_position..]; + + match self.grapheme_cursor.next_boundary(chunk, start_position) { + Ok(_) => {} + Err(err) => match err { + GraphemeIncomplete::PreContext(ctx) => { + // Provide the entire string as context. Not efficient but should resolve failures. + self.grapheme_cursor + .provide_context(&self.current_search_query[0..ctx], 0); + + self.grapheme_cursor + .next_boundary(chunk, start_position) + .unwrap(); + } + _ => Err(err).unwrap(), + }, + } + } + + pub(crate) fn walk_backward(&mut self) { + // TODO: Add tests for this. + let start_position = self.grapheme_cursor.cur_cursor(); + let chunk = &self.current_search_query[..start_position]; + + match self.grapheme_cursor.prev_boundary(chunk, 0) { + Ok(_) => {} + Err(err) => match err { + GraphemeIncomplete::PreContext(ctx) => { + // Provide the entire string as context. Not efficient but should resolve failures. + self.grapheme_cursor + .provide_context(&self.current_search_query[0..ctx], 0); + + self.grapheme_cursor.prev_boundary(chunk, 0).unwrap(); + } + _ => Err(err).unwrap(), + }, + } + } + + pub(crate) fn update_sizes(&mut self) { + self.size_mappings.clear(); + let mut curr_offset = 0; + for (index, grapheme) in + UnicodeSegmentation::grapheme_indices(self.current_search_query.as_str(), true) + { + let width = str_width(grapheme); + let end = curr_offset + width; + + self.size_mappings.insert(index, curr_offset..end); + + curr_offset = end; + } + + self.size_mappings.shrink_to_fit(); + } } pub struct ProcState { @@ -266,3 +405,92 @@ pub struct ParagraphScrollState { pub current_scroll_index: u16, pub max_scroll_index: u16, } + +#[cfg(test)] +mod test { + use super::*; + + fn move_right(state: &mut AppSearchState) { + state.walk_forward(); + state.cursor_direction = CursorDirection::Right; + } + + fn move_left(state: &mut AppSearchState) { + state.walk_backward(); + state.cursor_direction = CursorDirection::Left; + } + + #[test] + fn search_cursor_moves() { + let mut state = AppSearchState::default(); + state.current_search_query = "Hi, 你好! 🇦🇶".to_string(); + state.grapheme_cursor = GraphemeCursor::new(0, state.current_search_query.len(), true); + state.update_sizes(); + + // Moving right. + state.get_start_position(4, false); + assert_eq!(state.grapheme_cursor.cur_cursor(), 0); + assert_eq!(state.display_start_char_index, 0); + + move_right(&mut state); + state.get_start_position(4, false); + assert_eq!(state.grapheme_cursor.cur_cursor(), 1); + assert_eq!(state.display_start_char_index, 0); + + move_right(&mut state); + state.get_start_position(4, false); + assert_eq!(state.grapheme_cursor.cur_cursor(), 2); + assert_eq!(state.display_start_char_index, 0); + + move_right(&mut state); + state.get_start_position(4, false); + assert_eq!(state.grapheme_cursor.cur_cursor(), 3); + assert_eq!(state.display_start_char_index, 0); + + move_right(&mut state); + state.get_start_position(4, false); + assert_eq!(state.grapheme_cursor.cur_cursor(), 4); + assert_eq!(state.display_start_char_index, 2); + + move_right(&mut state); + state.get_start_position(4, false); + assert_eq!(state.grapheme_cursor.cur_cursor(), 7); + assert_eq!(state.display_start_char_index, 4); + + move_right(&mut state); + state.get_start_position(4, false); + assert_eq!(state.grapheme_cursor.cur_cursor(), 10); + assert_eq!(state.display_start_char_index, 7); + + move_right(&mut state); + move_right(&mut state); + state.get_start_position(4, false); + assert_eq!(state.grapheme_cursor.cur_cursor(), 12); + assert_eq!(state.display_start_char_index, 10); + + // Moving left. + move_left(&mut state); + state.get_start_position(4, false); + assert_eq!(state.grapheme_cursor.cur_cursor(), 11); + assert_eq!(state.display_start_char_index, 10); + + move_left(&mut state); + move_left(&mut state); + state.get_start_position(4, false); + assert_eq!(state.grapheme_cursor.cur_cursor(), 7); + assert_eq!(state.display_start_char_index, 7); + + move_left(&mut state); + move_left(&mut state); + move_left(&mut state); + move_left(&mut state); + state.get_start_position(4, false); + assert_eq!(state.grapheme_cursor.cur_cursor(), 1); + assert_eq!(state.display_start_char_index, 1); + + move_left(&mut state); + state.get_start_position(4, false); + assert_eq!(state.grapheme_cursor.cur_cursor(), 0); + assert_eq!(state.display_start_char_index, 0); + } +} diff --git a/src/canvas/drawing_utils.rs b/src/canvas/drawing_utils.rs index 8d29b9a5..8a658929 100644 --- a/src/canvas/drawing_utils.rs +++ b/src/canvas/drawing_utils.rs @@ -2,45 +2,6 @@ use std::{cmp::min, time::Instant}; use tui::layout::Rect; -use crate::app::CursorDirection; - -pub fn get_search_start_position( - num_columns: usize, cursor_direction: &CursorDirection, cursor_bar: &mut usize, - current_cursor_position: usize, is_force_redraw: bool, -) -> usize { - if is_force_redraw { - *cursor_bar = 0; - } - - match cursor_direction { - CursorDirection::Right => { - if current_cursor_position < *cursor_bar + num_columns { - // If, using previous_scrolled_position, we can see the element - // (so within that and + num_rows) just reuse the current previously scrolled position. - *cursor_bar - } else if current_cursor_position >= num_columns { - // Else if the current position past the last element visible in the list, omit - // until we can see that element. - *cursor_bar = current_cursor_position - num_columns; - *cursor_bar - } else { - // Else, if it is not past the last element visible, do not omit anything. - 0 - } - } - CursorDirection::Left => { - if current_cursor_position <= *cursor_bar { - // If it's past the first element, then show from that element downwards. - *cursor_bar = current_cursor_position; - } else if current_cursor_position >= *cursor_bar + num_columns { - *cursor_bar = current_cursor_position - num_columns; - } - // Else, don't change what our start position is from whatever it is set to! - *cursor_bar - } - } -} - /// Calculate how many bars are to be drawn within basic mode's components. pub fn calculate_basic_use_bars(use_percentage: f64, num_bars_available: usize) -> usize { min( diff --git a/src/canvas/widgets/process_table.rs b/src/canvas/widgets/process_table.rs index ca6fd54e..9b0c23ea 100644 --- a/src/canvas/widgets/process_table.rs +++ b/src/canvas/widgets/process_table.rs @@ -1,16 +1,16 @@ use tui::{ backend::Backend, layout::{Alignment, Constraint, Direction, Layout, Rect}, + style::Style, terminal::Frame, text::{Span, Spans}, widgets::{Block, Borders, Paragraph}, }; -use unicode_segmentation::{GraphemeIndices, UnicodeSegmentation}; -use unicode_width::UnicodeWidthStr; +use unicode_segmentation::UnicodeSegmentation; use crate::{ - app::App, - canvas::{drawing_utils::get_search_start_position, Painter}, + app::{App, AppSearchState}, + canvas::Painter, components::data_table::{DrawInfo, SelectionState}, constants::*, }; @@ -68,8 +68,6 @@ impl Painter { /// Draws the process sort box. /// - `widget_id` represents the widget ID of the process widget itself.an - /// - /// This should not be directly called. fn draw_processes_table( &self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, widget_id: u64, ) { @@ -99,38 +97,42 @@ impl Painter { /// 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 App, 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_pos = 0; + fn build_query_span( + search_state: &AppSearchState, available_width: usize, is_on_widget: bool, + currently_selected_text_style: Style, text_style: Style, + ) -> Vec> { + let start_index = search_state.display_start_char_index; + let cursor_index = search_state.grapheme_cursor.cur_cursor(); + let mut current_width = 0; + let query = search_state.current_search_query.as_str(); if is_on_widget { - let mut res = grapheme_indices - .filter_map(|grapheme| { - current_grapheme_pos += UnicodeWidthStr::width(grapheme.1); - - if current_grapheme_pos <= start_position { - None + let mut res = Vec::with_capacity(available_width); + for ((index, grapheme), lengths) in + UnicodeSegmentation::grapheme_indices(query, true) + .zip(search_state.size_mappings.values()) + { + if index < start_index { + continue; + } else if current_width > available_width { + break; + } else { + let styled = if index == cursor_index { + Span::styled(grapheme, currently_selected_text_style) } 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::>(); + Span::styled(grapheme, text_style) + }; - if cursor_position == query.len() { + res.push(styled); + current_width += lengths.end - lengths.start; + } + } + + if cursor_index == query.len() { res.push(Span::styled(" ", currently_selected_text_style)) } @@ -143,44 +145,36 @@ impl Painter { } } - // 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 = "> "; + const SEARCH_TITLE: &str = "> "; + let offset = if draw_border { 4 } else { 2 }; // width of 3 removed for >_| + let available_width = if num_columns > (offset + 3) { + num_columns - offset + } else { + num_columns + }; - 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(); + proc_widget_state + .proc_search + .search_state + .get_start_position(available_width, app_state.is_force_redraw); - let start_position: usize = get_search_start_position( - num_columns - num_chars_for_text - 5, - &proc_widget_state.proc_search.search_state.cursor_direction, - &mut proc_widget_state.proc_search.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( + let query_with_cursor = build_query_span( + &proc_widget_state.proc_search.search_state, + available_width, 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, + SEARCH_TITLE, if is_on_widget { self.colours.table_header_style } else { @@ -300,8 +294,6 @@ impl Painter { /// 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_sort_table( &self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, widget_id: u64, ) { diff --git a/src/components/data_table/state.rs b/src/components/data_table/state.rs index 1376328a..7398d54a 100644 --- a/src/components/data_table/state.rs +++ b/src/components/data_table/state.rs @@ -52,26 +52,27 @@ impl Default for DataTableState { impl DataTableState { /// Gets the starting position of a table. pub fn get_start_position(&mut self, num_rows: usize, is_force_redraw: bool) { - let mut start_index = self.display_start_index; + let start_index = if is_force_redraw { + 0 + } else { + self.display_start_index + }; let current_scroll_position = self.current_index; let scroll_direction = self.scroll_direction; - if is_force_redraw { - start_index = 0; - } - self.display_start_index = match scroll_direction { ScrollDirection::Down => { if current_scroll_position < start_index + num_rows { - // If, using previous_scrolled_position, we can see the element - // (so within that and + num_rows) just reuse the current previously scrolled position + // If, using the current scroll position, we can see the element + // (so within that and + num_rows) just reuse the current previously + // scrolled position. start_index } else if current_scroll_position >= num_rows { - // Else if the current position past the last element visible in the list, omit - // until we can see that element + // If the current position past the last element visible in the list, + // then skip until we can see that element. current_scroll_position - num_rows + 1 } else { - // Else, if it is not past the last element visible, do not omit anything + // Else, if it is not past the last element visible, do not omit anything. 0 } } diff --git a/src/utils/gen_util.rs b/src/utils/gen_util.rs index 02b8bfd4..5f41d2c5 100644 --- a/src/utils/gen_util.rs +++ b/src/utils/gen_util.rs @@ -103,10 +103,39 @@ pub fn truncate_to_text<'a, U: Into>(content: &str, width: U) -> Text<'a> } } +/// Returns the width of a str `s`. This takes into account some things like +/// joiners when calculating width. +pub fn str_width(s: &str) -> usize { + UnicodeSegmentation::graphemes(s, true) + .map(|g| { + if g.contains('\u{200d}') { + 2 + } else { + UnicodeWidthStr::width(g) + } + }) + .sum() +} + +/// Returns the "width" of grapheme `g`. This takes into account some things like +/// joiners when calculating width. +/// +/// Note that while you *can* pass in an entire string, the point is to check +/// individual graphemes (e.g. `"a"`, `"💎"`, `"大"`, `"🇨🇦"`). +#[inline] +fn grapheme_width(g: &str) -> usize { + if g.contains('\u{200d}') { + 2 + } else { + UnicodeWidthStr::width(g) + } +} + /// Truncates a string with an ellipsis character. /// /// NB: This probably does not handle EVERY case, but I think it handles most cases /// we will use this function for fine... hopefully. +#[inline] fn truncate_str>(content: &str, width: U) -> String { let width = width.into(); let mut text = String::with_capacity(width); @@ -124,11 +153,7 @@ fn truncate_str>(content: &str, width: U) -> String { // - Adds a character not up to the boundary, then fails. // Inspired by https://tomdebruijn.com/posts/rust-string-length-width-calculations/ for g in UnicodeSegmentation::graphemes(content, true) { - let g_width = if g.contains('\u{200d}') { - 2 - } else { - UnicodeWidthStr::width(g) - }; + let g_width = grapheme_width(g); if curr_width + g_width <= width { curr_width += g_width; @@ -164,7 +189,6 @@ pub const fn sort_partial_fn(is_descending: bool) -> fn /// Returns an [`Ordering`] between two [`PartialOrd`]s. #[inline] pub fn partial_ordering(a: T, b: T) -> Ordering { - // TODO: Switch to `total_cmp` on 1.62 a.partial_cmp(&b).unwrap_or(Ordering::Equal) } diff --git a/src/widgets/process_table.rs b/src/widgets/process_table.rs index 390158ec..46d6ef60 100644 --- a/src/widgets/process_table.rs +++ b/src/widgets/process_table.rs @@ -27,7 +27,6 @@ pub use proc_widget_data::*; mod sort_table; use sort_table::SortTableColumn; -use unicode_segmentation::GraphemeIncomplete; /// ProcessSearchState only deals with process' search's current settings and state. pub struct ProcessSearchState { @@ -710,19 +709,15 @@ impl ProcWidget { .collect::>() } - pub fn get_search_cursor_position(&self) -> usize { + pub fn cursor_char_index(&self) -> usize { self.proc_search.search_state.grapheme_cursor.cur_cursor() } - pub fn get_char_cursor_position(&self) -> usize { - self.proc_search.search_state.char_cursor_position - } - pub fn is_search_enabled(&self) -> bool { self.proc_search.search_state.is_enabled } - pub fn get_current_search_query(&self) -> &String { + pub fn current_search_query(&self) -> &str { &self.proc_search.search_state.current_search_query } @@ -759,6 +754,9 @@ impl ProcWidget { self.table.state.display_start_index = 0; self.table.state.current_index = 0; + // Update the internal sizes too. + self.proc_search.search_state.update_sizes(); + self.force_data_update(); } @@ -767,69 +765,12 @@ impl ProcWidget { self.force_data_update(); } - pub fn search_walk_forward(&mut self, start_position: usize) { - // TODO: Add tests for this. - let chunk = &self.proc_search.search_state.current_search_query[start_position..]; - - match self - .proc_search - .search_state - .grapheme_cursor - .next_boundary(chunk, start_position) - { - Ok(_) => {} - Err(err) => match err { - GraphemeIncomplete::PreContext(ctx) => { - // Provide the entire string as context. Not efficient but should resolve failures. - self.proc_search - .search_state - .grapheme_cursor - .provide_context( - &self.proc_search.search_state.current_search_query[0..ctx], - 0, - ); - - self.proc_search - .search_state - .grapheme_cursor - .next_boundary(chunk, start_position) - .unwrap(); - } - _ => Err(err).unwrap(), - }, - } + pub fn search_walk_forward(&mut self) { + self.proc_search.search_state.walk_forward(); } - pub fn search_walk_back(&mut self, start_position: usize) { - // TODO: Add tests for this. - let chunk = &self.proc_search.search_state.current_search_query[..start_position]; - match self - .proc_search - .search_state - .grapheme_cursor - .prev_boundary(chunk, 0) - { - Ok(_) => {} - Err(err) => match err { - GraphemeIncomplete::PreContext(ctx) => { - // Provide the entire string as context. Not efficient but should resolve failures. - self.proc_search - .search_state - .grapheme_cursor - .provide_context( - &self.proc_search.search_state.current_search_query[0..ctx], - 0, - ); - - self.proc_search - .search_state - .grapheme_cursor - .prev_boundary(chunk, 0) - .unwrap(); - } - _ => Err(err).unwrap(), - }, - } + pub fn search_walk_back(&mut self) { + self.proc_search.search_state.walk_backward(); } /// Returns the number of columns *enabled*. Note this differs from *visible* - a column may be enabled but not