diff --git a/src/function.rs b/src/function.rs index c8caacd..c14390f 100644 --- a/src/function.rs +++ b/src/function.rs @@ -1,8 +1,8 @@ #![allow(clippy::too_many_arguments)] // Clippy, shut -use crate::egui_app::AppSettings; +use crate::math_app::AppSettings; use crate::misc::*; -use crate::parsing::BackingFunction; +use crate::parsing::{process_func_str, BackingFunction}; use crate::suggestions::{generate_hint, HintEnum}; use eframe::{egui, epaint}; use egui::{ @@ -65,6 +65,116 @@ impl AutoComplete { self.changed = false; } } + + /// Creates and manages text box and autocompletion of function input + /// Returns whether or not the function text box is in focus + fn ui(&mut self, ui: &mut egui::Ui, string: &mut String) -> bool { + // Put here so these key presses don't interact with other elements + let enter_pressed = ui + .input_mut() + .consume_key(egui::Modifiers::NONE, Key::Enter); + let tab_pressed = ui.input_mut().consume_key(egui::Modifiers::NONE, Key::Tab); + + let te_id = ui.make_persistent_id("text_edit_ac".to_string()); + + // update self.autocomplete + self.changed(string.clone()); + + let mut func_edit = egui::TextEdit::singleline(string) + .hint_forward(true) + .lock_focus(true); + + if self.hint.is_none() { + func_edit.id(te_id).ui(ui); + return false; + } + + if let Some(single_hint) = self.hint.get_single() { + let func_edit_2 = func_edit; + func_edit = func_edit_2.hint_text(&single_hint); + } + + let re = func_edit.id(te_id).ui(ui); + + let func_edit_focus = re.has_focus(); + + // If in focus and right arrow key was pressed, apply hint + if func_edit_focus { + let mut push_cursor: bool = false; + let apply_key = ui.input().key_pressed(Key::ArrowRight) | enter_pressed | tab_pressed; + + if apply_key && let Some(single_hint) = self.hint.get_single() { + push_cursor = true; + *string = string.clone() + &single_hint; + } else if self.hint.is_multi() { + let selections = self.hint.ensure_many(); + + let max_i = selections.len() as i16 - 1; + + let mut i = self.i as i16; + + if ui.input().key_pressed(Key::ArrowDown) { + i += 1; + if i > max_i { + i = 0; + } + } else if ui.input().key_pressed(Key::ArrowUp) { + i -= 1; + if 0 > i { + i = max_i + } + } + + self.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| { + for (i, candidate) in selections.iter().enumerate() { + if ui + .selectable_label(i == self.i, *candidate) + .clicked() + { + clicked = true; + self.i = i; + } + } + }); + + if clicked | apply_key { + *string += selections[self.i]; + push_cursor = true; + + + // don't need this here as it simply won't be display next frame in `math_app.rs` + // ui.memory().close_popup(); + } 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 { + ccursor: CCursor { + index: 0, + prefer_next_row: false, + }, + rcursor: RCursor { row: 0, column: 0 }, + pcursor: PCursor { + paragraph: 0, + offset: 10000, + prefer_next_row: false, + }, + }))); + TextEdit::store_state(ui.ctx(), te_id, state); + } + } + func_edit_focus + } } /// `FunctionEntry` is a function that can calculate values, integrals, @@ -77,7 +187,7 @@ pub struct FunctionEntry { /// Stores a function string (that hasn't been processed via /// `process_func_str`) to display to the user - func_str: String, + raw_func_str: String, /// Minimum and Maximum values of what do display min_x: f64, @@ -104,7 +214,7 @@ impl Default for FunctionEntry { fn default() -> FunctionEntry { FunctionEntry { function: BackingFunction::new(""), - func_str: String::new(), + raw_func_str: String::new(), min_x: -1.0, max_x: 1.0, integral: false, @@ -120,17 +230,30 @@ impl Default for FunctionEntry { } impl FunctionEntry { + pub fn get_func_raw(&self) -> String { self.raw_func_str.to_string() } + + pub fn auto_complete(&mut self, ui: &mut egui::Ui, string: &mut String) -> bool { + self.autocomplete.ui(ui, string) + } /// Update function settings - pub fn update(&mut self, func_str: &str, integral: bool, derivative: bool) { - // If the function string changes, just wipe and restart from scratch - if func_str != self.func_str { - self.func_str = func_str.to_string(); - self.function = BackingFunction::new(func_str); + pub fn update( + &mut self, raw_func_str: &str, integral: bool, derivative: bool, + ) -> Option { + if raw_func_str != self.get_func_raw() { + let processed_func = process_func_str(raw_func_str); + let output = crate::parsing::test_func(&processed_func); + self.raw_func_str = raw_func_str.to_string(); + if output.is_some() { + return output; + } + + self.function = BackingFunction::new(&processed_func); self.invalidate_whole(); } self.derivative = derivative; self.integral = integral; + return None; } fn get_sum_func(&self, sum: Riemann) -> FunctionHelper { @@ -147,111 +270,6 @@ impl FunctionEntry { } } - /// 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 enter_pressed = ui - .input_mut() - .consume_key(egui::Modifiers::NONE, Key::Enter); - let tab_pressed = ui.input_mut().consume_key(egui::Modifiers::NONE, Key::Tab); - - let te_id = ui.make_persistent_id("text_edit_ac".to_string()); - - // update self.autocomplete - self.autocomplete.changed(string.clone()); - - let mut func_edit = egui::TextEdit::singleline(string) - .hint_forward(true) - .lock_focus(true); - - 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(&single_hint); - } - - let re = func_edit.id(te_id).ui(ui); - - let func_edit_focus = re.has_focus(); - - // If in focus and right arrow key was pressed, apply hint - if func_edit_focus { - let mut push_cursor: bool = false; - let apply_key = ui.input().key_pressed(Key::ArrowRight) | enter_pressed | tab_pressed; - - if apply_key && let Some(single_hint) = self.autocomplete.hint.get_single() { - push_cursor = true; - *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.autocomplete.i as i16; - - if ui.input().key_pressed(Key::ArrowDown) { - i += 1; - if i > max_i { - i = 0; - } - } else if ui.input().key_pressed(Key::ArrowUp) { - i -= 1; - if 0 > i { - i = max_i - } - } - - 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| { - for (i, candidate) in selections.iter().enumerate() { - if ui - .selectable_label(i == self.autocomplete.i, *candidate) - .clicked() - { - clicked = true; - self.autocomplete.i = 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 { - ccursor: CCursor { - index: 0, - prefer_next_row: false, - }, - rcursor: RCursor { row: 0, column: 0 }, - pcursor: PCursor { - paragraph: 0, - offset: 10000, - prefer_next_row: false, - }, - }))); - TextEdit::store_state(ui.ctx(), te_id, state); - } - } - func_edit_focus - } - /// Creates and does the math for creating all the rectangles under the /// graph fn integral_rectangles( @@ -289,9 +307,6 @@ impl FunctionEntry { (data2, area) } - /// Returns `self.func_str` - pub fn get_func_str(&self) -> &str { &self.func_str } - /// Helps with processing newton's method depending on level of derivative fn newtons_method_helper( &self, threshold: &f64, derivative_level: usize, @@ -444,7 +459,6 @@ impl FunctionEntry { /// Displays the function's output on PlotUI `plot_ui` with settings /// `settings`. Returns an `Option` of the calculated integral pub fn display(&self, plot_ui: &mut PlotUi, settings: &AppSettings) -> Option { - let func_str = self.get_func_str(); let derivative_str = self.function.get_derivative_str(); let step = (settings.integral_min_x - settings.integral_max_x).abs() / (settings.integral_num as f64); @@ -455,7 +469,7 @@ impl FunctionEntry { .clone() .to_line() .color(Color32::RED) - .name(func_str), + .name(self.get_func_raw()), ); } @@ -595,7 +609,7 @@ mod tests { sum: Riemann, integral_min_x: f64, integral_max_x: f64, pixel_width: usize, integral_num: usize, ) -> AppSettings { - crate::egui_app::AppSettings { + crate::math_app::AppSettings { riemann_sum: sum, integral_min_x, integral_max_x, diff --git a/src/lib.rs b/src/lib.rs index aebaa85..90753a0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,9 +5,9 @@ extern crate static_assertions; mod consts; -mod egui_app; mod function; mod hashmap_helper; +mod math_app; mod misc; mod parsing; mod suggestions; @@ -32,7 +32,7 @@ cfg_if::cfg_if! { tracing::info!("Finished initializing!"); tracing::info!("Starting App..."); - eframe::start_web("canvas", Box::new(|cc| Box::new(egui_app::MathApp::new(cc)))) + eframe::start_web("canvas", Box::new(|cc| Box::new(math_app::MathApp::new(cc)))) } } } diff --git a/src/main.rs b/src/main.rs index 14bab8c..ac93905 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,9 +5,9 @@ extern crate static_assertions; mod consts; -mod egui_app; mod function; mod hashmap_helper; +mod math_app; mod misc; mod parsing; mod suggestions; @@ -24,6 +24,6 @@ fn main() { eframe::run_native( "(Yet-to-be-named) Graphing Software", eframe::NativeOptions::default(), - Box::new(|cc| Box::new(egui_app::MathApp::new(cc))), + Box::new(|cc| Box::new(math_app::MathApp::new(cc))), ); } diff --git a/src/egui_app.rs b/src/math_app.rs similarity index 95% rename from src/egui_app.rs rename to src/math_app.rs index ea2a0d0..e9b54b9 100644 --- a/src/egui_app.rs +++ b/src/math_app.rs @@ -1,7 +1,6 @@ use crate::consts::*; use crate::function::{FunctionEntry, Riemann, DEFAULT_FUNCTION_ENTRY}; use crate::misc::{dyn_mut_iter, option_vec_printer, JsonFileOutput, SerdeValueHelper}; -use crate::parsing::{process_func_str, test_func}; use eframe::{egui, epi}; use egui::plot::Plot; use egui::{ @@ -450,12 +449,6 @@ impl MathApp { self.settings.integral_changed = max_x_changed | min_x_changed | integral_num_changed | riemann_changed; - // Stores whether global config options changed - let configs_changed = max_x_changed - | min_x_changed | integral_num_changed - | roots_toggled | extrema_toggled - | riemann_changed; - let functions_len = self.functions.len(); let mut remove_i: Option = None; self.text_boxes_focused = false; @@ -510,22 +503,12 @@ impl MathApp { self.exists_error = true; } - let proc_func_str = process_func_str(&self.func_strs[i]); - if configs_changed - | integral_toggle | derivative_toggle - | (proc_func_str != function.get_func_str()) - | func_failed - { - integral_enabled.bitxor_assign(integral_toggle); - derivative_enabled.bitxor_assign(derivative_toggle); + integral_enabled.bitxor_assign(integral_toggle); + derivative_enabled.bitxor_assign(derivative_toggle); - if let Some(test_output_value) = test_func(&proc_func_str) { - self.func_errors[i] = Some((i, test_output_value)); - } else { - function.update(&proc_func_str, integral_enabled, derivative_enabled); - self.func_errors[i] = None; - } - } + self.func_errors[i] = function + .update(&self.func_strs[i], integral_enabled, derivative_enabled) + .map(|error| (i, error)); } // Remove function if the user requests it @@ -535,12 +518,13 @@ impl MathApp { self.func_errors.remove(remove_i_unwrap); } - // Open Source and Licensing information + // Hyperlink to project's github ui.hyperlink_to( "I'm Open Source!", "https://github.com/Titaniumtown/YTBN-Graphing-Software", ); + // Licensing information ui.label(RichText::new("(and licensed under AGPLv3)").color(Color32::LIGHT_GRAY)) .on_hover_text(&ASSETS.text_license_info); });