use std::ops::RangeInclusive; use crate::function::{Function, RiemannSum}; use crate::misc::{add_asterisks, digits_precision, test_func}; use eframe::{egui, epi}; use egui::plot::Plot; use egui::widgets::Button; use egui::{Color32, FontData, FontFamily, Vec2}; use git_version::git_version; use include_flate::flate; // Grabs git version on compile time const GIT_VERSION: &str = git_version!(); // Sets some hard-coded limits to the application const INTEGRAL_NUM_RANGE: RangeInclusive = 1..=100000; const INTEGRAL_X_MIN: f64 = -1000.0; const INTEGRAL_X_MAX: f64 = 1000.0; const INTEGRAL_X_RANGE: RangeInclusive = INTEGRAL_X_MIN..=INTEGRAL_X_MAX; const DEFAULT_FUNCION: &str = "x^2"; // Default function that appears when adding a new function const DEFAULT_RIEMANN: RiemannSum = RiemannSum::Left; const DEFAULT_MIN_X: f64 = -10.0; const DEFAULT_MAX_X: f64 = 10.0; const DEFAULT_INTEGRAL_NUM: usize = 100; flate!(static FONT_DATA: [u8] from "assets/Ubuntu-Light.ttf"); // Font used when displaying text // Used when displaying supported expressions in the Help window const HELP1_TEXT: &str = "- sqrt(x): square root of x - abs(x): absolute value of x - exp(x): e^x - ln(x): log with base e - log10(x): base 10 logarithm of x - log(x): same as log10(x) - sin(x): Sine of x - cos(x): Cosine of x - tan(x): Tangent of x - asin(x): arcsine of x - acos(x): arccosine of x - atan(x): arctangent of x - atan2, sinh, cosh, tanh, asinh, acosh, atanh - floor, ceil, round - signum, min, max"; // Used in the "Buttons" section of the Help window const HELP2_TEXT: &str = "- The ∫ button next to the function input indicates whether estimating an integral for that function is enabled or not. - 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 'Open Help' Button on the top bar opens and closes this window! - The 'Open Info' Button on the top bar opens and closes a window which contains info such as the estimated area for each function, and the time it took to render the last frame."; pub struct MathApp { // Stores vector of functions functions: Vec, // Stores vector containing the string representation of the functions. This is used because of hacky reasons func_strs: Vec, // Min and Max range for calculating an integral integral_min_x: f64, integral_max_x: f64, // Number of rectangles used to calculate integral integral_num: usize, // Stores whether or not the help window is open help_open: bool, // Stores whether or not the info window is open info_open: bool, // Stores font data that's used when displaying text font: FontData, // Stores the type of Rienmann sum that should be calculated sum: RiemannSum, } impl Default for MathApp { fn default() -> Self { Self { functions: vec![Function::new( String::from(DEFAULT_FUNCION), DEFAULT_MIN_X, DEFAULT_MAX_X, 100, // Doesn't matter as it will be overwritten true, // Enables integral Some(DEFAULT_MIN_X), Some(DEFAULT_MAX_X), Some(DEFAULT_INTEGRAL_NUM), Some(DEFAULT_RIEMANN), )], func_strs: vec![String::from(DEFAULT_FUNCION)], integral_min_x: DEFAULT_MIN_X, integral_max_x: DEFAULT_MAX_X, integral_num: DEFAULT_INTEGRAL_NUM, help_open: true, info_open: false, font: FontData::from_static(&FONT_DATA), sum: DEFAULT_RIEMANN, } } } impl MathApp { // Sets up fonts to use Ubuntu-Light fn init_font(&self, ctx: &egui::Context) { // Reduce size of final binary by just including one font let mut fonts = egui::FontDefinitions::default(); fonts .font_data .insert("Ubuntu-Light".to_owned(), self.font.clone()); fonts .families .insert(FontFamily::Monospace, vec!["Ubuntu-Light".to_owned()]); fonts .families .insert(FontFamily::Proportional, vec!["Ubuntu-Light".to_owned()]); ctx.set_fonts(fonts); } } impl epi::App for MathApp { // The name of the program (displayed when running natively as the window title) fn name(&self) -> &str { "Integral Demonstration" } // Called once before the first frame. fn setup( &mut self, _ctx: &egui::Context, _frame: &epi::Frame, _storage: Option<&dyn epi::Storage>, ) { } // Called each time the UI needs repainting, which may be many times per second. #[inline(always)] fn update(&mut self, ctx: &egui::Context, _frame: &epi::Frame) { // Note: This Instant implementation does not show microseconds when using wasm. let start = instant::Instant::now(); self.init_font(ctx); // Setup fonts // Creates Top bar that contains some general options egui::TopBottomPanel::top("top_bar").show(ctx, |ui| { ui.horizontal(|ui| { if ui.add(egui::Button::new("Add Function")).clicked() { self.functions.push(Function::new( String::from(DEFAULT_FUNCION), -1.0, // Doesn't matter, updated later 1.0, // Doesn't matter, updated later 100, // Doesn't matter, updated later false, None, // Doesn't matter, updated later None, // Doesn't matter, updated later None, // Doesn't matter, updated later Some(self.sum), )); self.func_strs.push(String::from(DEFAULT_FUNCION)); } if ui.add(egui::Button::new("Open Help")).clicked() { self.help_open = !self.help_open; } if ui.add(egui::Button::new("Open Info")).clicked() { self.info_open = !self.info_open; } }); }); // Cute little window that lists supported functions! egui::Window::new("Help") .default_pos([200.0, 200.0]) .open(&mut self.help_open) .resizable(false) .show(ctx, |ui| { ui.heading("Supported Expressions"); ui.label(HELP1_TEXT); ui.heading("Buttons"); ui.label(HELP2_TEXT); }); let mut parse_error: String = String::new(); // Stores errors found when interpreting input functions // Side Panel which contains vital options to the operation of the application (such as adding functions and other options) egui::SidePanel::left("side_panel") .resizable(false) .show(ctx, |ui| { egui::ComboBox::from_label("Riemann Sum Type") .selected_text(self.sum.to_string()) .show_ui(ui, |ui| { ui.selectable_value(&mut self.sum, RiemannSum::Left, "Left"); ui.selectable_value(&mut self.sum, RiemannSum::Middle, "Middle"); ui.selectable_value(&mut self.sum, RiemannSum::Right, "Right"); }); let min_x_old = self.integral_min_x; let min_x_response = ui.add(egui::Slider::new(&mut self.integral_min_x, INTEGRAL_X_RANGE.clone()).text("Min X")); let max_x_old = self.integral_max_x; let max_x_response = ui.add(egui::Slider::new(&mut self.integral_max_x, INTEGRAL_X_RANGE).text("Max X")); // Checks bounds, and if they are invalid, fix them if self.integral_min_x >= self.integral_max_x { if max_x_response.changed() { self.integral_max_x = max_x_old; } else if min_x_response.changed() { self.integral_min_x = min_x_old; } else { self.integral_min_x = -10.0; self.integral_max_x = 10.0; } } ui.add(egui::Slider::new(&mut self.integral_num, INTEGRAL_NUM_RANGE).text("Interval")); for (i, function) in self.functions.iter_mut().enumerate() { let mut integral_toggle: bool = false; // Entry for a function ui.horizontal(|ui| { ui.label("Function: "); if ui.add(Button::new("∫")).clicked() { integral_toggle = true; } ui.text_edit_singleline(&mut self.func_strs[i]); }); let integral: bool = if integral_toggle { !function.integral } else { function.integral }; if !self.func_strs[i].is_empty() { let proc_func_str = add_asterisks(self.func_strs[i].clone()); let func_test_output = test_func(proc_func_str.clone()); if let Some(test_output_value) = func_test_output { parse_error += &format!("(Function #{}) {}", i, test_output_value); } else { function.update(proc_func_str, integral, Some(self.integral_min_x), Some(self.integral_max_x), Some(self.integral_num), Some(self.sum)); } } else { function.func_str = "".to_string(); } } // Open Source and Licensing information ui.horizontal(|ui| { ui.hyperlink_to( "I'm Opensource!", "https://github.com/Titaniumtown/integral_site", ); ui.label("(and licensed under AGPLv3)").on_hover_text("The AGPL license ensures that the end user, even if not hosting the program itself, still is guaranteed access to the source code of the project in question."); }); // Displays commit info ui.horizontal(|ui| { ui.label("Commit: "); // Only include hyperlink if the build doesn't have untracked files if GIT_VERSION.contains("-modified") { // If git version is modified, don't display a link to the commit on github (as the commit will not exist) ui.label(GIT_VERSION); } else { ui.hyperlink_to( GIT_VERSION, format!( "https://github.com/Titaniumtown/integral_site/commit/{}", GIT_VERSION ), ); } }); }); let step = (self.integral_min_x - self.integral_max_x).abs() / (self.integral_num as f64); let mut area_list: Vec = Vec::new(); // Stores list of areas resulting from calculating the integral of functions // Stores the final Plot egui::CentralPanel::default().show(ctx, |ui| { if !parse_error.is_empty() { ui.label(parse_error); return; } let available_width: usize = ui.available_width() as usize; Plot::new("plot") .set_margin_fraction(Vec2::ZERO) // .view_aspect(1.0) .data_aspect(1.0) .include_y(0) .show(ui, |plot_ui| { let bounds = plot_ui.plot_bounds(); let minx_bounds: f64 = bounds.min()[0]; let maxx_bounds: f64 = bounds.max()[0]; let mut i: usize = 0; for function in self.functions.iter_mut() { if self.func_strs[i].is_empty() { continue; } function.update_bounds(minx_bounds, maxx_bounds, available_width); let (back_values, bars) = function.run(); plot_ui.line(back_values.color(Color32::RED)); if let Some(bars_data) = bars { let (bar_chart, area) = bars_data; plot_ui.bar_chart(bar_chart.color(Color32::BLUE).width(step)); area_list.push(digits_precision(area, 8)) } else { area_list.push(f64::NAN); } i += 1; } }); }); let duration = start.elapsed(); egui::Window::new("Info") .default_pos([200.0, 200.0]) .resizable(false) .collapsible(false) .open(&mut self.info_open) .show(ctx, |ui| { // Displays all areas of functions along with how long it took to complete the entire frame ui.label(format!( "Area: {:?} Took: {:?}", area_list.clone(), duration )); }); } }