From 66c56a015a60fd63245fe827a7cc5c1edc6b3e01 Mon Sep 17 00:00:00 2001 From: Simon Gardling Date: Thu, 7 Apr 2022 11:33:31 -0400 Subject: [PATCH] split up AutoComplete logic for testability --- src/suggestions.rs | 10 +-- src/widgets.rs | 212 +++++++++++++++++++++++++++++++++------------ 2 files changed, 161 insertions(+), 61 deletions(-) diff --git a/src/suggestions.rs b/src/suggestions.rs index ca47063..085ca25 100644 --- a/src/suggestions.rs +++ b/src/suggestions.rs @@ -41,16 +41,16 @@ pub fn generate_hint<'a>(input: &str) -> &'a HintEnum<'a> { #[derive(Clone, PartialEq)] pub enum HintEnum<'a> { - Single(&'static str), - Many(&'a [&'static str]), + Single(&'a str), + Many(&'a [&'a str]), None, } -impl std::fmt::Debug for HintEnum<'static> { +impl<'a> std::fmt::Debug for HintEnum<'a> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self) } } -impl std::fmt::Display for HintEnum<'static> { +impl<'a> std::fmt::Display for HintEnum<'a> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { HintEnum::Single(single_data) => { @@ -66,7 +66,7 @@ impl std::fmt::Display for HintEnum<'static> { } } -impl HintEnum<'static> { +impl<'a> HintEnum<'a> { pub fn is_some(&self) -> bool { !matches!(self, HintEnum::None) } pub fn is_none(&self) -> bool { !self.is_some() } diff --git a/src/widgets.rs b/src/widgets.rs index ce94b9a..6adf48d 100644 --- a/src/widgets.rs +++ b/src/widgets.rs @@ -1,18 +1,26 @@ use crate::suggestions::{generate_hint, HintEnum}; use eframe::{egui, epaint}; -use egui::{text::CCursor, text_edit::CursorRange, Key, Modifiers, TextEdit, Widget}; +use egui::{text::CCursor, text_edit::CursorRange, Key, Label, Modifiers, TextEdit, Widget}; use epaint::text::cursor::{Cursor, PCursor, RCursor}; #[derive(Clone)] pub struct AutoComplete<'a> { pub i: usize, - pub hint: &'a HintEnum<'static>, + pub hint: &'a HintEnum<'a>, pub func_str: Option, pub changed: bool, } -impl Default for AutoComplete<'static> { - fn default() -> AutoComplete<'static> { +#[derive(PartialEq, Debug)] +enum Movement { + Complete, + Down, + Up, + None, +} + +impl<'a> Default for AutoComplete<'a> { + fn default() -> AutoComplete<'a> { AutoComplete { i: 0, hint: &HintEnum::None, @@ -24,18 +32,61 @@ impl Default for AutoComplete<'static> { impl<'a> AutoComplete<'a> { fn changed(&mut self, string: &str) { - if self.func_str != Some(string.to_string()) { + let new_func_str = Some(string.to_string()); + if self.func_str != new_func_str { self.changed = true; - self.func_str = Some(string.to_string()); + self.func_str = new_func_str; self.hint = generate_hint(string); } else { self.changed = false; } } + fn interact_back(&mut self, new_string: &mut String, movement: &Movement) { + match self.hint { + HintEnum::Many(hints) => { + if movement == &Movement::Complete { + *new_string += hints[self.i]; + return; + } else if movement == &Movement::None { + return; + } + + let max_i = hints.len() as i16 - 1; + let mut i = self.i as i16; + + match movement { + Movement::Up => { + i -= 1; + if 0 > i { + i = max_i + } + } + Movement::Down => { + i += 1; + if i > max_i { + i = 0; + } + } + _ => {} + } + self.i = i as usize; + } + HintEnum::Single(hint) => match movement { + Movement::Complete => { + *new_string += hint; + } + _ => {} + }, + HintEnum::None => {} + } + } + pub fn ui(&mut self, ui: &mut egui::Ui, string: String, func_i: i32) -> (String, bool) { let mut new_string = string.clone(); + let mut movement: Movement = Movement::None; + // update self self.changed(&string); @@ -54,6 +105,9 @@ impl<'a> AutoComplete<'a> { // Put here so these key presses don't interact with other elements let enter_pressed = ui.input_mut().consume_key(Modifiers::NONE, Key::Enter); let tab_pressed = ui.input_mut().consume_key(Modifiers::NONE, Key::Tab); + if enter_pressed | tab_pressed | ui.input().key_pressed(Key::ArrowRight) { + movement = Movement::Complete; + } if let HintEnum::Single(single_hint) = self.hint { let func_edit_2 = func_edit; @@ -66,68 +120,56 @@ impl<'a> AutoComplete<'a> { // If in focus and right arrow key was pressed, apply hint if func_edit_focus { - let apply_key = ui.input().key_pressed(Key::ArrowRight) | enter_pressed | tab_pressed; + if ui.input().key_pressed(Key::ArrowDown) { + movement = Movement::Down; + } else if ui.input().key_pressed(Key::ArrowUp) { + movement = Movement::Up; + } - let push_cursor: bool = match self.hint { - HintEnum::Single(hint) => { - if apply_key { - new_string += hint; - true - } else { - false - } - } - HintEnum::Many(hints) => { - let max_i = hints.len() as i16 - 1; + // if movement != Movement::None { + // println!("{:?}", movement); + // } - let mut i = self.i as i16; + self.interact_back(&mut new_string, &movement); - if ui.input().key_pressed(Key::ArrowDown) { - i += 1; - if i > max_i { - i = 0; + // TODO: fix clicking on labels (no clue why it doesn't work, time to take a walk) + if movement != Movement::Complete && let HintEnum::Many(hints) = self.hint { + // Doesn't need to have a number in id as there should only be 1 autocomplete popup in the entire gui + let popup_id = ui.make_persistent_id("autocomplete_popup"); + + // let mut clicked = false; + + egui::popup_below_widget(ui, popup_id, &re, |ui| { + hints.iter().enumerate().for_each(|(i, candidate)| { + /* + if ui.selectable_label(i == self.i, *candidate).clicked() { + clicked = true; + self.i = i; } - } else if ui.input().key_pressed(Key::ArrowUp) { - i -= 1; - if 0 > i { - i = max_i - } - } + */ - self.i = i as usize; - - // Doesn't need to have a number in id as there should only be 1 autocomplete - // popup in entire gui - let popup_id = ui.make_persistent_id("autocomplete_popup"); - - let mut clicked = false; - - egui::popup_below_widget(ui, popup_id, &re, |ui| { - for (i, candidate) in hints.iter().enumerate() { - if ui.selectable_label(i == self.i, *candidate).clicked() { - clicked = true; - self.i = i; - } - } + // placeholder for now + ui.add_enabled(i == self.i, Label::new(*candidate)); }); + }); - if clicked | apply_key { - new_string += hints[self.i]; + ui.memory().open_popup(popup_id) + /* + if clicked { + new_string += hints[self.i]; - // don't need this here as it simply won't be display next frame in `math_app.rs` - // ui.memory().close_popup(); + // don't need this here as it simply won't be display next frame in `math_app.rs` + // ui.memory().close_popup(); - true - } else { - ui.memory().open_popup(popup_id); - false - } + movement = Movement::Complete; + } else { + ui.memory().open_popup(popup_id); } - _ => false, - }; + */ + } // Push cursor to end if needed - if push_cursor { + if movement == Movement::Complete { let mut state = TextEdit::load_state(ui.ctx(), te_id).unwrap(); state.set_cursor_range(Some(CursorRange::one(Cursor { ccursor: CCursor { @@ -147,3 +189,61 @@ impl<'a> AutoComplete<'a> { (new_string, func_edit_focus) } } + +#[cfg(test)] +mod tests { + use super::*; + + fn auto_complete_helper(string: &str, movement: Movement) -> (AutoComplete, String) { + let mut auto_complete = AutoComplete::default(); + auto_complete.changed(string); + let mut string_1 = String::from(string); + auto_complete.interact_back(&mut string_1, &movement); + + (auto_complete, string_1) + } + + #[test] + fn auto_complete_single_still() { + let (_, string) = auto_complete_helper("", Movement::None); + + assert_eq!(&*string, ""); + } + + #[test] + fn auto_complete_single_complete() { + let (_, string) = auto_complete_helper("", Movement::Complete); + + assert_eq!(&*string, "x^2"); + } + + #[test] + fn auto_complete_single_down() { + let (_, string) = auto_complete_helper("", Movement::Down); + + assert_eq!(&*string, ""); + } + + #[test] + fn auto_complete_single_up() { + let (_, string) = auto_complete_helper("", Movement::Up); + + assert_eq!(&*string, ""); + } + + #[test] + fn auto_complete_multi_down() { + let (auto_complete, string) = auto_complete_helper("s", Movement::Down); + + assert_eq!(auto_complete.i, 1); + assert_eq!(&*string, "s"); + } + + #[test] + fn auto_complete_multi_complete() { + let (auto_complete, string) = auto_complete_helper("s", Movement::Complete); + + assert_eq!(auto_complete.i, 0); + assert_eq!(&*string, "sin("); + } +}