summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorTim Oram <dev@mitmaro.ca>2022-03-19 03:23:05 -0230
committerTim Oram <dev@mitmaro.ca>2022-03-19 03:57:28 -0230
commite12c2eb2ff7ddde82c65931e1135384bd525f6b1 (patch)
treecbaf981bead90cb5c101f29f8ddc8ea3e9f2e3c0
parentced9c585fb5dfb711f4b594ba4f8809632884a2e (diff)
Extract editable line from edit component
The edit component contains the functionality to edit a line, which is useful outside the context of that component. This extracts that functionality into a shared location.
-rw-r--r--src/core/src/components/edit/mod.rs148
-rw-r--r--src/core/src/components/edit/tests.rs472
-rw-r--r--src/core/src/components/mod.rs1
-rw-r--r--src/core/src/components/shared/editable_line.rs598
-rw-r--r--src/core/src/components/shared/mod.rs3
5 files changed, 633 insertions, 589 deletions
diff --git a/src/core/src/components/edit/mod.rs b/src/core/src/components/edit/mod.rs
index b1dc884..1436651 100644
--- a/src/core/src/components/edit/mod.rs
+++ b/src/core/src/components/edit/mod.rs
@@ -4,18 +4,22 @@ mod tests;
use display::DisplayColor;
use input::{Event, InputOptions, KeyCode, KeyEvent, KeyModifiers};
use lazy_static::lazy_static;
-use unicode_segmentation::UnicodeSegmentation;
use view::{LineSegment, ViewData, ViewDataUpdater, ViewLine};
+use crate::components::shared::EditableLine;
+
lazy_static! {
pub static ref INPUT_OPTIONS: InputOptions = InputOptions::RESIZE;
}
+const FINISH_EVENT: Event = Event::Key(KeyEvent {
+ code: KeyCode::Enter,
+ modifiers: KeyModifiers::NONE,
+});
+
pub(crate) struct Edit {
- content: String,
- cursor_position: usize,
+ editable_line: EditableLine,
finished: bool,
- label: Option<String>,
view_data: ViewData,
}
@@ -25,10 +29,8 @@ impl Edit {
updater.set_show_title(true);
});
Self {
- content: String::from(""),
- cursor_position: 0,
+ editable_line: EditableLine::new(),
finished: false,
- label: None,
view_data,
}
}
@@ -38,48 +40,15 @@ impl Edit {
F: FnOnce(&mut ViewDataUpdater<'_>),
G: FnOnce(&mut ViewDataUpdater<'_>),
{
- let line = self.content.as_str();
- let pointer = self.cursor_position;
-
- let graphemes = UnicodeSegmentation::graphemes(line, true);
-
- let start = graphemes.clone().take(pointer).collect::<String>();
- let indicator = graphemes.clone().skip(pointer).take(1).collect::<String>();
- let end = graphemes.skip(pointer + 1).collect::<String>();
-
- let mut segments = vec![];
- if let Some(label) = self.label.as_ref() {
- segments.push(LineSegment::new_with_color_and_style(
- label.as_str(),
- DisplayColor::Normal,
- true,
- false,
- false,
- ));
- }
- if !start.is_empty() {
- segments.push(LineSegment::new(start.as_str()));
- }
- segments.push(
- if indicator.is_empty() {
- LineSegment::new_with_color_and_style(" ", DisplayColor::Normal, false, true, false)
- }
- else {
- LineSegment::new_with_color_and_style(indicator.as_str(), DisplayColor::Normal, false, true, false)
- },
- );
- if !end.is_empty() {
- segments.push(LineSegment::new(end.as_str()));
- }
self.view_data.update_view_data(|updater| {
updater.clear();
before_build(updater);
- updater.push_line(ViewLine::from(segments));
+ updater.push_line(ViewLine::from(self.editable_line.line_segments()));
updater.push_trailing_line(ViewLine::new_pinned(vec![LineSegment::new_with_color(
"Enter to finish",
DisplayColor::IndicatorColor,
)]));
- updater.ensure_column_visible(pointer);
+ updater.ensure_column_visible(self.editable_line.cursor_position());
updater.ensure_line_visible(0);
after_build(updater);
});
@@ -91,95 +60,30 @@ impl Edit {
}
pub(crate) fn handle_event(&mut self, event: Event) {
- match event {
- Event::Key(KeyEvent {
- code: KeyCode::Backspace,
- modifiers: KeyModifiers::NONE,
- }) => {
- if self.cursor_position != 0 {
- let start = UnicodeSegmentation::graphemes(self.content.as_str(), true)
- .take(self.cursor_position - 1)
- .collect::<String>();
- let end = UnicodeSegmentation::graphemes(self.content.as_str(), true)
- .skip(self.cursor_position)
- .collect::<String>();
- self.content = format!("{}{}", start, end);
- self.cursor_position -= 1;
- }
- },
- Event::Key(KeyEvent {
- code: KeyCode::Delete,
- modifiers: KeyModifiers::NONE,
- }) => {
- let length = UnicodeSegmentation::graphemes(self.content.as_str(), true).count();
- if self.cursor_position != length {
- let start = UnicodeSegmentation::graphemes(self.content.as_str(), true)
- .take(self.cursor_position)
- .collect::<String>();
- let end = UnicodeSegmentation::graphemes(self.content.as_str(), true)
- .skip(self.cursor_position + 1)
- .collect::<String>();
- self.content = format!("{}{}", start, end);
- }
- },
- Event::Key(KeyEvent {
- code: KeyCode::Home,
- modifiers: KeyModifiers::NONE,
- }) => self.cursor_position = 0,
- Event::Key(KeyEvent {
- code: KeyCode::End,
- modifiers: KeyModifiers::NONE,
- }) => self.cursor_position = UnicodeSegmentation::graphemes(self.content.as_str(), true).count(),
- Event::Key(KeyEvent {
- code: KeyCode::Right,
- modifiers: KeyModifiers::NONE,
- }) => {
- let length = UnicodeSegmentation::graphemes(self.content.as_str(), true).count();
- if self.cursor_position < length {
- self.cursor_position += 1;
- }
- },
- Event::Key(KeyEvent {
- code: KeyCode::Left,
- modifiers: KeyModifiers::NONE,
- }) => {
- if self.cursor_position != 0 {
- self.cursor_position -= 1;
- }
- },
- Event::Key(KeyEvent {
- code: KeyCode::Enter,
- modifiers: KeyModifiers::NONE,
- }) => self.finished = true,
- Event::Key(KeyEvent {
- code: KeyCode::Char(c),
- modifiers: KeyModifiers::NONE | KeyModifiers::SHIFT,
- }) => {
- let start = UnicodeSegmentation::graphemes(self.content.as_str(), true)
- .take(self.cursor_position)
- .collect::<String>();
- let end = UnicodeSegmentation::graphemes(self.content.as_str(), true)
- .skip(self.cursor_position)
- .collect::<String>();
- self.content = format!("{}{}{}", start, c, end);
- self.cursor_position += 1;
- },
- _ => {},
+ if event == FINISH_EVENT {
+ self.finished = true;
+ }
+ else {
+ self.editable_line.handle_event(event);
}
}
pub(crate) fn set_label(&mut self, label: &str) {
- self.label = Some(String::from(label));
+ self.editable_line.set_label(LineSegment::new_with_color_and_style(
+ label,
+ DisplayColor::Normal,
+ true,
+ false,
+ false,
+ ));
}
pub(crate) fn set_content(&mut self, content: &str) {
- self.content = String::from(content);
- self.cursor_position = UnicodeSegmentation::graphemes(content, true).count();
+ self.editable_line.set_content(content);
}
pub(crate) fn clear(&mut self) {
- self.content.clear();
- self.cursor_position = 0;
+ self.editable_line.clear();
self.finished = false;
}
@@ -188,6 +92,6 @@ impl Edit {
}
pub(crate) fn get_content(&self) -> String {
- self.content.clone()
+ self.editable_line.get_content()
}
}
diff --git a/src/core/src/components/edit/tests.rs b/src/core/src/components/edit/tests.rs
index 9fb5e79..f491211 100644
--- a/src/core/src/components/edit/tests.rs
+++ b/src/core/src/components/edit/tests.rs
@@ -2,29 +2,6 @@ use view::assert_rendered_output;
use super::*;
-fn handle_events(module: &mut Edit, events: &[Event]) {
- for event in events {
- module.handle_event(*event);
- }
-}
-
-#[test]
-fn with_label() {
- let mut module = Edit::new();
- module.set_content("foobar");
- module.set_label("Label: ");
- let view_data = module.get_view_data();
- assert_rendered_output!(
- Options AssertRenderOptions::INCLUDE_TRAILING_WHITESPACE,
- view_data,
- "{TITLE}",
- "{BODY}",
- "{Normal,Dimmed}Label: {Normal}foobar{Normal,Underline} ",
- "{TRAILING}",
- "{IndicatorColor}Enter to finish"
- );
-}
-
#[test]
fn with_before_and_after_build() {
let mut module = Edit::new();
@@ -51,473 +28,35 @@ fn with_before_and_after_build() {
}
#[test]
-fn move_cursor_end() {
- let mut module = Edit::new();
- module.set_content("foobar");
- module.handle_event(Event::from(KeyCode::Right));
- let view_data = module.get_view_data();
- assert_rendered_output!(
- Options AssertRenderOptions::INCLUDE_TRAILING_WHITESPACE,
- view_data,
- "{TITLE}",
- "{BODY}",
- "{Normal}foobar{Normal,Underline} ",
- "{TRAILING}",
- "{IndicatorColor}Enter to finish"
- );
-}
-
-#[test]
-fn move_cursor_1_left() {
+fn edit_event() {
let mut module = Edit::new();
module.set_content("foobar");
module.handle_event(Event::from(KeyCode::Left));
let view_data = module.get_view_data();
- assert_rendered_output!(
- view_data,
- "{TITLE}",
- "{BODY}",
- "{Normal}fooba{Normal,Underline}r",
- "{TRAILING}",
- "{IndicatorColor}Enter to finish"
- );
-}
-
-#[test]
-fn move_cursor_2_from_start() {
- let mut module = Edit::new();
- module.set_content("foobar");
- handle_events(&mut module, &[Event::from(KeyCode::Left); 2]);
- let view_data = module.get_view_data();
- assert_rendered_output!(
- view_data,
- "{TITLE}",
- "{BODY}",
- "{Normal}foob{Normal,Underline}a{Normal}r",
- "{TRAILING}",
- "{IndicatorColor}Enter to finish"
- );
-}
-
-#[test]
-fn move_cursor_1_from_start() {
- let mut module = Edit::new();
- module.set_content("foobar");
- handle_events(&mut module, &[Event::from(KeyCode::Left); 5]);
- let view_data = module.get_view_data();
- assert_rendered_output!(
- view_data,
- "{TITLE}",
- "{BODY}",
- "{Normal}f{Normal,Underline}o{Normal}obar",
- "{TRAILING}",
- "{IndicatorColor}Enter to finish"
- );
-}
-
-#[test]
-fn move_cursor_to_start() {
- let mut module = Edit::new();
- module.set_content("foobar");
- handle_events(&mut module, &[Event::from(KeyCode::Left); 6]);
- let view_data = module.get_view_data();
- assert_rendered_output!(
- view_data,
- "{TITLE}",
- "{BODY}",
- "{Normal,Underline}f{Normal}oobar",
- "{TRAILING}",
- "{IndicatorColor}Enter to finish"
- );
-}
-
-#[test]
-fn move_cursor_to_home() {
- let mut module = Edit::new();
- module.set_content("foobar");
- module.handle_event(Event::from(KeyCode::Home));
- let view_data = module.get_view_data();
- assert_rendered_output!(
- view_data,
- "{TITLE}",
- "{BODY}",
- "{Normal,Underline}f{Normal}oobar",
- "{TRAILING}",
- "{IndicatorColor}Enter to finish"
- );
-}
-
-#[test]
-fn move_cursor_to_end() {
- let mut module = Edit::new();
- module.set_content("foobar");
- handle_events(&mut module, &[
- Event::from(KeyCode::Left),
- Event::from(KeyCode::Left),
- Event::from(KeyCode::Left),
- Event::from(KeyCode::End),
- ]);
- let view_data = module.get_view_data();
- assert_rendered_output!(
- Options AssertRenderOptions::INCLUDE_TRAILING_WHITESPACE,
- view_data,
- "{TITLE}",
- "{BODY}",
- "{Normal}foobar{Normal,Underline} ",
- "{TRAILING}",
- "{IndicatorColor}Enter to finish"
- );
-}
-#[test]
-fn move_cursor_on_empty_content() {
- let mut module = Edit::new();
- handle_events(&mut module, &[
- Event::from(KeyCode::Left),
- Event::from(KeyCode::Right),
- Event::from(KeyCode::End),
- Event::from(KeyCode::Home),
- ]);
- let view_data = module.get_view_data();
assert_rendered_output!(
Options AssertRenderOptions::INCLUDE_TRAILING_WHITESPACE,
view_data,
"{TITLE}",
"{BODY}",
- "{Normal,Underline} ",
- "{TRAILING}",
- "{IndicatorColor}Enter to finish"
- );
-}
-
-#[test]
-fn move_cursor_attempt_past_start() {
- let mut module = Edit::new();
- module.set_content("foobar");
- handle_events(&mut module, &[Event::from(KeyCode::Left); 10]);
- let view_data = module.get_view_data();
- assert_rendered_output!(
- view_data,
- "{TITLE}",
- "{BODY}",
- "{Normal,Underline}f{Normal}oobar",
+ "{Normal}fooba{Normal,Underline}r",
"{TRAILING}",
"{IndicatorColor}Enter to finish"
);
}
#[test]
-fn move_cursor_attempt_past_end() {
+fn finish_event() {
let mut module = Edit::new();
module.set_content("foobar");
- handle_events(&mut module, &[Event::from(KeyCode::Right); 10]);
- let view_data = module.get_view_data();
- assert_rendered_output!(
- Options AssertRenderOptions::INCLUDE_TRAILING_WHITESPACE,
- view_data,
- "{TITLE}",
- "{BODY}",
- "{Normal}foobar{Normal,Underline} ",
- "{TRAILING}",
- "{IndicatorColor}Enter to finish"
- );
-}
-
-#[test]
-fn multiple_width_unicode_single_width() {
- let mut module = Edit::new();
- module.set_content("a🗳b");
- handle_events(&mut module, &[Event::from(KeyCode::Left); 2]);
- let view_data = module.get_view_data();
- assert_rendered_output!(
- view_data,
- "{TITLE}",
- "{BODY}",
- "{Normal}a{Normal,Underline}🗳{Normal}b",
- "{TRAILING}",
- "{IndicatorColor}Enter to finish"
- );
-}
-
-#[test]
-fn multiple_width_unicode_emoji() {
- let mut module = Edit::new();
- module.set_content("a😀b");
- handle_events(&mut module, &[Event::from(KeyCode::Left); 2]);
- let view_data = module.get_view_data();
- assert_rendered_output!(
- view_data,
- "{TITLE}",
- "{BODY}",
- "{Normal}a{Normal,Underline}😀{Normal}b",
- "{TRAILING}",
- "{IndicatorColor}Enter to finish"
- );
-}
-
-#[test]
-fn add_character_end() {
- let mut module = Edit::new();
- module.set_content("abcd");
- module.handle_event(Event::from('x'));
- let view_data = module.get_view_data();
- assert_rendered_output!(
- Options AssertRenderOptions::INCLUDE_TRAILING_WHITESPACE,
- view_data,
- "{TITLE}",
- "{BODY}",
- "{Normal}abcdx{Normal,Underline} ",
- "{TRAILING}",
- "{IndicatorColor}Enter to finish"
- );
-}
-
-#[test]
-fn add_character_one_from_end() {
- let mut module = Edit::new();
- module.set_content("abcd");
- handle_events(&mut module, &[Event::from(KeyCode::Left), Event::from('x')]);
- let view_data = module.get_view_data();
- assert_rendered_output!(
- view_data,
- "{TITLE}",
- "{BODY}",
- "{Normal}abcx{Normal,Underline}d",
- "{TRAILING}",
- "{IndicatorColor}Enter to finish"
- );
-}
-
-#[test]
-fn add_character_one_from_start() {
- let mut module = Edit::new();
- module.set_content("abcd");
- handle_events(&mut module, &[
- Event::from(KeyCode::Left),
- Event::from(KeyCode::Left),
- Event::from(KeyCode::Left),
- Event::from('x'),
- ]);
- let view_data = module.get_view_data();
- assert_rendered_output!(
- view_data,
- "{TITLE}",
- "{BODY}",
- "{Normal}ax{Normal,Underline}b{Normal}cd",
- "{TRAILING}",
- "{IndicatorColor}Enter to finish"
- );
-}
-
-#[test]
-fn add_character_at_start() {
- let mut module = Edit::new();
- module.set_content("abcd");
- handle_events(&mut module, &[
- Event::from(KeyCode::Left),
- Event::from(KeyCode::Left),
- Event::from(KeyCode::Left),
- Event::from(KeyCode::Left),
- Event::from('x'),
- ]);
- let view_data = module.get_view_data();
- assert_rendered_output!(
- view_data,
- "{TITLE}",
- "{BODY}",
- "{Normal}x{Normal,Underline}a{Normal}bcd",
- "{TRAILING}",
- "{IndicatorColor}Enter to finish"
- );
-}
-
-#[test]
-fn add_character_uppercase() {
- let mut module = Edit::new();
- module.set_content("abcd");
- module.handle_event(Event::Key(KeyEvent {
- code: input::KeyCode::Char('X'),
- modifiers: input::KeyModifiers::SHIFT,
- }));
- let view_data = module.get_view_data();
- assert_rendered_output!(
- Options AssertRenderOptions::INCLUDE_TRAILING_WHITESPACE,
- view_data,
- "{TITLE}",
- "{BODY}",
- "{Normal}abcdX{Normal,Underline} ",
- "{TRAILING}",
- "{IndicatorColor}Enter to finish"
- );
-}
-
-#[test]
-fn backspace_at_end() {
- let mut module = Edit::new();
- module.set_content("abcd");
- module.handle_event(Event::from(KeyCode::Backspace));
- let view_data = module.get_view_data();
- assert_rendered_output!(
- Options AssertRenderOptions::INCLUDE_TRAILING_WHITESPACE,
- view_data,
- "{TITLE}",
- "{BODY}",
- "{Normal}abc{Normal,Underline} ",
- "{TRAILING}",
- "{IndicatorColor}Enter to finish"
- );
-}
-
-#[test]
-fn backspace_one_from_end() {
- let mut module = Edit::new();
- module.set_content("abcd");
- handle_events(&mut module, &[
- Event::from(KeyCode::Left),
- Event::from(KeyCode::Backspace),
- ]);
- let view_data = module.get_view_data();
- assert_rendered_output!(
- view_data,
- "{TITLE}",
- "{BODY}",
- "{Normal}ab{Normal,Underline}d",
- "{TRAILING}",
- "{IndicatorColor}Enter to finish"
- );
-}
-
-#[test]
-fn backspace_one_from_start() {
- let mut module = Edit::new();
- module.set_content("abcd");
- handle_events(&mut module, &[
- Event::from(KeyCode::Left),
- Event::from(KeyCode::Left),
- Event::from(KeyCode::Left),
- Event::from(KeyCode::Backspace),
- ]);
- let view_data = module.get_view_data();
- assert_rendered_output!(
- view_data,
- "{TITLE}",
- "{BODY}",
- "{Normal,Underline}b{Normal}cd",
- "{TRAILING}",
- "{IndicatorColor}Enter to finish"
- );
-}
-
-#[test]
-fn backspace_at_start() {
- let mut module = Edit::new();
- module.set_content("abcd");
- handle_events(&mut module, &[
- Event::from(KeyCode::Left),
- Event::from(KeyCode::Left),
- Event::from(KeyCode::Left),
- Event::from(KeyCode::Left),
- Event::from(KeyCode::Backspace),
- ]);
- let view_data = module.get_view_data();
- assert_rendered_output!(
- view_data,
- "{TITLE}",
- "{BODY}",
- "{Normal,Underline}a{Normal}bcd",
- "{TRAILING}",
- "{IndicatorColor}Enter to finish"
- );
-}
-
-#[test]
-fn delete_at_end() {
- let mut module = Edit::new();
- module.set_content("abcd");
- module.handle_event(Event::from(KeyCode::Delete));
- let view_data = module.get_view_data();
- assert_rendered_output!(
- Options AssertRenderOptions::INCLUDE_TRAILING_WHITESPACE,
- view_data,
- "{TITLE}",
- "{BODY}",
- "{Normal}abcd{Normal,Underline} ",
- "{TRAILING}",
- "{IndicatorColor}Enter to finish"
- );
-}
-
-#[test]
-fn delete_last_character() {
- let mut module = Edit::new();
- module.set_content("abcd");
- handle_events(&mut module, &[Event::from(KeyCode::Left), Event::from(KeyCode::Delete)]);
- let view_data = module.get_view_data();
- assert_rendered_output!(
- Options AssertRenderOptions::INCLUDE_TRAILING_WHITESPACE,
- view_data,
- "{TITLE}",
- "{BODY}",
- "{Normal}abc{Normal,Underline} ",
- "{TRAILING}",
- "{IndicatorColor}Enter to finish"
- );
-}
-
-#[test]
-fn delete_second_character() {
- let mut module = Edit::new();
- module.set_content("abcd");
- handle_events(&mut module, &[
- Event::from(KeyCode::Left),
- Event::from(KeyCode::Left),
- Event::from(KeyCode::Left),
- Event::from(KeyCode::Delete),
- ]);
- let view_data = module.get_view_data();
- assert_rendered_output!(
- view_data,
- "{TITLE}",
- "{BODY}",
- "{Normal}a{Normal,Underline}c{Normal}d",
- "{TRAILING}",
- "{IndicatorColor}Enter to finish"
- );
-}
-
-#[test]
-fn delete_first_character() {
- let mut module = Edit::new();
- module.set_content("abcd");
- handle_events(&mut module, &[
- Event::from(KeyCode::Left),
- Event::from(KeyCode::Left),
- Event::from(KeyCode::Left),
- Event::from(KeyCode::Left),
- Event::from(KeyCode::Delete),
- ]);
- let view_data = module.get_view_data();
- assert_rendered_output!(
- view_data,
- "{TITLE}",
- "{BODY}",
- "{Normal,Underline}b{Normal}cd",
- "{TRAILING}",
- "{IndicatorColor}Enter to finish"
- );
-}
-
-#[test]
-fn ignore_other_input() {
- let mut module = Edit::new();
- module.handle_event(Event::from(KeyCode::Null));
+ module.handle_event(Event::from(KeyCode::Enter));
+ assert!(module.is_finished());
}
#[test]
fn set_get_content() {
let mut module = Edit::new();
module.set_content("abcd");
- assert_eq!(module.cursor_position, 4);
assert_eq!(module.get_content(), "abcd");
}
@@ -526,6 +65,5 @@ fn clear_content() {
let mut module = Edit::new();
module.set_content("abcd");
module.clear();
- assert_eq!(module.cursor_position, 0);
assert_eq!(module.get_content(), "");
}
diff --git a/src/core/src/components/mod.rs b/src/core/src/components/mod.rs
index 1020e77..6627ff9 100644
--- a/src/core/src/components/mod.rs
+++ b/src/core/src/components/mod.rs
@@ -2,3 +2,4 @@ pub(crate) mod choice;
pub(crate) mod confirm;
pub(crate) mod edit;
pub(crate) mod help;
+mod shared;
diff --git a/src/core/src/components/shared/editable_line.rs b/src/core/src/components/shared/editable_line.rs
new file mode 100644
index 0000000..1a39f13
--- /dev/null
+++ b/src/core/src/components/shared/editable_line.rs
@@ -0,0 +1,598 @@
+use display::DisplayColor;
+use input::{Event, KeyCode, KeyEvent, KeyModifiers};
+use unicode_segmentation::UnicodeSegmentation;
+use view::LineSegment;
+
+pub(crate) struct EditableLine {
+ content: String,
+ cursor_position: usize,
+ label: Option<LineSegment>,
+}
+
+impl EditableLine {
+ pub(crate) fn new() -> Self {
+ Self {
+ content: String::from(""),
+ cursor_position: 0,
+ label: None,
+ }
+ }
+
+ pub(crate) fn set_label(&mut self, label: LineSegment) {
+ self.label = Some(label);
+ }
+
+ pub(crate) fn set_content(&mut self, content: &str) {
+ self.content = String::from(content);
+ self.cursor_position = UnicodeSegmentation::graphemes(content, true).count();
+ }
+
+ pub(crate) fn clear(&mut self) {
+ self.content.clear();
+ self.cursor_position = 0;
+ }
+
+ pub(crate) fn get_content(&self) -> String {
+ self.content.clone()
+ }
+
+ pub(crate) const fn cursor_position(&self) -> usize {
+ self.cursor_position
+ }
+
+ pub(crate) fn line_segments(&self) -> Vec<LineSegment> {
+ let line = self.content.as_str();
+ let pointer = self.cursor_position;
+
+ let graphemes = UnicodeSegmentation::graphemes(line, true);
+
+ let start = graphemes.clone().take(pointer).collect::<String>();
+ let indicator = graphemes.clone().skip(pointer).take(1).collect::<String>();
+ let end = graphemes.skip(pointer + 1).collect::<String>();
+
+ let mut segments = vec![];
+ if let Some(label) = self.label.as_ref() {
+ segments.push(label.clone());
+ }
+ if !start.is_empty() {
+ segments.push(LineSegment::new(start.as_str()));
+ }
+ segments.push(
+ if indicator.is_empty() {
+ LineSegment::new_with_color_and_style(" ", DisplayColor::Normal, false, true, false)
+ }
+ else {
+ LineSegment::new_with_color_and_style(indicator.as_str(), DisplayColor::Normal, false, true, false)
+ },
+ );
+ if !end.is_empty() {
+ segments.push(LineSegment::new(end.as_str()));
+ }
+
+ segments
+ }
+
+ pub(crate) fn handle_event(&mut self, event: Event) {
+ match event {
+ Event::Key(KeyEvent {
+ code: KeyCode::Backspace,
+ modifiers: KeyModifiers::NONE,
+ }) => {
+ if self.cursor_position != 0 {
+ let start = UnicodeSegmentation::graphemes(self.content.as_str(), true)
+ .take(self.cursor_position - 1)
+ .collect::<String>();
+ let end = UnicodeSegmentation::graphemes(self.content.as_str(), true)
+ .skip(self.cursor_position)
+ .collect::<String>();
+ self.content = format!("{}{}", start, end);
+ self.cursor_position -= 1;
+ }
+ },
+ Event::Key(KeyEvent {
+ code: KeyCode::Delete,
+ modifiers: KeyModifiers::NONE,
+ }) => {
+ let length = UnicodeSegmentation::graphemes(self.content.as_str(), true).count();
+ if self.cursor_position != length {
+ let start = UnicodeSegmentation::graphemes(self.content.as_str(), true)
+ .take(self.cursor_position)
+ .collect::<String>();
+ let end = UnicodeSegmentation::graphemes(self.content.as_str(), true)
+ .skip(self.cursor_position + 1)
+ .collect::<String>();
+ self.content = format!("{}{}", start, end);
+ }
+ },
+ Event::Key(KeyEvent {
+ code: KeyCode::Home,
+ modifiers: KeyModifiers::NONE,
+ }) => self.cursor_position = 0,
+ Event::Key(KeyEvent {
+ code: KeyCode::End,
+ modifiers: KeyModifiers::NONE,
+ }) => self.cursor_position = UnicodeSegmentation::graphemes(self.content.as_str(), true).count(),
+ Event::Key(KeyEvent {
+ code: KeyCode::Right,
+ modifiers: KeyModifiers::NONE,
+ }) => {
+ let length = UnicodeSegmentation::graphemes(self.content.as_str(), true).count();
+ if self.cursor_position < length {
+ self.cursor_position += 1;
+ }
+ },
+ Event::Key(KeyEvent {
+ code: KeyCode::Left,
+ modifiers: KeyModifiers::NONE,
+ }) => {
+ if self.cursor_position != 0 {
+ self.cursor_position -= 1;
+ }
+ },
+ Event::Key(KeyEvent {
+ code: KeyCode::Char(c),
+ modifiers: KeyModifiers::NONE | KeyModifiers::SHIFT,
+ }) => {
+ let start = UnicodeSegmentation::graphemes(self.content.as_str(), true)
+ .take(self.cursor_position)
+ .collect::<String>();
+ let end = UnicodeSegmentation::graphemes(self.content.as_str(), true)
+ .skip(self.cursor_position)
+ .collect::<String>();
+ self.content = format!("{}{}{}", start, c, end);
+ self.cursor_position += 1;
+ },
+ _ => {},
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use view::{assert_rendered_output, ViewData, ViewLine};
+
+ use super::*;
+
+ macro_rules! view_data_from_editable_line {
+ ($editable_line:expr) => {{
+ let segments = $editable_line.line_segments();
+ &ViewData::new(|updater| updater.push_line(ViewLine::from(segments)))
+ }};
+ }
+
+ fn handle_events(module: &mut EditableLine, events: &[Event]) {
+ for event in events {
+ module.handle_event(*event);
+ }
+ }
+
+ #[test]
+ fn with_label() {
+ let mut editable_line = EditableLine::new();
+ editable_line.set_content("foobar");
+ editable_line.set_label(LineSegment::new("Label: "));
+ assert_rendered_output!(
+ Options AssertRenderOptions::INCLUDE_TRAILING_WHITESPACE,
+ view_data_from_editable_line!(&editable_line),
+ "{BODY}",
+ "{Normal}Label: {Normal}foobar{Normal,Underline} "
+ );
+ }
+
+ #[test]
+ fn move_cursor_end() {
+ let mut editable_line = EditableLine::new();
+ editable_line.set_content("foobar");
+ editable_line.handle_event(Event::from(KeyCode::Right));
+ assert_rendered_output!(
+ Options AssertRenderOptions::INCLUDE_TRAILING_WHITESPACE,
+ view_data_from_editable_line!(&editable_line),
+ "{BODY}",
+ "{Normal}foobar{Normal,Underline} "
+ );
+ }
+
+ #[test]
+ fn move_cursor_1_left() {
+ let mut editable_line = EditableLine::new();
+ editable_line.set_content("foobar");
+ editable_line.handle_event(Event::from(KeyCode::Left));
+ assert_rendered_output!(
+ Options AssertRenderOptions::INCLUDE_TRAILING_WHITESPACE,
+ view_data_from_editable_line!(&editable_line),
+ "{BODY}",
+ "{Normal}fooba{Normal,Underline}r"
+ );
+ }
+
+ #[test]
+ fn move_cursor_2_from_start() {
+ let mut editable_line = EditableLine::new();
+ editable_line.set_content("foobar");
+ handle_events(&mut editable_line, &[Event::from(KeyCode::Left); 2]);
+ assert_rendered_output!(
+ Options AssertRenderOptions::INCLUDE_TRAILING_WHITESPACE,
+ view_data_from_editable_line!(&editable_line),
+ "{BODY}",
+ "{Normal}foob{Normal,Underline}a{Normal}r"
+ );
+ }
+
+ #[test]
+ fn move_cursor_1_from_start() {
+ let mut editable_line = EditableLine::new();
+ editable_line.set_content("foobar");
+ handle_events(&mut editable_line, &[Event::from(KeyCode::Left); 5]);
+ assert_rendered_output!(
+ Options AssertRenderOptions::INCLUDE_TRAILING_WHITESPACE,
+ view_data_from_editable_line!(&editable_line),
+ "{BODY}",
+ "{Normal}f{Normal,Underline}o{Normal}obar"
+ );
+ }
+
+ #[test]
+ fn move_cursor_to_start() {
+ let mut editable_line = EditableLine::new();
+ editable_line.set_content("foobar");
+ handle_events(&mut editable_line, &[Event::from(KeyCode::Left); 6]);
+ assert_rendered_output!(
+ Options AssertRenderOptions::INCLUDE_TRAILING_WHITESPACE,
+ view_data_from_editable_line!(&editable_line),
+ "{BODY}",
+ "{Normal,Underline}f{Normal}oobar"
+ );
+ }
+
+ #[test]
+ fn move_cursor_to_home() {
+ let mut editable_line = EditableLine::new();
+ editable_line.set_content("foobar");
+ editable_line.handle_event(Event::from(KeyCode::Home));
+ assert_rendered_output!(
+ Options AssertRenderOptions::INCLUDE_TRAILING_WHITESPACE,
+ view_data_from_editable_line!(&editable_line),
+ "{BODY}",
+ "{Normal,Underline}f{Normal}oobar"
+ );
+ }
+
+ #[test]
+ fn move_cursor_right() {
+ let mut editable_line = EditableLine::new();
+ editable_line.set_content("foobar");
+ handle_events(&mut editable_line, &[
+ Event::from(KeyCode::Left),
+ Event::from(KeyCode::Left),
+ Event::from(KeyCode::Left),
+ Event::from(KeyCode::Right),
+ ]);
+ assert_rendered_output!(
+ Options AssertRenderOptions::INCLUDE_TRAILING_WHITESPACE,
+ view_data_from_editable_line!(&editable_line),
+ "{BODY}",
+ "{Normal}foob{Normal,Underline}a{Normal}r"
+ );
+ }
+
+ #[test]
+ fn move_cursor_to_end() {
+ let mut editable_line = EditableLine::new();
+ editable_line.set_content("foobar");
+ handle_events(&mut editable_line, &[
+ Event::from(KeyCode::Left),
+ Event::from(KeyCode::Left),
+ Event::from(KeyCode::Left),
+ Event::from(KeyCode::End),
+ ]);
+ assert_rendered_output!(
+ Options AssertRenderOptions::INCLUDE_TRAILING_WHITESPACE,
+ view_data_from_editable_line!(&editable_line),
+ "{BODY}",
+ "{Normal}foobar{Normal,Underline} "
+ );
+ }
+
+ #[test]
+ fn move_cursor_on_empty_content() {
+ let mut editable_line = EditableLine::new();
+ handle_events(&mut editable_line, &[
+ Event::from(KeyCode::Left),
+ Event::from(KeyCode::Right),
+ Event::from(KeyCode::End),
+ Event::from(KeyCode::Home),
+ ]);
+ assert_rendered_output!(
+ Options AssertRenderOptions::INCLUDE_TRAILING_WHITESPACE,
+ view_data_from_editable_line!(&editable_line),
+ "{BODY}",
+ "{Normal,Underline} "
+ );
+ }
+
+ #[test]
+ fn move_cursor_attempt_past_start() {
+ let mut editable_line = EditableLine::new();
+ editable_line.set_content("foobar");
+ handle_events(&mut editable_line, &[Event::from(KeyCode::Left); 10]);
+ assert_rendered_output!(
+ Options AssertRenderOptions::INCLUDE_TRAILING_WHITESPACE,
+ view_data_from_editable_line!(&editable_line),
+ "{BODY}",
+ "{Normal,Underline}f{Normal}oobar"
+ );
+ }
+
+ #[test]
+ fn move_cursor_attempt_past_end() {
+ let mut editable_line = EditableLine::new();
+ editable_line.set_content("foobar");
+ handle_events(&mut editable_line, &[Event::from(KeyCode::Right); 10]);
+ assert_rendered_output!(
+ Options AssertRenderOptions::INCLUDE_TRAILING_WHITESPACE,
+ view_data_from_editable_line!(&editable_line),
+ "{BODY}",
+ "{Normal}foobar{Normal,Underline} "
+ );
+ }
+
+ #[test]
+ fn multiple_width_unicode_single_width() {
+ let mut editable_line = EditableLine::new();
+ editable_line.set_content("a🗳b");
+ handle_events(&mut editable_line, &[Event::from(KeyCode::Left); 2]);
+ assert_rendered_output!(
+ Options AssertRenderOptions::INCLUDE_TRAILING_WHITESPACE,
+ view_data_from_editable_line!(&editable_line),
+ "{BODY}",
+ "{Normal}a{Normal,Underline}🗳{Normal}b"
+ );
+ }
+
+ #[test]
+ fn multiple_width_unicode_emoji() {
+ let mut editable_line = EditableLine::new();
+ editable_line.set_content("a😀b");
+ handle_events(&mut editable_line, &[Event::from(KeyCode::Left); 2]);
+ assert_rendered_output!(
+ Options AssertRenderOptions::INCLUDE_TRAILING_WHITESPACE,
+ view_data_from_editable_line!(&editable_line),
+ "{BODY}",
+ "{Normal}a{Normal,Underline}😀{Normal}b"
+ );
+ }
+
+ #[test]
+ fn add_character_end() {
+ let mut editable_line = EditableLine::new();
+ editable_line.set_content("abcd");
+ editable_line.handle_event(Event::from('x'));
+ assert_rendered_output!(
+ Options AssertRenderOptions::INCLUDE_TRAILING_WHITESPACE,
+ view_data_from_editable_line!(&editable_line),