use unicode_width::UnicodeWidthChar;
-use super::tui::{OurKey, OurKey::*};
+use super::client::Client;
+use super::coloured_string::ColouredString;
+use super::tui::{
+ ActivityState, CursorPosition, LogicalAction,
+ OurKey, OurKey::*,
+};
struct EditorCore {
text: String,
}
}
+ fn char_width_and_bytes(&self, pos: usize) -> Option<(usize, usize)> {
+ match self.text[pos..].chars().next() {
+ None => None,
+ Some(c) => {
+ let width = UnicodeWidthChar::width(c).unwrap_or(0);
+ let mut end = pos + 1;
+ while !self.is_char_boundary(end) {
+ end += 1;
+ }
+ Some((width, end - pos))
+ },
+ }
+ }
+
fn is_word_boundary(&self, pos: usize) -> bool {
if !self.is_char_boundary(pos) {
false
Ctrl('T') => { self.forward_word(); },
Ctrl('Y') => { self.paste(); },
Pr(c) => { self.insert(&c.to_string()); },
+ Space => { self.insert(" "); },
_ => (),
}
}
pub struct SingleLineEditor {
core: EditorCore,
+ width: usize,
+ first_visible: usize,
}
impl SingleLineEditor {
point: 0,
paste_buffer: "".to_owned(),
},
+ width: 0,
+ first_visible: 0,
+ }
+ }
+
+ fn update_first_visible(&mut self) {
+ if self.first_visible > self.core.point {
+ self.first_visible = self.core.point;
+ } else {
+ let mut avail_width = self.width.saturating_sub(1);
+ let mut counted_initial_trunc_marker = false;
+ if self.first_visible > 0 {
+ counted_initial_trunc_marker = true;
+ avail_width = avail_width.saturating_sub(1);
+ }
+ let mut head = self.first_visible;
+ let mut tail = self.first_visible;
+ let mut currwidth = 0;
+ while head < self.core.point || currwidth > avail_width {
+ if currwidth <= avail_width {
+ match self.core.char_width_and_bytes(head) {
+ None => break,
+ Some((w, b)) => {
+ head += b;
+ currwidth += w;
+ },
+ }
+ } else {
+ match self.core.char_width_and_bytes(tail) {
+ None => panic!("tail should always be behind head"),
+ Some((w, b)) => {
+ tail += b;
+ currwidth -= w;
+ if !counted_initial_trunc_marker {
+ counted_initial_trunc_marker = true;
+ avail_width = avail_width.saturating_sub(1);
+ }
+ },
+ }
+ }
+ }
+ self.first_visible = tail;
+ }
+
+ // Special case: if the < indicator hides a single character
+ // at the start of the buffer which is the same width as it,
+ // we can just reveal it, which is strictly better.
+ if let Some((w, b)) = self.core.char_width_and_bytes(0) {
+ if w == 1 && b == self.first_visible {
+ self.first_visible = 0;
+ }
}
}
Return => { return true; },
_ => { self.core.handle_keypress(key); }
}
+ self.update_first_visible();
return false;
}
+
+ pub fn draw(&self, width: usize) -> (ColouredString, Option<usize>) {
+ let mut s = ColouredString::plain("");
+ if self.first_visible > 0 {
+ s.push_str(&ColouredString::uniform("<", '>').slice());
+ }
+ let mut pos = self.first_visible;
+ let mut cursor = None;
+ loop {
+ let width_so_far = s.width();
+ if pos == self.core.point {
+ cursor = Some(width_so_far);
+ }
+ match self.core.char_width_and_bytes(pos) {
+ None => break,
+ Some((w, b)) => {
+ if width_so_far + w > width ||
+ (width_so_far + w == width &&
+ pos + b < self.core.text.len())
+ {
+ s.push_str(&ColouredString::uniform(">", '>').slice());
+ break;
+ } else {
+ s.push_str(&ColouredString::plain(
+ &self.core.text[pos..pos+b]).slice());
+ pos += b;
+ }
+ }
+ }
+ }
+ (s, cursor)
+ }
+
+ pub fn resize(&mut self, width: usize) {
+ self.width = width;
+ self.update_first_visible();
+ }
+}
+
+#[test]
+fn test_single_line_extra_ops() {
+ let mut sle = SingleLineEditor {
+ core: EditorCore {
+ text: "hélło".to_owned(),
+ point: 3,
+ paste_buffer: "".to_owned(),
+ },
+ width: 0,
+ first_visible: 0,
+ };
+
+ sle.cut_to_end();
+ assert_eq!(&sle.core.text, "hé");
+ assert_eq!(sle.core.point, 3);
+ assert_eq!(sle.core.paste_buffer, "lło");
+
+ sle.core.beginning_of_buffer();
+ assert_eq!(sle.core.point, 0);
+
+ sle.core.paste();
+ assert_eq!(&sle.core.text, "lłohé");
+ assert_eq!(sle.core.point, 4);
+
+ sle.core.end_of_buffer();
+ assert_eq!(sle.core.point, 7);
+}
+
+#[test]
+fn test_single_line_visibility() {
+ let mut sle = SingleLineEditor {
+ core: EditorCore {
+ text: "".to_owned(),
+ point: 0,
+ paste_buffer: "".to_owned(),
+ },
+ width: 5,
+ first_visible: 0,
+ };
+
+ assert_eq!(sle.draw(sle.width),
+ (ColouredString::plain(""), Some(0)));
+
+ // Typing 'a' doesn't move first_visible away from the buffer start
+ sle.core.insert("a");
+ assert_eq!(sle.core.point, 1);
+ sle.update_first_visible();
+ assert_eq!(sle.first_visible, 0);
+ assert_eq!(sle.draw(sle.width),
+ (ColouredString::plain("a"), Some(1)));
+
+ // Typing three more characters leaves the cursor in the last of
+ // the 5 positions, so we're still good: we can print "abcd"
+ // followed by an empty space containing the cursor.
+ sle.core.insert("bcd");
+ assert_eq!(sle.core.point, 4);
+ sle.update_first_visible();
+ assert_eq!(sle.first_visible, 0);
+ assert_eq!(sle.draw(sle.width),
+ (ColouredString::plain("abcd"), Some(4)));
+
+ // One more character and we overflow. Now we must print "<cde"
+ // followed by the cursor.
+ sle.core.insert("e");
+ assert_eq!(sle.core.point, 5);
+ sle.update_first_visible();
+ assert_eq!(sle.first_visible, 2);
+ assert_eq!(sle.draw(sle.width),
+ (ColouredString::general("<cde", "> "), Some(4)));
+
+ // And another two characters move that on in turn: "<efg" + cursor.
+ sle.core.insert("fg");
+ assert_eq!(sle.core.point, 7);
+ sle.update_first_visible();
+ assert_eq!(sle.first_visible, 4);
+ assert_eq!(sle.draw(sle.width),
+ (ColouredString::general("<efg", "> "), Some(4)));
+
+ // Now start moving backwards. Three backwards movements leave the
+ // cursor on the e, but nothing has changed.
+ sle.core.backward();
+ sle.core.backward();
+ sle.core.backward();
+ assert_eq!(sle.core.point, 4);
+ sle.update_first_visible();
+ assert_eq!(sle.first_visible, 4);
+ assert_eq!(sle.draw(sle.width),
+ (ColouredString::general("<efg", "> "), Some(1)));
+
+ // Move backwards one more, so that we must scroll to get the d in view.
+ sle.core.backward();
+ assert_eq!(sle.core.point, 3);
+ sle.update_first_visible();
+ assert_eq!(sle.first_visible, 3);
+ assert_eq!(sle.draw(sle.width),
+ (ColouredString::general("<defg", "> "), Some(1)));
+
+ // And on the _next_ backwards scroll, the end of the string also
+ // becomes hidden.
+ sle.core.backward();
+ assert_eq!(sle.core.point, 2);
+ sle.update_first_visible();
+ assert_eq!(sle.first_visible, 2);
+ assert_eq!(sle.draw(sle.width),
+ (ColouredString::general("<cde>", "> >"), Some(1)));
+
+ // The one after that would naively leave us at "<bcd>" with the
+ // cursor on the b. But we can do better! In this case, the <
+ // marker hides just one single-width character, so we can reveal
+ // it and put first_visible right back to 0.
+ sle.core.backward();
+ assert_eq!(sle.core.point, 1);
+ sle.update_first_visible();
+ assert_eq!(sle.first_visible, 0);
+ assert_eq!(sle.draw(sle.width),
+ (ColouredString::general("abcd>", " >"), Some(1)));
+}
+
+struct BottomLineEditorOverlay {
+ prompt: ColouredString,
+ promptwidth: usize,
+ ed: SingleLineEditor,
+}
+
+impl BottomLineEditorOverlay {
+ fn new(prompt: ColouredString) -> Self {
+ let promptwidth = prompt.width();
+ BottomLineEditorOverlay {
+ prompt: prompt,
+ promptwidth,
+ ed: SingleLineEditor::new(),
+ }
+ }
+}
+
+impl ActivityState for BottomLineEditorOverlay {
+ fn resize(&mut self, w: usize, _h: usize) {
+ self.ed.resize(w.saturating_sub(self.promptwidth));
+ }
+
+ fn draw(&self, w: usize, _h: usize) ->
+ (Vec<ColouredString>, CursorPosition)
+ {
+ let (buffer, cursorpos) = self.ed.draw(
+ w.saturating_sub(self.promptwidth));
+
+ let cursorpos = match cursorpos {
+ Some(x) => CursorPosition::At(x + self.promptwidth, 0),
+ None => CursorPosition::None,
+ };
+
+ (vec! { &self.prompt + buffer }, cursorpos)
+ }
+
+ fn handle_keypress(&mut self, key: OurKey, _client: &mut Client) ->
+ LogicalAction
+ {
+ if self.ed.handle_keypress(key) {
+ LogicalAction::Beep // FIXME: do something!
+ } else {
+ LogicalAction::Nothing
+ }
+ }
+}
+
+pub fn get_user_to_examine() -> Box<dyn ActivityState> {
+ Box::new(BottomLineEditorOverlay::new(
+ ColouredString::plain("Examine User: ")))
}