initial function management refactoring

This commit is contained in:
Simon Gardling 2022-04-23 15:39:40 -04:00
parent 22d1be59f5
commit 2172f3da61
11 changed files with 173 additions and 122 deletions

11
Cargo.lock generated
View File

@ -1913,6 +1913,16 @@ dependencies = [
"percent-encoding", "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]] [[package]]
name = "valuable" name = "valuable"
version = "0.1.0" version = "0.1.0"
@ -2337,6 +2347,7 @@ dependencies = [
"tracing", "tracing",
"tracing-subscriber", "tracing-subscriber",
"tracing-wasm", "tracing-wasm",
"uuid",
"wasm-bindgen", "wasm-bindgen",
"web-sys", "web-sys",
"wee_alloc", "wee_alloc",

View File

@ -47,6 +47,8 @@ tracing = "0.1.34"
itertools = "0.10.3" itertools = "0.10.3"
static_assertions = "1.1.0" static_assertions = "1.1.0"
phf = "0.10.1" phf = "0.10.1"
uuid = { version = "1.0.0", features = ["v4", "fast-rng"] }
[build-dependencies] [build-dependencies]
shadow-rs = "0.11.0" shadow-rs = "0.11.0"

32
TODO.md
View File

@ -1,18 +1,18 @@
## TODO: ## TODO:
1. Multiple functions in one graph. 1. Function management
- Backend support - Integrals between functions (too hard to implement, maybe will shelve)
- 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)
- Display intersection between functions (would have to rewrite a lot of the function plotting handling) - Sort by UUIDs
2. Rerwite of function parsing code - [Drag and drop support](https://github.com/emilk/egui/discussions/1530) in the UI
- Non `y=` functions. - Hide/disable functions
3. Smart display of graph 2. Smart display of graph
- Display of intersections between functions - Display of intersections between functions
4. Allow constants in min/max integral input (like pi or euler's number) 3. 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) 4. Sliding values for functions (like a user-interactable slider that adjusts a variable in the function, like desmos)
6. Fix integral display 5. Fix integral display
7. Better handling of panics and errors to display to the user 6. Better handling of panics and errors to display to the user
8. Turn Dynamic Iterator functions into traits 7. Turn Dynamic Iterator functions into traits
9. Better handling of roots and extrema finding 8. Better handling of roots and extrema finding
10. Add closing animation for function entry 9. Add closing animation for function entry
11. Create actual icon(s) for PWA/favicon (using placeholder from eframe_template) 10. Create actual icon(s) for PWA/favicon (using placeholder from eframe_template)
12. Fix mobile text input 11. Fix mobile text input

View File

@ -7,17 +7,18 @@
"- PI is available through 'pi' or 'π'" "- PI is available through 'pi' or 'π'"
], ],
"help_panel": [ "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 '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 on the top panel adds a new function to be graphed. You can then configure that function in the side panel.", "- The 'Add Function' button 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 'Help' button opens and closes this window!",
"- The 'Info' button provides information on the build currently running.", "- The 'Info' button provides information on the build currently running.",
"- The Sun/Moon button toggles Dark and Light mode." "- The Sun/Moon button toggles Dark and Light mode."
], ],
"help_function": [ "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.", "(From Left to Right)",
"- The ∫ button (between the '✖' and 'd/dx' buttons) indicates whether to integrate the function in question.", "- The `✖` allows you to delete the function in question. Deleting a function is prevented if only 1 function exists.",
"- The 'd/dx' button next to the gear icon indicates whether or not calculating the derivative is enabled or not.", "- The `∫` indicates whether to integrate the function in question.",
"- The gear icon next to the function input allows you to tweak settings in relation to the selected function." "- The `d/dx` toggles the calculation of derivatives.",
"- The `⚙` opens a window to tweak function options."
], ],
"help_other": [ "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.", "- 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.",

View File

@ -18,3 +18,6 @@ lazy_static = "1.4.0"
[build-dependencies] [build-dependencies]
phf_codegen = "0.10.0" phf_codegen = "0.10.0"
[package.metadata.cargo-all-features]
skip_optional_dependencies = true #don't test optional dependencies, only features

View File

@ -71,3 +71,20 @@ pub const COLORS: &[Color32; 13] = &[
Color32::DARK_GREEN, Color32::DARK_GREEN,
Color32::DARK_BLUE, Color32::DARK_BLUE,
]; ];
#[cfg(target_arch = "wasm32")]
lazy_static::lazy_static! {
pub static IS_MOBILE: bool = {
fn is_mobile() -> Option<bool> {
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;

View File

@ -1,5 +1,6 @@
#![allow(clippy::too_many_arguments)] // Clippy, shut #![allow(clippy::too_many_arguments)] // Clippy, shut
use crate::consts::IS_MOBILE;
use crate::math_app::AppSettings; use crate::math_app::AppSettings;
use crate::misc::*; use crate::misc::*;
use crate::widgets::{widgets_ontop, AutoComplete, Movement}; use crate::widgets::{widgets_ontop, AutoComplete, Movement};
@ -101,9 +102,7 @@ impl Default for FunctionEntry {
impl FunctionEntry { impl FunctionEntry {
/// Creates edit box for [`FunctionEntry`] to edit function settings and string. /// Creates edit box for [`FunctionEntry`] to edit function settings and string.
/// Returns whether or not this function was marked for removal. /// Returns whether or not this function was marked for removal.
pub fn function_entry( pub fn function_entry(&mut self, ui: &mut egui::Ui, can_remove: bool, i: usize) -> bool {
&mut self, ui: &mut egui::Ui, can_remove: bool, i: usize, mobile: bool,
) -> bool {
let output_string = self.autocomplete.string.clone(); let output_string = self.autocomplete.string.clone();
self.update_string(&output_string); self.update_string(&output_string);
@ -154,7 +153,7 @@ impl FunctionEntry {
self.autocomplete.update_string(&new_string); self.autocomplete.update_string(&new_string);
if !self.autocomplete.hint.is_none() { 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) { if ui.input().key_pressed(Key::ArrowDown) {
movement = Movement::Down; movement = Movement::Down;
} else if ui.input().key_pressed(Key::ArrowUp) { } else if ui.input().key_pressed(Key::ArrowUp) {
@ -729,7 +728,6 @@ mod tests {
do_extrema: false, do_extrema: false,
do_roots: false, do_roots: false,
plot_width: pixel_width, plot_width: pixel_width,
is_mobile: false,
} }
} }

55
src/function_manager.rs Normal file
View File

@ -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<usize> = 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 }
}

View File

@ -7,6 +7,7 @@ extern crate static_assertions;
mod consts; mod consts;
mod function_entry; mod function_entry;
mod function_manager;
mod math_app; mod math_app;
mod misc; mod misc;
mod widgets; mod widgets;

View File

@ -7,6 +7,7 @@ extern crate static_assertions;
mod consts; mod consts;
mod function_entry; mod function_entry;
mod function_manager;
mod math_app; mod math_app;
mod misc; mod misc;
mod widgets; mod widgets;

View File

@ -1,5 +1,6 @@
use crate::consts::*; 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 crate::misc::{dyn_mut_iter, option_vec_printer, JsonFileOutput, SerdeValueHelper};
use egui::style::Margin; use egui::style::Margin;
use egui::Frame; use egui::Frame;
@ -30,14 +31,6 @@ cfg_if::cfg_if! {
// Remove the element // Remove the element
loading_element.remove(); loading_element.remove();
} }
fn is_mobile() -> Option<bool> {
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 /// Stores current plot pixel width
pub plot_width: usize, pub plot_width: usize,
pub is_mobile: bool,
} }
impl Default for AppSettings { impl Default for AppSettings {
@ -85,7 +76,6 @@ impl Default for AppSettings {
do_extrema: true, do_extrema: true,
do_roots: true, do_roots: true,
plot_width: 0, plot_width: 0,
is_mobile: false,
} }
} }
} }
@ -112,7 +102,7 @@ impl Default for Opened {
/// The actual application /// The actual application
pub struct MathApp { pub struct MathApp {
/// Stores vector of functions /// Stores vector of functions
functions: Vec<FunctionEntry>, 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. /// 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<Option<f64>>, Duration), last_info: (Vec<Option<f64>>, Duration),
@ -135,19 +125,10 @@ impl MathApp {
pub fn new(cc: &eframe::CreationContext<'_>) -> Self { pub fn new(cc: &eframe::CreationContext<'_>) -> Self {
let start = instant::Instant::now(); let start = instant::Instant::now();
#[allow(unused_mut)]
#[allow(unused_assignments)]
let mut mobile = false;
// Remove loading indicator on wasm // Remove loading indicator on wasm
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
stop_loading(); stop_loading();
#[cfg(target_arch = "wasm32")]
{
mobile = is_mobile().unwrap_or_default();
}
#[cfg(threading)] #[cfg(threading)]
tracing::info!("Threading: Enabled"); tracing::info!("Threading: Enabled");
@ -271,15 +252,12 @@ impl MathApp {
tracing::info!("Initialized! Took: {:?}", start.elapsed()); tracing::info!("Initialized! Took: {:?}", start.elapsed());
Self { Self {
functions: vec![DEFAULT_FUNCTION_ENTRY.clone()], functions: Default::default(),
last_info: (vec![None], Duration::ZERO), last_info: (vec![None], Duration::ZERO),
dark_mode: true, dark_mode: true,
text: text_data.expect("text.json failed to load"), text: text_data.expect("text.json failed to load"),
opened: Opened::default(), opened: Opened::default(),
settings: AppSettings { settings: Default::default(),
is_mobile: mobile,
..AppSettings::default()
},
} }
} }
@ -292,30 +270,27 @@ impl MathApp {
.show(ctx, |ui| { .show(ctx, |ui| {
let prev_sum = self.settings.riemann_sum; let prev_sum = self.settings.riemann_sum;
// ComboBox for selecting what Riemann sum type to use // ComboBox for selecting what Riemann sum type to use
ui.add_enabled_ui( ui.add_enabled_ui(self.functions.any_using_integral(), |ui| {
self.functions.iter().filter(|func| func.integral).count() > 0, ComboBox::from_label("Riemann Sum")
|ui| { .selected_text(self.settings.riemann_sum.to_string())
ComboBox::from_label("Riemann Sum") .show_ui(ui, |ui| {
.selected_text(self.settings.riemann_sum.to_string()) ui.selectable_value(
.show_ui(ui, |ui| { &mut self.settings.riemann_sum,
ui.selectable_value( Riemann::Left,
&mut self.settings.riemann_sum, "Left",
Riemann::Left, );
"Left", ui.selectable_value(
); &mut self.settings.riemann_sum,
ui.selectable_value( Riemann::Middle,
&mut self.settings.riemann_sum, "Middle",
Riemann::Middle, );
"Middle", ui.selectable_value(
); &mut self.settings.riemann_sum,
ui.selectable_value( Riemann::Right,
&mut self.settings.riemann_sum, "Right",
Riemann::Right, );
"Right", });
); });
});
},
);
let riemann_changed = prev_sum != self.settings.riemann_sum; let riemann_changed = prev_sum != self.settings.riemann_sum;
@ -379,39 +354,26 @@ impl MathApp {
self.settings.integral_changed = self.settings.integral_changed =
max_x_changed | min_x_changed | integral_num_changed | riemann_changed; max_x_changed | min_x_changed | integral_num_changed | riemann_changed;
let can_remove = self.functions.len() > 1; self.functions.display_entries(ui);
ui.label("Functions:");
let mut remove_i: Option<usize> = None; // Only render if there's enough space
for (i, function) in self.functions.iter_mut().enumerate() { if ui.available_height() > 0.0 {
// Entry for a function ui.with_layout(egui::Layout::bottom_up(emath::Align::Min), |ui| {
if function.function_entry(ui, can_remove, i, self.settings.is_mobile) { // Contents put in reverse order from bottom to top due to the 'buttom_up' layout
remove_i = Some(i);
}
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") .on_hover_text("Create and graph new function")
.clicked() .clicked()
{ {
self.functions.push(DEFAULT_FUNCTION_ENTRY.clone()); self.functions.new_function();
} }
// Toggles opening the Help window // Toggles opening the Help window
@ -575,8 +537,9 @@ impl epi::App for MathApp {
// Display an error if it exists // Display an error if it exists
let errors_formatted: String = self let errors_formatted: String = self
.functions .functions
.get_entries()
.iter() .iter()
.map(|func| func.get_test_result()) .map(|(_, func)| func.get_test_result())
.enumerate() .enumerate()
.filter(|(_, error)| error.is_some()) .filter(|(_, error)| error.is_some())
.map(|(i, error)| { .map(|(i, error)| {
@ -612,22 +575,21 @@ impl epi::App for MathApp {
let minx_bounds: f64 = bounds.min()[0]; let minx_bounds: f64 = bounds.min()[0];
let maxx_bounds: f64 = bounds.max()[0]; let maxx_bounds: f64 = bounds.max()[0];
dyn_mut_iter(&mut self.functions) dyn_mut_iter(self.functions.get_entries_mut()).for_each(|(_, function)| {
.enumerate() function.calculate(
.for_each(|(_, function)| { &minx_bounds,
function.calculate( &maxx_bounds,
&minx_bounds, width_changed,
&maxx_bounds, &self.settings,
width_changed, )
&self.settings, });
)
});
area_list = self area_list = self
.functions .functions
.get_entries()
.iter() .iter()
.enumerate() .enumerate()
.map(|(i, function)| { .map(|(i, (_, function))| {
function.display(plot_ui, &self.settings, COLORS[i]) function.display(plot_ui, &self.settings, COLORS[i])
}) })
.collect(); .collect();