split up AutoComplete logic for testability

This commit is contained in:
Simon Gardling 2022-04-07 11:33:31 -04:00
parent 95fb39715f
commit 66c56a015a
2 changed files with 161 additions and 61 deletions

View File

@ -41,16 +41,16 @@ pub fn generate_hint<'a>(input: &str) -> &'a HintEnum<'a> {
#[derive(Clone, PartialEq)] #[derive(Clone, PartialEq)]
pub enum HintEnum<'a> { pub enum HintEnum<'a> {
Single(&'static str), Single(&'a str),
Many(&'a [&'static str]), Many(&'a [&'a str]),
None, 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) } 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 { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { match self {
HintEnum::Single(single_data) => { 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_some(&self) -> bool { !matches!(self, HintEnum::None) }
pub fn is_none(&self) -> bool { !self.is_some() } pub fn is_none(&self) -> bool { !self.is_some() }

View File

@ -1,18 +1,26 @@
use crate::suggestions::{generate_hint, HintEnum}; use crate::suggestions::{generate_hint, HintEnum};
use eframe::{egui, epaint}; 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}; use epaint::text::cursor::{Cursor, PCursor, RCursor};
#[derive(Clone)] #[derive(Clone)]
pub struct AutoComplete<'a> { pub struct AutoComplete<'a> {
pub i: usize, pub i: usize,
pub hint: &'a HintEnum<'static>, pub hint: &'a HintEnum<'a>,
pub func_str: Option<String>, pub func_str: Option<String>,
pub changed: bool, pub changed: bool,
} }
impl Default for AutoComplete<'static> { #[derive(PartialEq, Debug)]
fn default() -> AutoComplete<'static> { enum Movement {
Complete,
Down,
Up,
None,
}
impl<'a> Default for AutoComplete<'a> {
fn default() -> AutoComplete<'a> {
AutoComplete { AutoComplete {
i: 0, i: 0,
hint: &HintEnum::None, hint: &HintEnum::None,
@ -24,18 +32,61 @@ impl Default for AutoComplete<'static> {
impl<'a> AutoComplete<'a> { impl<'a> AutoComplete<'a> {
fn changed(&mut self, string: &str) { 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.changed = true;
self.func_str = Some(string.to_string()); self.func_str = new_func_str;
self.hint = generate_hint(string); self.hint = generate_hint(string);
} else { } else {
self.changed = false; 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) { pub fn ui(&mut self, ui: &mut egui::Ui, string: String, func_i: i32) -> (String, bool) {
let mut new_string = string.clone(); let mut new_string = string.clone();
let mut movement: Movement = Movement::None;
// update self // update self
self.changed(&string); self.changed(&string);
@ -54,6 +105,9 @@ impl<'a> AutoComplete<'a> {
// Put here so these key presses don't interact with other elements // 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 enter_pressed = ui.input_mut().consume_key(Modifiers::NONE, Key::Enter);
let tab_pressed = ui.input_mut().consume_key(Modifiers::NONE, Key::Tab); 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 { if let HintEnum::Single(single_hint) = self.hint {
let func_edit_2 = func_edit; 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 in focus and right arrow key was pressed, apply hint
if func_edit_focus { 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 { // if movement != Movement::None {
HintEnum::Single(hint) => { // println!("{:?}", movement);
if apply_key { // }
new_string += hint;
true
} else {
false
}
}
HintEnum::Many(hints) => {
let max_i = hints.len() as i16 - 1;
let mut i = self.i as i16; self.interact_back(&mut new_string, &movement);
if ui.input().key_pressed(Key::ArrowDown) { // TODO: fix clicking on labels (no clue why it doesn't work, time to take a walk)
i += 1; if movement != Movement::Complete && let HintEnum::Many(hints) = self.hint {
if i > max_i { // Doesn't need to have a number in id as there should only be 1 autocomplete popup in the entire gui
i = 0; 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; // placeholder for now
ui.add_enabled(i == self.i, Label::new(*candidate));
// 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;
}
}
}); });
});
if clicked | apply_key { ui.memory().open_popup(popup_id)
new_string += hints[self.i]; /*
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` // don't need this here as it simply won't be display next frame in `math_app.rs`
// ui.memory().close_popup(); // ui.memory().close_popup();
true movement = Movement::Complete;
} else { } else {
ui.memory().open_popup(popup_id); ui.memory().open_popup(popup_id);
false
}
} }
_ => false, */
}; }
// Push cursor to end if needed // Push cursor to end if needed
if push_cursor { if movement == Movement::Complete {
let mut state = TextEdit::load_state(ui.ctx(), te_id).unwrap(); let mut state = TextEdit::load_state(ui.ctx(), te_id).unwrap();
state.set_cursor_range(Some(CursorRange::one(Cursor { state.set_cursor_range(Some(CursorRange::one(Cursor {
ccursor: CCursor { ccursor: CCursor {
@ -147,3 +189,61 @@ impl<'a> AutoComplete<'a> {
(new_string, func_edit_focus) (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(");
}
}