From 68b4eb945497f6499e789bf50625bbe52dab16c1 Mon Sep 17 00:00:00 2001 From: Simon Gardling Date: Thu, 31 Mar 2022 09:46:01 -0400 Subject: [PATCH] autocomplete fully works now --- build.rs | 7 +++- src/egui_app.rs | 3 ++ src/function.rs | 101 +++++++++++++++++++++++++++++++-------------- src/lib.rs | 2 +- src/main.rs | 1 + src/suggestions.rs | 29 +++++++++++++ 6 files changed, 110 insertions(+), 33 deletions(-) diff --git a/build.rs b/build.rs index 34d1cfe..c69c188 100644 --- a/build.rs +++ b/build.rs @@ -5,6 +5,9 @@ use std::fs::File; use std::io::{BufWriter, Write}; use std::path::Path; +/// Should build.rs generate the autocomplete hashmap to codegen.rs? +const DO_HASHMAP_GEN: bool = false; + fn main() { // rebuild if new commit or contents of `assets` folder changed println!("cargo:rerun-if-changed=.git/logs/HEAD"); @@ -15,7 +18,9 @@ fn main() { .run(); shadow_rs::new().unwrap(); - // generate_hashmap(); + if DO_HASHMAP_GEN { + generate_hashmap(); + } } fn generate_hashmap() { diff --git a/src/egui_app.rs b/src/egui_app.rs index 094c2df..ea2a0d0 100644 --- a/src/egui_app.rs +++ b/src/egui_app.rs @@ -560,6 +560,9 @@ impl epi::App for MathApp { false => Visuals::light(), }); + // if text boxes aren't in focus, allow H keybind to toggle side panel. + // this is behind this check as if it wasn't, it would trigger if the user + // presses the h key in a text box as well if !self.text_boxes_focused { self.show_side_panel .bitxor_assign(ctx.input().key_down(Key::H)); diff --git a/src/function.rs b/src/function.rs index 9800a68..d12bc77 100644 --- a/src/function.rs +++ b/src/function.rs @@ -36,6 +36,37 @@ lazy_static::lazy_static! { pub static ref DEFAULT_FUNCTION_ENTRY: FunctionEntry = FunctionEntry::default(); } +#[derive(Clone)] +struct AutoComplete { + pub i: usize, + pub hint: HintEnum<'static>, + pub func_str: Option, + pub changed: bool, +} + +impl Default for AutoComplete { + fn default() -> AutoComplete { + AutoComplete { + i: 0, + hint: HintEnum::None, + func_str: None, + changed: true, + } + } +} + +impl AutoComplete { + fn changed(&mut self, string: String) { + if self.func_str != Some(string.clone()) { + self.changed = true; + self.func_str = Some(string.clone()); + self.hint = generate_hint(string); + } else { + self.changed = false; + } + } +} + /// `FunctionEntry` is a function that can calculate values, integrals, /// derivatives, etc etc #[derive(Clone)] @@ -65,7 +96,7 @@ pub struct FunctionEntry { extrema_data: Option>, roots_data: Option>, - auto_complete_i: usize, + autocomplete: AutoComplete, } impl Default for FunctionEntry { @@ -83,7 +114,7 @@ impl Default for FunctionEntry { derivative_data: None, extrema_data: None, roots_data: None, - auto_complete_i: 0, + autocomplete: AutoComplete::default(), } } } @@ -116,18 +147,32 @@ impl FunctionEntry { } } - /// Returns whether or not the hint was applied + /// Creates and manages text box and autocompletion of function input + /// Returns whether or not the function text box is in focus pub fn auto_complete(&mut self, ui: &mut egui::Ui, string: &mut String) -> bool { + // Put here so these key presses don't interact with other elements + let consumables_pressed = ui + .input_mut() + .consume_key(egui::Modifiers::NONE, Key::Enter) + | ui.input_mut().consume_key(egui::Modifiers::NONE, Key::Tab); + let te_id = ui.make_persistent_id("text_edit_ac".to_string()); - let hint = generate_hint(string.clone()); + // update self.autocomplete + self.autocomplete.changed(string.clone()); - let mut func_edit = egui::TextEdit::singleline(string).hint_forward(true); + let mut func_edit = egui::TextEdit::singleline(string) + .hint_forward(true) + .lock_focus(true); - let hint_text = hint.get_single().unwrap_or_default(); - if !hint_text.is_empty() { + if self.autocomplete.hint.is_none() { + func_edit.id(te_id).ui(ui); + return false; + } + + if let Some(single_hint) = self.autocomplete.hint.get_single() { let func_edit_2 = func_edit; - func_edit = func_edit_2.hint_text(&hint_text); + func_edit = func_edit_2.hint_text(&single_hint); } let re = func_edit.id(te_id).ui(ui); @@ -137,61 +182,56 @@ impl FunctionEntry { // If in focus and right arrow key was pressed, apply hint if func_edit_focus { let mut push_cursor: bool = false; - let right_arrow = ui.input().key_pressed(Key::ArrowRight); + let apply_key = ui.input().key_pressed(Key::ArrowRight) | consumables_pressed; - if !hint_text.is_empty() && right_arrow { + if apply_key && let Some(single_hint) = self.autocomplete.hint.get_single() { push_cursor = true; - *string = string.clone() + &hint_text; - } else if hint.is_multi() { - let selections = match hint { - HintEnum::Many(selections) => selections, - _ => unimplemented!(), - }; + *string = string.clone() + &single_hint; + } else if self.autocomplete.hint.is_multi() { + let selections = self.autocomplete.hint.ensure_many(); let max_i = selections.len() as i16 - 1; - let mut i = self.auto_complete_i as i16; + let mut i = self.autocomplete.i as i16; if ui.input().key_pressed(Key::ArrowDown) { - if i + 1 > max_i { + i += 1; + if i > max_i { i = 0; - } else { - i += 1; } } else if ui.input().key_pressed(Key::ArrowUp) { - if 0 > i - 1 { - i = max_i; - } else { - i -= 1; + i -= 1; + if 0 > i { + i = max_i } } - self.auto_complete_i = i as usize; + self.autocomplete.i = i as usize; let popup_id = ui.make_persistent_id("autocomplete_popup"); let mut clicked = false; egui::popup_below_widget(ui, popup_id, &re, |ui| { - re.request_focus(); for (i, candidate) in selections.iter().enumerate() { if ui - .selectable_label(i == self.auto_complete_i, *candidate) + .selectable_label(i == self.autocomplete.i, *candidate) .clicked() { clicked = true; - self.auto_complete_i = i; + self.autocomplete.i = i; } } }); - if right_arrow | clicked { - *string = string.clone() + &selections[self.auto_complete_i]; + if apply_key | clicked { + *string = string.clone() + &selections[self.autocomplete.i]; push_cursor = true; } else { ui.memory().open_popup(popup_id); } } + // Push cursor to end if needed if push_cursor { let mut state = TextEdit::load_state(ui.ctx(), te_id).unwrap(); state.set_cursor_range(Some(CursorRange::one(Cursor { @@ -209,7 +249,6 @@ impl FunctionEntry { TextEdit::store_state(ui.ctx(), te_id, state); } } - return func_edit_focus; } diff --git a/src/lib.rs b/src/lib.rs index b84fa5d..6c33a93 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,5 @@ -#![allow(clippy::unused_unit)] // Fixes clippy keep complaining about wasm_bindgen #![feature(const_mut_refs)] +#![feature(let_chains)] #[macro_use] extern crate static_assertions; diff --git a/src/main.rs b/src/main.rs index d74e65d..4ab1f5c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,5 @@ #![feature(const_mut_refs)] +#![feature(let_chains)] #[macro_use] extern crate static_assertions; diff --git a/src/suggestions.rs b/src/suggestions.rs index f91deb8..f59a3c8 100644 --- a/src/suggestions.rs +++ b/src/suggestions.rs @@ -72,9 +72,38 @@ impl HintEnum<'static> { _ => false, } } + + pub fn ensure_many(&self) -> &[&str] { + match self { + HintEnum::Many(data) => data, + _ => panic!("ensure_many called on non-Many value"), + } + } + pub fn is_some(&self) -> bool { + match self { + HintEnum::None => false, + _ => true, + } + } + + pub fn is_none(&self) -> bool { !self.is_some() } + + #[allow(dead_code)] + pub fn ensure_single(&self) -> &&str { + match self { + HintEnum::Single(data) => data, + _ => panic!("ensure_single called on non-Single value"), + } + } + + #[allow(dead_code)] + pub fn is_single(&self) -> bool { !self.is_multi() } } // include!(concat!(env!("OUT_DIR"), "/codegen.rs")); + +/// Generated by build.rs +/// Manually put here so build.rs doesn't have to recalculate every time static COMPLETION_HASHMAP: phf::Map<&'static str, HintEnum> = ::phf::Map { key: 2980949210194914378, disps: &[