diff --git a/Cargo.lock b/Cargo.lock index 47de6ff..53a96e5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1913,6 +1913,16 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "uuid" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cfcd319456c4d6ea10087ed423473267e1a071f3bc0aa89f80d60997843c6f0" +dependencies = [ + "getrandom", + "rand", +] + [[package]] name = "valuable" version = "0.1.0" @@ -2337,6 +2347,7 @@ dependencies = [ "tracing", "tracing-subscriber", "tracing-wasm", + "uuid", "wasm-bindgen", "web-sys", "wee_alloc", diff --git a/Cargo.toml b/Cargo.toml index 11c93ab..8c655a5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,6 +47,8 @@ tracing = "0.1.34" itertools = "0.10.3" static_assertions = "1.1.0" phf = "0.10.1" +uuid = { version = "1.0.0", features = ["v4", "fast-rng"] } + [build-dependencies] shadow-rs = "0.11.0" diff --git a/TODO.md b/TODO.md index 9604d70..97e7beb 100644 --- a/TODO.md +++ b/TODO.md @@ -1,18 +1,18 @@ ## TODO: -1. Multiple functions in one graph. - - Backend support - - Integrals between functions (too hard to implement, maybe will shelve) - - Display intersection between functions (would have to rewrite a lot of the function plotting handling) -2. Rerwite of function parsing code - - Non `y=` functions. -3. Smart display of graph +1. Function management + - Integrals between functions (too hard to implement, maybe will shelve) + - Display intersection between functions (would have to rewrite a lot of the function plotting handling) + - Sort by UUIDs + - [Drag and drop support](https://github.com/emilk/egui/discussions/1530) in the UI + - Hide/disable functions +2. Smart display of graph - Display of intersections between functions -4. Allow constants in min/max integral input (like pi or euler's number) -5. Sliding values for functions (like a user-interactable slider that adjusts a variable in the function, like desmos) -6. Fix integral display -7. Better handling of panics and errors to display to the user -8. Turn Dynamic Iterator functions into traits -9. Better handling of roots and extrema finding -10. Add closing animation for function entry -11. Create actual icon(s) for PWA/favicon (using placeholder from eframe_template) -12. Fix mobile text input +3. Allow constants in min/max integral input (like pi or euler's number) +4. Sliding values for functions (like a user-interactable slider that adjusts a variable in the function, like desmos) +5. Fix integral display +6. Better handling of panics and errors to display to the user +7. Turn Dynamic Iterator functions into traits +8. Better handling of roots and extrema finding +9. Add closing animation for function entry +10. Create actual icon(s) for PWA/favicon (using placeholder from eframe_template) +11. Fix mobile text input diff --git a/assets/text.json b/assets/text.json index 26a6ca4..c27efc8 100644 --- a/assets/text.json +++ b/assets/text.json @@ -7,17 +7,18 @@ "- PI is available through 'pi' or 'π'" ], "help_panel": [ - "- The 'Panel' button on the top bar toggles if the side bar should be shown or not. This can also be accomplished by pressing the 'h' key.", - "- The 'Add Function' button on the top panel adds a new function to be graphed. You can then configure that function in the side panel.", - "- The 'Help' button on the top bar opens and closes this window!", + "- The 'Panel' button toggles if the side bar should be shown or not. This can also be accomplished by pressing the 'h' key.", + "- The 'Add Function' button adds a new function to be graphed. You can then configure that function in the side panel.", + "- The 'Help' button opens and closes this window!", "- The 'Info' button provides information on the build currently running.", "- The Sun/Moon button toggles Dark and Light mode." ], "help_function": [ - "- The '✖' button before the '∫' button allows you to delete the function in question. Deleting a function is prevented if only 1 function exists.", - "- The ∫ button (between the '✖' and 'd/dx' buttons) indicates whether to integrate the function in question.", - "- The 'd/dx' button next to the gear icon indicates whether or not calculating the derivative is enabled or not.", - "- The gear icon next to the function input allows you to tweak settings in relation to the selected function." + "(From Left to Right)", + "- The `✖` allows you to delete the function in question. Deleting a function is prevented if only 1 function exists.", + "- The `∫` indicates whether to integrate the function in question.", + "- The `d/dx` toggles the calculation of derivatives.", + "- The `⚙` opens a window to tweak function options." ], "help_other": [ "- Extrema (local minimums and maximums) and Roots (intersections with the x-axis) are displayed though yellow and light blue points on the graph. You can toggle these in the Side Panel.", diff --git a/parsing/Cargo.toml b/parsing/Cargo.toml index d171775..0a52907 100644 --- a/parsing/Cargo.toml +++ b/parsing/Cargo.toml @@ -18,3 +18,6 @@ lazy_static = "1.4.0" [build-dependencies] phf_codegen = "0.10.0" + +[package.metadata.cargo-all-features] +skip_optional_dependencies = true #don't test optional dependencies, only features diff --git a/src/consts.rs b/src/consts.rs index 492e53e..a93a4c9 100644 --- a/src/consts.rs +++ b/src/consts.rs @@ -71,3 +71,20 @@ pub const COLORS: &[Color32; 13] = &[ Color32::DARK_GREEN, Color32::DARK_BLUE, ]; + +#[cfg(target_arch = "wasm32")] +lazy_static::lazy_static! { + pub static IS_MOBILE: bool = { + fn is_mobile() -> Option { + const MOBILE_DEVICE: [&str; 6] = ["Android", "iPhone", "iPad", "iPod", "webOS", "BlackBerry"]; + + let user_agent = web_sys::window()?.navigator().user_agent().ok()?; + Some(MOBILE_DEVICE.iter().any(|&name| user_agent.contains(name))) + } + + is_mobile().unwrap_or_default() + } +} + +#[cfg(not(target_arch = "wasm32"))] +pub const IS_MOBILE: bool = false; diff --git a/src/function_entry.rs b/src/function_entry.rs index 0502097..be10690 100644 --- a/src/function_entry.rs +++ b/src/function_entry.rs @@ -1,5 +1,6 @@ #![allow(clippy::too_many_arguments)] // Clippy, shut +use crate::consts::IS_MOBILE; use crate::math_app::AppSettings; use crate::misc::*; use crate::widgets::{widgets_ontop, AutoComplete, Movement}; @@ -101,9 +102,7 @@ impl Default for FunctionEntry { impl FunctionEntry { /// Creates edit box for [`FunctionEntry`] to edit function settings and string. /// Returns whether or not this function was marked for removal. - pub fn function_entry( - &mut self, ui: &mut egui::Ui, can_remove: bool, i: usize, mobile: bool, - ) -> bool { + pub fn function_entry(&mut self, ui: &mut egui::Ui, can_remove: bool, i: usize) -> bool { let output_string = self.autocomplete.string.clone(); self.update_string(&output_string); @@ -154,7 +153,7 @@ impl FunctionEntry { self.autocomplete.update_string(&new_string); if !self.autocomplete.hint.is_none() { - if !self.autocomplete.hint.is_single() { + if !IS_MOBILE && !self.autocomplete.hint.is_single() { if ui.input().key_pressed(Key::ArrowDown) { movement = Movement::Down; } else if ui.input().key_pressed(Key::ArrowUp) { @@ -729,7 +728,6 @@ mod tests { do_extrema: false, do_roots: false, plot_width: pixel_width, - is_mobile: false, } } diff --git a/src/function_manager.rs b/src/function_manager.rs new file mode 100644 index 0000000..b985406 --- /dev/null +++ b/src/function_manager.rs @@ -0,0 +1,55 @@ +use crate::function_entry::{FunctionEntry, DEFAULT_FUNCTION_ENTRY}; +use uuid::Uuid; + +pub struct Manager { + functions: Vec<(Uuid, FunctionEntry)>, +} + +impl Default for Manager { + fn default() -> Self { + Self { + functions: vec![(Uuid::new_v4(), DEFAULT_FUNCTION_ENTRY.clone())], + } + } +} + +impl Manager { + pub fn display_entries(&mut self, ui: &mut egui::Ui) { + // ui.label("Functions:"); + let can_remove = self.functions.len() > 1; + + let mut remove_i: Option = None; + for (i, (uuid, function)) in self.functions.iter_mut().enumerate() { + // Entry for a function + if function.function_entry(ui, can_remove, i) { + remove_i = Some(i); + } + + function.settings_window(ui.ctx()); + } + + // Remove function if the user requests it + if let Some(remove_i_unwrap) = remove_i { + self.functions.remove(remove_i_unwrap); + } + } + + pub fn new_function(&mut self) { + self.functions + .push((Uuid::new_v4(), DEFAULT_FUNCTION_ENTRY.clone())); + } + + pub fn any_using_integral(&self) -> bool { + self.functions + .iter() + .filter(|(_, func)| func.integral) + .count() > 0 + } + + #[inline] + pub fn len(&self) -> usize { self.functions.len() } + + pub fn get_entries_mut(&mut self) -> &mut Vec<(Uuid, FunctionEntry)> { &mut self.functions } + + pub fn get_entries(&self) -> &Vec<(Uuid, FunctionEntry)> { &self.functions } +} diff --git a/src/lib.rs b/src/lib.rs index f27ec82..40f5c81 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,6 +7,7 @@ extern crate static_assertions; mod consts; mod function_entry; +mod function_manager; mod math_app; mod misc; mod widgets; diff --git a/src/main.rs b/src/main.rs index 659a70b..3f74f81 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,6 +7,7 @@ extern crate static_assertions; mod consts; mod function_entry; +mod function_manager; mod math_app; mod misc; mod widgets; diff --git a/src/math_app.rs b/src/math_app.rs index 596fa25..d50297f 100644 --- a/src/math_app.rs +++ b/src/math_app.rs @@ -1,5 +1,6 @@ use crate::consts::*; -use crate::function_entry::{FunctionEntry, Riemann, DEFAULT_FUNCTION_ENTRY}; +use crate::function_entry::Riemann; +use crate::function_manager::Manager; use crate::misc::{dyn_mut_iter, option_vec_printer, JsonFileOutput, SerdeValueHelper}; use egui::style::Margin; use egui::Frame; @@ -30,14 +31,6 @@ cfg_if::cfg_if! { // Remove the element loading_element.remove(); } - - fn is_mobile() -> Option { - const MOBILE_DEVICE: [&str; 6] = ["Android", "iPhone", "iPad", "iPod", "webOS", "BlackBerry"]; - - let user_agent = web_sys::window()?.navigator().user_agent().ok()?; - Some(MOBILE_DEVICE.iter().any(|&name| user_agent.contains(name))) - } - } } @@ -68,8 +61,6 @@ pub struct AppSettings { /// Stores current plot pixel width pub plot_width: usize, - - pub is_mobile: bool, } impl Default for AppSettings { @@ -85,7 +76,6 @@ impl Default for AppSettings { do_extrema: true, do_roots: true, plot_width: 0, - is_mobile: false, } } } @@ -112,7 +102,7 @@ impl Default for Opened { /// The actual application pub struct MathApp { /// Stores vector of functions - functions: Vec, + functions: Manager, /// Contains the list of Areas calculated (the vector of f64) and time it took for the last frame (the Duration). Stored in a Tuple. last_info: (Vec>, Duration), @@ -135,19 +125,10 @@ impl MathApp { pub fn new(cc: &eframe::CreationContext<'_>) -> Self { let start = instant::Instant::now(); - #[allow(unused_mut)] - #[allow(unused_assignments)] - let mut mobile = false; - // Remove loading indicator on wasm #[cfg(target_arch = "wasm32")] stop_loading(); - #[cfg(target_arch = "wasm32")] - { - mobile = is_mobile().unwrap_or_default(); - } - #[cfg(threading)] tracing::info!("Threading: Enabled"); @@ -271,15 +252,12 @@ impl MathApp { tracing::info!("Initialized! Took: {:?}", start.elapsed()); Self { - functions: vec![DEFAULT_FUNCTION_ENTRY.clone()], + functions: Default::default(), last_info: (vec![None], Duration::ZERO), dark_mode: true, text: text_data.expect("text.json failed to load"), opened: Opened::default(), - settings: AppSettings { - is_mobile: mobile, - ..AppSettings::default() - }, + settings: Default::default(), } } @@ -292,30 +270,27 @@ impl MathApp { .show(ctx, |ui| { let prev_sum = self.settings.riemann_sum; // ComboBox for selecting what Riemann sum type to use - ui.add_enabled_ui( - self.functions.iter().filter(|func| func.integral).count() > 0, - |ui| { - ComboBox::from_label("Riemann Sum") - .selected_text(self.settings.riemann_sum.to_string()) - .show_ui(ui, |ui| { - ui.selectable_value( - &mut self.settings.riemann_sum, - Riemann::Left, - "Left", - ); - ui.selectable_value( - &mut self.settings.riemann_sum, - Riemann::Middle, - "Middle", - ); - ui.selectable_value( - &mut self.settings.riemann_sum, - Riemann::Right, - "Right", - ); - }); - }, - ); + ui.add_enabled_ui(self.functions.any_using_integral(), |ui| { + ComboBox::from_label("Riemann Sum") + .selected_text(self.settings.riemann_sum.to_string()) + .show_ui(ui, |ui| { + ui.selectable_value( + &mut self.settings.riemann_sum, + Riemann::Left, + "Left", + ); + ui.selectable_value( + &mut self.settings.riemann_sum, + Riemann::Middle, + "Middle", + ); + ui.selectable_value( + &mut self.settings.riemann_sum, + Riemann::Right, + "Right", + ); + }); + }); let riemann_changed = prev_sum != self.settings.riemann_sum; @@ -379,39 +354,26 @@ impl MathApp { self.settings.integral_changed = max_x_changed | min_x_changed | integral_num_changed | riemann_changed; - let can_remove = self.functions.len() > 1; - ui.label("Functions:"); + self.functions.display_entries(ui); - let mut remove_i: Option = None; - for (i, function) in self.functions.iter_mut().enumerate() { - // Entry for a function - if function.function_entry(ui, can_remove, i, self.settings.is_mobile) { - remove_i = Some(i); - } + // Only render if there's enough space + if ui.available_height() > 0.0 { + ui.with_layout(egui::Layout::bottom_up(emath::Align::Min), |ui| { + // Contents put in reverse order from bottom to top due to the 'buttom_up' layout - function.settings_window(ctx); + // Licensing information + ui.label( + RichText::new("(and licensed under AGPLv3)").color(Color32::LIGHT_GRAY), + ) + .on_hover_text(&self.text.license_info); + + // Hyperlink to project's github + ui.hyperlink_to( + "I'm Open Source!", + "https://github.com/Titaniumtown/YTBN-Graphing-Software", + ); + }); } - - // Remove function if the user requests it - if let Some(remove_i_unwrap) = remove_i { - self.functions.remove(remove_i_unwrap); - } - - ui.with_layout(egui::Layout::bottom_up(emath::Align::Min), |ui| { - // Contents put in reverse order from bottom to top due to the 'buttom_up' layout - - // Licensing information - ui.label( - RichText::new("(and licensed under AGPLv3)").color(Color32::LIGHT_GRAY), - ) - .on_hover_text(&self.text.license_info); - - // Hyperlink to project's github - ui.hyperlink_to( - "I'm Open Source!", - "https://github.com/Titaniumtown/YTBN-Graphing-Software", - ); - }); }); } } @@ -458,7 +420,7 @@ impl epi::App for MathApp { .on_hover_text("Create and graph new function") .clicked() { - self.functions.push(DEFAULT_FUNCTION_ENTRY.clone()); + self.functions.new_function(); } // Toggles opening the Help window @@ -575,8 +537,9 @@ impl epi::App for MathApp { // Display an error if it exists let errors_formatted: String = self .functions + .get_entries() .iter() - .map(|func| func.get_test_result()) + .map(|(_, func)| func.get_test_result()) .enumerate() .filter(|(_, error)| error.is_some()) .map(|(i, error)| { @@ -612,22 +575,21 @@ impl epi::App for MathApp { let minx_bounds: f64 = bounds.min()[0]; let maxx_bounds: f64 = bounds.max()[0]; - dyn_mut_iter(&mut self.functions) - .enumerate() - .for_each(|(_, function)| { - function.calculate( - &minx_bounds, - &maxx_bounds, - width_changed, - &self.settings, - ) - }); + dyn_mut_iter(self.functions.get_entries_mut()).for_each(|(_, function)| { + function.calculate( + &minx_bounds, + &maxx_bounds, + width_changed, + &self.settings, + ) + }); area_list = self .functions + .get_entries() .iter() .enumerate() - .map(|(i, function)| { + .map(|(i, (_, function))| { function.display(plot_ui, &self.settings, COLORS[i]) }) .collect();