diff --git a/Cargo.toml b/Cargo.toml index 549a012..4e7b256 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,11 @@ authors = ["Mieszko Grodzicki", "Aleksander Tudruj"] name = "jp2gmd" path = "src/main.rs" +[lib] +name = "jp2gmd_lib" +path = "src/lib.rs" +doctest = true + [features] fft = [] clock = [] diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..e4e73be --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,544 @@ +mod constants; +mod editor_gui; +mod env_gui; +mod environment; +#[cfg(feature = "fft")] +mod fourier; +#[cfg(feature = "clock")] +mod fractal_clock; +mod locale; +mod matrices; +mod matrix_algorithms; +mod parser; +mod rationals; +mod traits; + +#[cfg(feature = "fft")] +use crate::constants::DFT_PATH; +use crate::constants::{ + APP_NAME, DEFAULT_HEIGHT, DEFAULT_LEFT_PANEL_WIDTH, DEFAULT_WIDTH, ICON_PATH, +}; +use crate::editor_gui::{ + display_editor, set_editor_to_existing_matrix, set_editor_to_existing_scalar, + set_editor_to_matrix, set_editor_to_scalar, EditorState, +}; +use crate::environment::{Environment, Identifier, Type}; +use crate::locale::{Language, Locale}; +use crate::matrix_algorithms::Aftermath; +use crate::parser::parse_instruction; +use crate::traits::{GuiDisplayable, LaTeXable, MatrixNumber}; +use arboard::Clipboard; +use constants::{FONT_ID, TEXT_COLOR, VALUE_PADDING}; +use eframe::{egui, IconData}; + +use egui::{gui_zoom, vec2, Align2, Context, Response, Sense, Ui}; +use env_gui::insert_to_env; +use num_rational::Rational64; +use std::collections::HashMap; +use std::default::Default; +use std::time::Duration; +use traits::BoxedShape; + +#[cfg(feature = "fft")] +use crate::fourier::Fourier; +#[cfg(feature = "clock")] +use crate::fractal_clock::FractalClock; +use clap::builder::TypedValueParser; +use clap::Parser; +use egui_toast::{Toast, ToastKind, ToastOptions, Toasts}; + +pub use matrices::*; + +/// Field for matrices. +type F = Rational64; + +pub fn run_application() -> Result<(), eframe::Error> { + let options = eframe::NativeOptions { + initial_window_size: Some(vec2(DEFAULT_WIDTH, DEFAULT_HEIGHT)), + icon_data: load_icon(ICON_PATH), + ..Default::default() + }; + let args = MatrixAppArgs::parse(); + let locale = Locale::new(args.language); + eframe::run_native( + &locale.get_translated(APP_NAME), + options, + Box::new(|_cc| Box::>::new(MatrixApp::new(locale))), + ) +} + +fn load_icon(path: &str) -> Option { + let image = image::open(path).ok()?.into_rgba8(); + let (width, height) = image.dimensions(); + Some(IconData { + rgba: image.into_raw(), + width, + height, + }) +} + +#[derive(Parser, Debug)] +#[command( + author, + version, + about, + long_about = "**Just Pure 2D Graphics Matrix Display** is a powerful matrix calculator written in Rust using egui." +)] +struct MatrixAppArgs { + #[arg( + long, + default_value_t = Language::English, + value_parser = clap::builder::PossibleValuesParser::new(["English", "Polish", "Spanish"]) + .map(|s| Language::of(Some(s))), + )] + language: Language, +} + +pub struct WindowState { + is_open: bool, +} + +#[derive(Default)] +struct ShellState { + text: String, +} + +pub struct State { + env: Environment, + windows: HashMap, + shell: ShellState, + editor: EditorState, + toasts: Toasts, + clipboard: Clipboard, + #[cfg(feature = "clock")] + clock: FractalClock, + #[cfg(feature = "fft")] + fourier: Option, +} + +impl Default for State { + fn default() -> Self { + Self { + env: Default::default(), + windows: Default::default(), + shell: Default::default(), + editor: Default::default(), + toasts: Default::default(), + #[cfg(feature = "clock")] + clock: Default::default(), + clipboard: Clipboard::new().expect("Failed to create Clipboard context!"), + #[cfg(feature = "fft")] + fourier: Fourier::from_json_file(DFT_PATH.to_string()).ok(), + } + } +} + +struct MatrixApp { + state: State, + locale: Locale, +} + +impl MatrixApp { + fn new(locale: Locale) -> Self { + Self { + state: State::default(), + locale, + } + } + + // Get Translated + fn gt(&self, str: &str) -> String { + self.locale.get_translated(str) + } +} + +impl eframe::App for MatrixApp { + fn update(&mut self, ctx: &Context, frame: &mut eframe::Frame) { + if !frame.is_web() { + gui_zoom::zoom_with_keyboard_shortcuts(ctx, frame.info().native_pixels_per_point); + } + + self.state.toasts = Toasts::default() + .anchor(Align2::RIGHT_BOTTOM, (-10.0, -40.0)) + .direction(egui::Direction::BottomUp); + + let (_top_menu, new_locale) = display_menu_bar(ctx, &mut self.state, &self.locale); + display_editor::(ctx, &mut self.state, &self.locale); + + let _left_panel = egui::SidePanel::left("objects") + .resizable(true) + .default_width(DEFAULT_LEFT_PANEL_WIDTH) + .show(ctx, |ui| { + egui::trace!(ui); + ui.vertical_centered(|ui| { + ui.heading(self.gt("objects")); + }); + + ui.separator(); + + self.state.env.iter_mut().for_each(|element| { + ui.horizontal(|ui| { + display_env_element(&mut self.state.windows, ui, element, &self.locale); + }); + }); + }) + .response; + + let mut windows_result = None; + for (id, window) in self.state.windows.iter_mut() { + if window.is_open { + let element = self.state.env.get(id).unwrap(); + let local_result = display_env_element_window( + ctx, + (id, element), + &self.locale, + &mut self.state.clipboard, + &mut self.state.editor, + &mut self.state.toasts, + &mut window.is_open, + ); + windows_result = windows_result.or(local_result); + } + } + + if let Some(value) = windows_result { + insert_to_env( + &mut self.state.env, + Identifier::result(), + value, + &mut self.state.windows, + ); + } + + display_shell::(ctx, &mut self.state, &self.locale); + + // Center panel has to be added last, otherwise the side panel will be on top of it. + egui::CentralPanel::default().show(ctx, |ui| { + ui.heading(self.gt(APP_NAME)); + #[cfg(feature = "fft")] + match &mut self.state.fourier { + Some(fourier) => { + fourier.ui(ui, _left_panel.rect.width(), _top_menu.rect.height()); + } + None => { + #[cfg(feature = "clock")] + self.state.clock.ui(ui, Some(seconds_since_midnight())); + } + } + #[cfg(feature = "clock")] + #[cfg(not(feature = "fft"))] + self.state.clock.ui(ui, Some(seconds_since_midnight())); + }); + + self.state.toasts.show(ctx); + + if let Some(new_locale) = new_locale { + self.locale = new_locale + } + } +} + +#[cfg(feature = "clock")] +fn seconds_since_midnight() -> f64 { + use chrono::Timelike; + let time = chrono::Local::now().time(); + time.num_seconds_from_midnight() as f64 + 1e-9 * (time.nanosecond() as f64) +} + +fn display_menu_bar( + ctx: &Context, + state: &mut State, + locale: &Locale, +) -> (Response, Option) { + let mut new_locale = None; + ( + egui::TopBottomPanel::top("menu_bar") + .show(ctx, |ui| { + egui::menu::bar(ui, |ui| { + display_add_matrix_button(ui, state, locale); + display_add_scalar_button(ui, state, locale); + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + display_zoom_panel(ui, ctx); + ui.separator(); + new_locale = Some(display_language_panel(ui, locale)); + ui.allocate_space(ui.available_size()); + }); + }) + }) + .response, + new_locale, + ) +} + +fn display_zoom_panel(ui: &mut Ui, ctx: &Context) { + if ui.button("+").clicked() { + gui_zoom::zoom_in(ctx); + } + if ui + .button(format!("{} %", (ctx.pixels_per_point() * 100.).round())) + .clicked() + { + ctx.set_pixels_per_point(1.); + } + if ui.button("-").clicked() { + gui_zoom::zoom_out(ctx); + } +} + +fn display_language_panel(ui: &mut Ui, locale: &Locale) -> Locale { + let mut selected = locale.get_language(); + egui::ComboBox::from_label(locale.get_translated("Language")) + .selected_text(locale.get_translated_from(selected.to_string())) + .show_ui(ui, |ui| { + ui.selectable_value( + &mut selected, + Language::English, + locale.get_translated("English"), + ); + ui.selectable_value( + &mut selected, + Language::Polish, + locale.get_translated("Polish"), + ); + ui.selectable_value( + &mut selected, + Language::Spanish, + locale.get_translated("Spanish"), + ); + }); + Locale::new(selected) +} + +fn display_add_matrix_button(ui: &mut Ui, state: &mut State, locale: &Locale) { + if ui.button(locale.get_translated("Add Matrix")).clicked() { + set_editor_to_matrix(&mut state.editor, &K::zero().to_string()); + } +} + +fn display_add_scalar_button(ui: &mut Ui, state: &mut State, locale: &Locale) { + if ui.button(locale.get_translated("Add Scalar")).clicked() { + set_editor_to_scalar(&mut state.editor, &K::zero().to_string()); + } +} + +fn display_env_element( + windows: &mut HashMap, + ui: &mut Ui, + (identifier, value): (&Identifier, &mut Type), + locale: &Locale, +) { + let mut is_open = windows.get(identifier).unwrap().is_open; + ui.horizontal(|ui| { + ui.checkbox(&mut is_open, identifier.to_string()); + ui.label(value.display_string(locale)); + }); + windows.insert(identifier.clone(), WindowState { is_open }); +} + +fn display_env_element_window( + ctx: &Context, + (identifier, value): (&Identifier, &Type), + locale: &Locale, + clipboard: &mut Clipboard, + editor: &mut EditorState, + toasts: &mut Toasts, + is_open: &mut bool, +) -> Option> { + let mut window_result = None; + + egui::Window::new(identifier.to_string()) + .open(is_open) + .resizable(false) + .show(ctx, |ui| { + ui.horizontal(|ui| { + if ui.button("LaTeX").clicked() { + let latex = value.to_latex(); + set_clipboard(Ok(latex), clipboard, toasts, locale); + } + if let Type::Matrix(m) = value { + if ui.button(locale.get_translated("Echelon")).clicked() { + let echelon = match m.echelon() { + Ok(Aftermath { result, steps }) => { + window_result = Some(Type::Matrix(result)); + Ok(steps.join("\n")) + } + Err(err) => Err(err), + }; + set_clipboard(echelon, clipboard, toasts, locale); + } + } + if ui.button(locale.get_translated("Inverse")).clicked() { + let inverse = match value { + Type::Scalar(s) => match K::one().checked_div(s) { + Some(inv) => { + window_result = Some(Type::Scalar(inv.clone())); + Ok(inv.to_latex()) + } + None => Err(anyhow::Error::msg( + locale.get_translated("Failed to calculate inverse"), + )), + }, + Type::Matrix(m) => match m.inverse() { + Ok(Aftermath { result, steps }) => { + window_result = Some(Type::Matrix(result)); + Ok(steps.join("\n")) + } + Err(err) => Err(err), + }, + }; + set_clipboard(inverse, clipboard, toasts, locale); + } + if let Type::Matrix(m) = value { + if ui.button(locale.get_translated("Transpose")).clicked() { + let transpose = m.transpose(); + window_result = Some(Type::Matrix(transpose)); + } + } + }); + let mut value_shape = value.to_shape(ctx, FONT_ID, TEXT_COLOR); + let value_rect = value_shape.get_rect(); + + ui.set_min_width(value_rect.width() + 2. * VALUE_PADDING); + ui.set_max_width(ui.min_size().x); + ui.separator(); + + let bar_height = ui.min_size().y; + + ui.add_space(value_rect.height() + VALUE_PADDING); + + value_shape.translate( + ui.clip_rect().min.to_vec2() + + vec2( + (ui.min_size().x - value_rect.width()) / 2., + bar_height + VALUE_PADDING, + ), + ); + ui.painter().add(value_shape); + + if !identifier.is_result() { + ui.separator(); + if ui.button(locale.get_translated("Edit")).clicked() { + match value { + Type::Scalar(s) => { + set_editor_to_existing_scalar(editor, s, identifier.to_string()) + } + Type::Matrix(m) => { + set_editor_to_existing_matrix(editor, m, identifier.to_string()) + } + } + } + }; + }); + + window_result +} + +fn set_clipboard( + message: anyhow::Result, + clipboard: &mut Clipboard, + toasts: &mut Toasts, + locale: &Locale, +) { + const CLIPBOARD_TOAST_DURATION: Duration = Duration::from_secs(5); + match message { + Ok(latex) => match clipboard.set_text(latex) { + Ok(_) => { + toasts_info( + toasts, + locale.get_translated("LaTeX copied to clipboard"), + CLIPBOARD_TOAST_DURATION, + ); + } + Err(e) => { + toasts_error( + toasts, + locale.get_translated("Failed to copy LaTeX to clipboard") + + "\n" + + e.to_string().as_str(), + CLIPBOARD_TOAST_DURATION, + ); + } + }, + Err(e) => { + toasts_error( + toasts, + locale.get_translated("Failed to generate LaTeX") + "\n" + e.to_string().as_str(), + CLIPBOARD_TOAST_DURATION, + ); + } + } +} + +fn display_shell( + ctx: &Context, + State { + shell, + env, + windows, + toasts, + .. + }: &mut State, + locale: &Locale, +) { + let mut run_shell_command = |shell_text: &mut String| match parse_instruction(shell_text, env) { + Ok(identifier) => { + shell_text.clear(); + windows.insert(identifier, WindowState { is_open: true }); + } + Err(error) => { + println!("{error}"); + toasts_error(toasts, error.to_string(), Duration::from_secs(5)); + } + }; + + egui::TopBottomPanel::bottom("shell") + .resizable(false) + .default_height(128.0) + .show(ctx, |ui| { + let button_sense = if shell.text.is_empty() { + Sense::hover() + } else { + Sense::click() + }; + + ui.horizontal(|ui| { + ui.with_layout(egui::Layout::right_to_left(egui::Align::BOTTOM), |ui| { + if ui + .add(egui::Button::new(locale.get_translated("Run")).sense(button_sense)) + .clicked() + { + run_shell_command(&mut shell.text); + } + + let response = ui.add( + egui::TextEdit::singleline(&mut shell.text) + .desired_rows(1) + .desired_width(ui.available_width()) + .code_editor(), + ); + if response.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)) { + run_shell_command(&mut shell.text); + response.request_focus(); + } + }); + }); + }); +} + +fn toasts_add_kind(toasts: &mut Toasts, text: String, duration: Duration, kind: ToastKind) { + toasts.add(Toast { + text: text.into(), + kind, + options: ToastOptions::default() + .duration(duration) + .show_progress(true), + }); +} + +fn toasts_info(toasts: &mut Toasts, text: String, duration: Duration) { + toasts_add_kind(toasts, text, duration, ToastKind::Info); +} + +fn toasts_error(toasts: &mut Toasts, text: String, duration: Duration) { + toasts_add_kind(toasts, text, duration, ToastKind::Error); +} diff --git a/src/main.rs b/src/main.rs index 7233333..f3f341d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,542 +1,3 @@ -mod constants; -mod editor_gui; -mod env_gui; -mod environment; -#[cfg(feature = "fft")] -mod fourier; -#[cfg(feature = "clock")] -mod fractal_clock; -mod locale; -mod matrices; -mod matrix_algorithms; -mod parser; -mod rationals; -mod traits; - -#[cfg(feature = "fft")] -use crate::constants::DFT_PATH; -use crate::constants::{ - APP_NAME, DEFAULT_HEIGHT, DEFAULT_LEFT_PANEL_WIDTH, DEFAULT_WIDTH, ICON_PATH, -}; -use crate::editor_gui::{ - display_editor, set_editor_to_existing_matrix, set_editor_to_existing_scalar, - set_editor_to_matrix, set_editor_to_scalar, EditorState, -}; -use crate::environment::{Environment, Identifier, Type}; -use crate::locale::{Language, Locale}; -use crate::matrix_algorithms::Aftermath; -use crate::parser::parse_instruction; -use crate::traits::{GuiDisplayable, LaTeXable, MatrixNumber}; -use arboard::Clipboard; -use constants::{FONT_ID, TEXT_COLOR, VALUE_PADDING}; -use eframe::{egui, IconData}; - -use egui::{gui_zoom, vec2, Align2, Context, Response, Sense, Ui}; -use env_gui::insert_to_env; -use num_rational::Rational64; -use std::collections::HashMap; -use std::default::Default; -use std::time::Duration; -use traits::BoxedShape; - -#[cfg(feature = "fft")] -use crate::fourier::Fourier; -#[cfg(feature = "clock")] -use crate::fractal_clock::FractalClock; -use clap::builder::TypedValueParser; -use clap::Parser; -use egui_toast::{Toast, ToastKind, ToastOptions, Toasts}; - -/// Field for matrices. -type F = Rational64; - fn main() -> Result<(), eframe::Error> { - let options = eframe::NativeOptions { - initial_window_size: Some(vec2(DEFAULT_WIDTH, DEFAULT_HEIGHT)), - icon_data: load_icon(ICON_PATH), - ..Default::default() - }; - let args = MatrixAppArgs::parse(); - let locale = Locale::new(args.language); - eframe::run_native( - &locale.get_translated(APP_NAME), - options, - Box::new(|_cc| Box::>::new(MatrixApp::new(locale))), - ) -} - -fn load_icon(path: &str) -> Option { - let image = image::open(path).ok()?.into_rgba8(); - let (width, height) = image.dimensions(); - Some(IconData { - rgba: image.into_raw(), - width, - height, - }) -} - -#[derive(Parser, Debug)] -#[command( - author, - version, - about, - long_about = "**Just Pure 2D Graphics Matrix Display** is a powerful matrix calculator written in Rust using egui." -)] -struct MatrixAppArgs { - #[arg( - long, - default_value_t = Language::English, - value_parser = clap::builder::PossibleValuesParser::new(["English", "Polish", "Spanish"]) - .map(|s| Language::of(Some(s))), - )] - language: Language, -} - -pub struct WindowState { - is_open: bool, -} - -#[derive(Default)] -struct ShellState { - text: String, -} - -pub struct State { - env: Environment, - windows: HashMap, - shell: ShellState, - editor: EditorState, - toasts: Toasts, - clipboard: Clipboard, - #[cfg(feature = "clock")] - clock: FractalClock, - #[cfg(feature = "fft")] - fourier: Option, -} - -impl Default for State { - fn default() -> Self { - Self { - env: Default::default(), - windows: Default::default(), - shell: Default::default(), - editor: Default::default(), - toasts: Default::default(), - #[cfg(feature = "clock")] - clock: Default::default(), - clipboard: Clipboard::new().expect("Failed to create Clipboard context!"), - #[cfg(feature = "fft")] - fourier: Fourier::from_json_file(DFT_PATH.to_string()).ok(), - } - } -} - -struct MatrixApp { - state: State, - locale: Locale, -} - -impl MatrixApp { - fn new(locale: Locale) -> Self { - Self { - state: State::default(), - locale, - } - } - - // Get Translated - fn gt(&self, str: &str) -> String { - self.locale.get_translated(str) - } -} - -impl eframe::App for MatrixApp { - fn update(&mut self, ctx: &Context, frame: &mut eframe::Frame) { - if !frame.is_web() { - gui_zoom::zoom_with_keyboard_shortcuts(ctx, frame.info().native_pixels_per_point); - } - - self.state.toasts = Toasts::default() - .anchor(Align2::RIGHT_BOTTOM, (-10.0, -40.0)) - .direction(egui::Direction::BottomUp); - - let (_top_menu, new_locale) = display_menu_bar(ctx, &mut self.state, &self.locale); - display_editor::(ctx, &mut self.state, &self.locale); - - let _left_panel = egui::SidePanel::left("objects") - .resizable(true) - .default_width(DEFAULT_LEFT_PANEL_WIDTH) - .show(ctx, |ui| { - egui::trace!(ui); - ui.vertical_centered(|ui| { - ui.heading(self.gt("objects")); - }); - - ui.separator(); - - self.state.env.iter_mut().for_each(|element| { - ui.horizontal(|ui| { - display_env_element(&mut self.state.windows, ui, element, &self.locale); - }); - }); - }) - .response; - - let mut windows_result = None; - for (id, window) in self.state.windows.iter_mut() { - if window.is_open { - let element = self.state.env.get(id).unwrap(); - let local_result = display_env_element_window( - ctx, - (id, element), - &self.locale, - &mut self.state.clipboard, - &mut self.state.editor, - &mut self.state.toasts, - &mut window.is_open, - ); - windows_result = windows_result.or(local_result); - } - } - - if let Some(value) = windows_result { - insert_to_env( - &mut self.state.env, - Identifier::result(), - value, - &mut self.state.windows, - ); - } - - display_shell::(ctx, &mut self.state, &self.locale); - - // Center panel has to be added last, otherwise the side panel will be on top of it. - egui::CentralPanel::default().show(ctx, |ui| { - ui.heading(self.gt(APP_NAME)); - #[cfg(feature = "fft")] - match &mut self.state.fourier { - Some(fourier) => { - fourier.ui(ui, _left_panel.rect.width(), _top_menu.rect.height()); - } - None => { - #[cfg(feature = "clock")] - self.state.clock.ui(ui, Some(seconds_since_midnight())); - } - } - #[cfg(feature = "clock")] - #[cfg(not(feature = "fft"))] - self.state.clock.ui(ui, Some(seconds_since_midnight())); - }); - - self.state.toasts.show(ctx); - - if let Some(new_locale) = new_locale { - self.locale = new_locale - } - } -} - -#[cfg(feature = "clock")] -fn seconds_since_midnight() -> f64 { - use chrono::Timelike; - let time = chrono::Local::now().time(); - time.num_seconds_from_midnight() as f64 + 1e-9 * (time.nanosecond() as f64) -} - -fn display_menu_bar( - ctx: &Context, - state: &mut State, - locale: &Locale, -) -> (Response, Option) { - let mut new_locale = None; - ( - egui::TopBottomPanel::top("menu_bar") - .show(ctx, |ui| { - egui::menu::bar(ui, |ui| { - display_add_matrix_button(ui, state, locale); - display_add_scalar_button(ui, state, locale); - ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - display_zoom_panel(ui, ctx); - ui.separator(); - new_locale = Some(display_language_panel(ui, locale)); - ui.allocate_space(ui.available_size()); - }); - }) - }) - .response, - new_locale, - ) -} - -fn display_zoom_panel(ui: &mut Ui, ctx: &Context) { - if ui.button("+").clicked() { - gui_zoom::zoom_in(ctx); - } - if ui - .button(format!("{} %", (ctx.pixels_per_point() * 100.).round())) - .clicked() - { - ctx.set_pixels_per_point(1.); - } - if ui.button("-").clicked() { - gui_zoom::zoom_out(ctx); - } -} - -fn display_language_panel(ui: &mut Ui, locale: &Locale) -> Locale { - let mut selected = locale.get_language(); - egui::ComboBox::from_label(locale.get_translated("Language")) - .selected_text(locale.get_translated_from(selected.to_string())) - .show_ui(ui, |ui| { - ui.selectable_value( - &mut selected, - Language::English, - locale.get_translated("English"), - ); - ui.selectable_value( - &mut selected, - Language::Polish, - locale.get_translated("Polish"), - ); - ui.selectable_value( - &mut selected, - Language::Spanish, - locale.get_translated("Spanish"), - ); - }); - Locale::new(selected) -} - -fn display_add_matrix_button(ui: &mut Ui, state: &mut State, locale: &Locale) { - if ui.button(locale.get_translated("Add Matrix")).clicked() { - set_editor_to_matrix(&mut state.editor, &K::zero().to_string()); - } -} - -fn display_add_scalar_button(ui: &mut Ui, state: &mut State, locale: &Locale) { - if ui.button(locale.get_translated("Add Scalar")).clicked() { - set_editor_to_scalar(&mut state.editor, &K::zero().to_string()); - } -} - -fn display_env_element( - windows: &mut HashMap, - ui: &mut Ui, - (identifier, value): (&Identifier, &mut Type), - locale: &Locale, -) { - let mut is_open = windows.get(identifier).unwrap().is_open; - ui.horizontal(|ui| { - ui.checkbox(&mut is_open, identifier.to_string()); - ui.label(value.display_string(locale)); - }); - windows.insert(identifier.clone(), WindowState { is_open }); -} - -fn display_env_element_window( - ctx: &Context, - (identifier, value): (&Identifier, &Type), - locale: &Locale, - clipboard: &mut Clipboard, - editor: &mut EditorState, - toasts: &mut Toasts, - is_open: &mut bool, -) -> Option> { - let mut window_result = None; - - egui::Window::new(identifier.to_string()) - .open(is_open) - .resizable(false) - .show(ctx, |ui| { - ui.horizontal(|ui| { - if ui.button("LaTeX").clicked() { - let latex = value.to_latex(); - set_clipboard(Ok(latex), clipboard, toasts, locale); - } - if let Type::Matrix(m) = value { - if ui.button(locale.get_translated("Echelon")).clicked() { - let echelon = match m.echelon() { - Ok(Aftermath { result, steps }) => { - window_result = Some(Type::Matrix(result)); - Ok(steps.join("\n")) - } - Err(err) => Err(err), - }; - set_clipboard(echelon, clipboard, toasts, locale); - } - } - if ui.button(locale.get_translated("Inverse")).clicked() { - let inverse = match value { - Type::Scalar(s) => match K::one().checked_div(s) { - Some(inv) => { - window_result = Some(Type::Scalar(inv.clone())); - Ok(inv.to_latex()) - } - None => Err(anyhow::Error::msg( - locale.get_translated("Failed to calculate inverse"), - )), - }, - Type::Matrix(m) => match m.inverse() { - Ok(Aftermath { result, steps }) => { - window_result = Some(Type::Matrix(result)); - Ok(steps.join("\n")) - } - Err(err) => Err(err), - }, - }; - set_clipboard(inverse, clipboard, toasts, locale); - } - if let Type::Matrix(m) = value { - if ui.button(locale.get_translated("Transpose")).clicked() { - let transpose = m.transpose(); - window_result = Some(Type::Matrix(transpose)); - } - } - }); - let mut value_shape = value.to_shape(ctx, FONT_ID, TEXT_COLOR); - let value_rect = value_shape.get_rect(); - - ui.set_min_width(value_rect.width() + 2. * VALUE_PADDING); - ui.set_max_width(ui.min_size().x); - ui.separator(); - - let bar_height = ui.min_size().y; - - ui.add_space(value_rect.height() + VALUE_PADDING); - - value_shape.translate( - ui.clip_rect().min.to_vec2() - + vec2( - (ui.min_size().x - value_rect.width()) / 2., - bar_height + VALUE_PADDING, - ), - ); - ui.painter().add(value_shape); - - if !identifier.is_result() { - ui.separator(); - if ui.button(locale.get_translated("Edit")).clicked() { - match value { - Type::Scalar(s) => { - set_editor_to_existing_scalar(editor, s, identifier.to_string()) - } - Type::Matrix(m) => { - set_editor_to_existing_matrix(editor, m, identifier.to_string()) - } - } - } - }; - }); - - window_result -} - -fn set_clipboard( - message: anyhow::Result, - clipboard: &mut Clipboard, - toasts: &mut Toasts, - locale: &Locale, -) { - const CLIPBOARD_TOAST_DURATION: Duration = Duration::from_secs(5); - match message { - Ok(latex) => match clipboard.set_text(latex) { - Ok(_) => { - toasts_info( - toasts, - locale.get_translated("LaTeX copied to clipboard"), - CLIPBOARD_TOAST_DURATION, - ); - } - Err(e) => { - toasts_error( - toasts, - locale.get_translated("Failed to copy LaTeX to clipboard") - + "\n" - + e.to_string().as_str(), - CLIPBOARD_TOAST_DURATION, - ); - } - }, - Err(e) => { - toasts_error( - toasts, - locale.get_translated("Failed to generate LaTeX") + "\n" + e.to_string().as_str(), - CLIPBOARD_TOAST_DURATION, - ); - } - } -} - -fn display_shell( - ctx: &Context, - State { - shell, - env, - windows, - toasts, - .. - }: &mut State, - locale: &Locale, -) { - let mut run_shell_command = |shell_text: &mut String| match parse_instruction(shell_text, env) { - Ok(identifier) => { - shell_text.clear(); - windows.insert(identifier, WindowState { is_open: true }); - } - Err(error) => { - println!("{error}"); - toasts_error(toasts, error.to_string(), Duration::from_secs(5)); - } - }; - - egui::TopBottomPanel::bottom("shell") - .resizable(false) - .default_height(128.0) - .show(ctx, |ui| { - let button_sense = if shell.text.is_empty() { - Sense::hover() - } else { - Sense::click() - }; - - ui.horizontal(|ui| { - ui.with_layout(egui::Layout::right_to_left(egui::Align::BOTTOM), |ui| { - if ui - .add(egui::Button::new(locale.get_translated("Run")).sense(button_sense)) - .clicked() - { - run_shell_command(&mut shell.text); - } - - let response = ui.add( - egui::TextEdit::singleline(&mut shell.text) - .desired_rows(1) - .desired_width(ui.available_width()) - .code_editor(), - ); - if response.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)) { - run_shell_command(&mut shell.text); - response.request_focus(); - } - }); - }); - }); -} - -fn toasts_add_kind(toasts: &mut Toasts, text: String, duration: Duration, kind: ToastKind) { - toasts.add(Toast { - text: text.into(), - kind, - options: ToastOptions::default() - .duration(duration) - .show_progress(true), - }); -} - -fn toasts_info(toasts: &mut Toasts, text: String, duration: Duration) { - toasts_add_kind(toasts, text, duration, ToastKind::Info); -} - -fn toasts_error(toasts: &mut Toasts, text: String, duration: Duration) { - toasts_add_kind(toasts, text, duration, ToastKind::Error); + jp2gmd_lib::run_application() } diff --git a/src/matrices.rs b/src/matrices.rs index 95815f6..54e1889 100644 --- a/src/matrices.rs +++ b/src/matrices.rs @@ -29,6 +29,7 @@ impl Matrix { /// A new matrix. /// # Examples /// ``` + /// # use jp2gmd_lib::Matrix; /// let m = Matrix::new_unsafe(vec![vec![1, 2, 3], vec![4, 5, 6]]); /// // m corresponds to the matrix /// // | 1 2 3 | @@ -50,6 +51,7 @@ impl Matrix { /// A new matrix. /// # Examples /// ``` + /// # use jp2gmd_lib::Matrix; /// let m = Matrix::new(vec![vec![1, 2, 3], vec![4, 5, 6]]); /// // m corresponds to the matrix /// // | 1 2 3 | @@ -58,6 +60,7 @@ impl Matrix { /// # Errors /// If the data is not a valid matrix. /// ``` + /// # use jp2gmd_lib::Matrix; /// let m = Matrix::new(vec![vec![1, 2, 3], vec![4, 5]]); /// // m is an error /// ``` @@ -86,6 +89,7 @@ impl Matrix { /// A new matrix. /// # Examples /// ``` + /// # use jp2gmd_lib::Matrix; /// let m = Matrix::from_vec(vec![1, 2, 3, 4, 5, 6], (2, 3)); /// // m corresponds to the matrix /// // | 1 2 3 | @@ -116,7 +120,8 @@ impl Matrix { /// * `separator` - The index of the separator (or None if there is no separator). /// # Examples /// ``` - /// let mut m = Matrix::new(vec![vec![1, 2, 3], vec![4, 5, 6]]); + /// # use jp2gmd_lib::Matrix; + /// let mut m = Matrix::new(vec![vec![1, 2, 3], vec![4, 5, 6]]).unwrap(); /// // m corresponds to the matrix /// // | 1 2 3 | /// // | 4 5 6 | @@ -149,11 +154,13 @@ impl Matrix { /// A new matrix. /// # Examples /// ``` - /// let m = Matrix::new(vec![vec![1, 2, 3], vec![4, 5, 6]]); + /// # use anyhow::Context; + /// # use jp2gmd_lib::Matrix; + /// let m = Matrix::new(vec![vec![1, 2, 3], vec![4, 5, 6]]).expect("Invalid matrix."); /// // m corresponds to the matrix /// // | 1 2 3 | /// // | 4 5 6 | - /// let m = Matrix::reshape(m, (3, 2)); + /// let m = Matrix::reshape(&m, (3, 2)).expect("Invalid size."); /// // m corresponds to the matrix /// // | 1 2 | /// // | 3 4 | @@ -176,11 +183,12 @@ impl Matrix { /// A new matrix of shape (h, w) with given supplier. /// # Examples /// ``` - /// let m = Matrix::new_with(2, 3, |i, j| i + j); + /// # use jp2gmd_lib::Matrix; + /// let m = Matrix::filled((2, 3), |i, j| (i + j) as i64); /// // m corresponds to the matrix /// // | 0 1 2 | /// // | 1 2 3 | - /// let m = Matrix::new_with(2, 3, |i, j| i * j); + /// let m = Matrix::filled((2, 3), |i, j| (i * j) as i64); /// // m corresponds to the matrix /// // | 0 0 0 | /// // | 0 1 2 | @@ -211,7 +219,8 @@ impl Matrix { /// A zero matrix of shape (h, w). /// # Examples /// ``` - /// let m = Matrix::zero(2, 3); + /// # use jp2gmd_lib::Matrix; + /// let m = Matrix::::zeros((2, 3)); /// // m corresponds to the matrix /// // | 0 0 0 | /// // | 0 0 0 | @@ -227,7 +236,8 @@ impl Matrix { /// A ones matrix of shape (h, w). /// # Examples /// ``` - /// let m = Matrix::ones(2, 3); + /// # use jp2gmd_lib::Matrix; + /// let m = Matrix::::ones((2, 3)); /// // m corresponds to the matrix /// // | 1 1 1 | /// // | 1 1 1 | @@ -243,7 +253,9 @@ impl Matrix { /// An identity matrix of shape (n, n). /// # Examples /// ``` - /// let m = Matrix::identity(3); + /// # use num_rational::Rational64; + /// # use jp2gmd_lib::Matrix; + /// let m = Matrix::::identity(3); /// // m corresponds to the matrix /// // | 1 0 0 | /// // | 0 1 0 | @@ -267,6 +279,7 @@ impl Matrix { /// A tuple of the form `(height, width)`. /// # Examples /// ``` + /// # use jp2gmd_lib::Matrix; /// let m = Matrix::new(vec![vec![1, 2, 3], vec![4, 5, 6]]).unwrap(); /// assert_eq!(m.get_shape(), (2, 3)); /// ``` @@ -283,12 +296,12 @@ impl Matrix { /// Matrix has to be valid. Otherwise, the behavior is undefined. /// # Examples /// ```rust - /// use matrix::Matrix; + /// # use jp2gmd_lib::Matrix; /// let m = Matrix::new(vec![vec![1, 2], vec![3, 4]]).unwrap(); /// assert!(!m.is_empty()); - /// let m = Matrix::new(vec![vec![], vec![], vec![]]).unwrap(); + /// let m = Matrix::::new(vec![vec![], vec![], vec![]]).unwrap(); /// assert!(m.is_empty()); - /// let m = Matrix::new(vec![]).unwrap(); + /// let m = Matrix::::new(vec![]).unwrap(); /// assert!(m.is_empty()); /// ``` pub fn is_empty(&self) -> bool { @@ -299,6 +312,7 @@ impl Matrix { /// Matrix is valid if all rows have the same length. /// # Examples /// ```rust + /// # use jp2gmd_lib::Matrix; /// let m = Matrix::new_unsafe(vec![vec![1, 2], vec![3, 4]]); /// assert!(m.is_valid()); /// let m = Matrix::new_unsafe(vec![vec![1, 2], vec![3, 4, 5]]); @@ -322,7 +336,7 @@ impl Matrix { /// Returns the raw data of the matrix. /// # Examples /// ```rust - /// use matrix::Matrix; + /// # use jp2gmd_lib::Matrix; /// let m = Matrix::new(vec![vec![1, 2], vec![3, 4]]).unwrap(); /// assert_eq!(m.get_data(), &vec![vec![1, 2], vec![3, 4]]); /// ``` @@ -333,9 +347,9 @@ impl Matrix { /// Returns the raw data of the matrix and consumes the matrix. /// # Examples /// ```rust - /// use matrix::Matrix; + /// # use jp2gmd_lib::Matrix; /// let m = Matrix::new(vec![vec![1, 2], vec![3, 4]]).unwrap(); - /// assert_eq!(m.into_data(), vec![vec![1, 2], vec![3, 4]]); + /// assert_eq!(m.consume(), vec![vec![1, 2], vec![3, 4]]); /// ``` pub fn consume(self) -> Vec> { self.data @@ -349,13 +363,13 @@ impl Matrix { /// `true` if the matrices have the same shape, `false` otherwise. /// # Examples /// ```rust - /// use matrix::Matrix; + /// # use jp2gmd_lib::Matrix; /// let m1 = Matrix::new(vec![vec![1, 2], vec![3, 4]]).unwrap(); /// let m2 = Matrix::new(vec![vec![1, 2], vec![3, 4]]).unwrap(); - /// assert!(m1.has_same_shape(&m2)); + /// assert!(m1.same_shapes(&m2)); /// let m1 = Matrix::new(vec![vec![1, 2], vec![3, 4]]).unwrap(); /// let m2 = Matrix::new(vec![vec![1, 2, 3], vec![4, 5, 6]]).unwrap(); - /// assert!(!m1.has_same_shape(&m2)); + /// assert!(!m1.same_shapes(&m2)); /// ``` pub fn same_shapes(&self, other: &Self) -> bool { let self_shape = self.get_shape(); @@ -372,15 +386,15 @@ impl Matrix { /// Returns `Err` if the matrices cannot be multiplied - e.g. if they have incompatible shapes. /// # Examples /// ```rust - /// use matrix::Matrix; + /// # use jp2gmd_lib::Matrix; /// let m1 = Matrix::new(vec![vec![1, 2, 3], vec![4, 5, 6]]).unwrap(); /// let m2 = Matrix::new(vec![vec![1, 2], vec![3, 4], vec![5, 6]]).unwrap(); - /// assert_eq!(m1.get_shape_after_mul(&m2), Ok((2, 2))); + /// assert_eq!(m1.result_shape_for_mul(&m2).unwrap(), (2, 2)); /// let m1 = Matrix::new(vec![vec![1, 2, 3], vec![4, 5, 6]]).unwrap(); /// let m2 = Matrix::new(vec![vec![1, 2], vec![3, 4]]).unwrap(); - /// assert!(m1.get_shape_after_mul(&m2).is_err()); + /// assert!(m1.result_shape_for_mul(&m2).is_err()); /// ``` - fn result_shape_for_mul(&self, other: &Self) -> anyhow::Result<(usize, usize)> { + pub fn result_shape_for_mul(&self, other: &Self) -> anyhow::Result<(usize, usize)> { let (h, self_w) = self.get_shape(); let (other_h, w) = other.get_shape(); if self_w == other_h { @@ -400,10 +414,10 @@ impl Matrix { /// A new matrix with the result of the operation. /// # Examples /// ```rust - /// use matrix::Matrix; + /// # use jp2gmd_lib::Matrix; /// let m1 = Matrix::new(vec![vec![1, 2], vec![3, 4]]).unwrap(); /// let m2 = Matrix::new(vec![vec![1, 2], vec![3, 4]]).unwrap(); - /// let m3 = m1.checked_operation_on_two(&m2, |a, b| a + b).unwrap(); + /// let m3 = m1.checked_operation_on_two(&m2, |a, b| Some(a + b)).unwrap(); /// assert_eq!(m3, Matrix::new(vec![vec![2, 4], vec![6, 8]]).unwrap()); /// ``` pub fn checked_operation_on_two(&self, other: &Self, operation: F) -> anyhow::Result @@ -436,8 +450,9 @@ impl Matrix { /// A new matrix with the result of the operation. /// # Examples /// ```rust + /// # use jp2gmd_lib::Matrix; /// let m = Matrix::new(vec![vec![1, 2], vec![3, 4]]).unwrap(); - /// let m2 = m.checked_operation(|a| a + 1).unwrap(); + /// let m2 = m.checked_operation(|a| Some(a + 1)).unwrap(); /// assert_eq!(m2, Matrix::new(vec![vec![2, 3], vec![4, 5]]).unwrap()); /// ``` pub fn checked_operation(&self, operation: F) -> anyhow::Result @@ -459,8 +474,8 @@ impl Matrix { /// # Errors /// Returns `Err` if the matrices have different shapes. /// # Examples - /// ```rust - /// use matrix::Matrix; + /// ``` + /// # use jp2gmd_lib::Matrix; /// let m1 = Matrix::new(vec![vec![1, 2], vec![3, 4]]).unwrap(); /// let m2 = Matrix::new(vec![vec![1, 2], vec![3, 4]]).unwrap(); /// let m3 = m1.checked_add(&m2).unwrap(); @@ -479,7 +494,7 @@ impl Matrix { /// Returns `Err` if the matrices have different shapes. /// # Examples /// ```rust - /// use matrix::Matrix; + /// # use jp2gmd_lib::Matrix; /// let m1 = Matrix::new(vec![vec![1, 2], vec![3, 4]]).unwrap(); /// let m2 = Matrix::new(vec![vec![1, 2], vec![3, 4]]).unwrap(); /// let m3 = m1.checked_sub(&m2).unwrap(); @@ -494,7 +509,7 @@ impl Matrix { /// A new matrix with the result of the negation. /// # Examples /// ```rust - /// use matrix::Matrix; + /// # use jp2gmd_lib::Matrix; /// let m1 = Matrix::new(vec![vec![1, 2], vec![3, 4]]).unwrap(); /// let m2 = m1.checked_neg().unwrap(); /// assert_eq!(m2, Matrix::new(vec![vec![-1, -2], vec![-3, -4]]).unwrap()); @@ -512,7 +527,7 @@ impl Matrix { /// Returns `Err` if height of the first matrix is not equal to the width of the second matrix. /// # Examples /// ```rust - /// use matrix::Matrix; + /// # use jp2gmd_lib::Matrix; /// let m1 = Matrix::new(vec![vec![1, 2], vec![3, 4]]).unwrap(); /// let m2 = Matrix::new(vec![vec![1, 2], vec![3, 4]]).unwrap(); /// let m3 = m1.checked_mul(&m2).unwrap(); @@ -544,7 +559,7 @@ impl Matrix { /// Returns `Err` if the multiplication overflows. /// # Examples /// ```rust - /// use matrix::Matrix; + /// # use jp2gmd_lib::Matrix; /// let m1 = Matrix::new(vec![vec![1, 2], vec![3, 4]]).unwrap(); /// let m2 = m1.checked_mul_scl(&2).unwrap(); /// assert_eq!(m2, Matrix::new(vec![vec![2, 4], vec![6, 8]]).unwrap()); @@ -587,7 +602,7 @@ impl Matrix { /// A new matrix with the result of the operation. /// # Examples /// ```rust - /// use matrix::Matrix; + /// # use jp2gmd_lib::Matrix; /// let m1 = Matrix::new(vec![vec![1, 2], vec![3, 4]]).unwrap(); /// let m2 = Matrix::new(vec![vec![5, 6], vec![7, 8]]).unwrap(); /// let m3 = m1.concat(m2).unwrap(); @@ -611,7 +626,7 @@ impl Matrix { /// A tuple of two matrices. /// # Examples /// ```rust - /// use matrix::Matrix; + /// # use jp2gmd_lib::Matrix; /// let m1 = Matrix::new(vec![vec![1, 2, 3, 4], vec![5, 6, 7, 8]]).unwrap(); /// let (m2, m3) = m1.split(2).unwrap(); /// assert_eq!(m2, Matrix::new(vec![vec![1, 2], vec![5, 6]]).unwrap()); @@ -803,11 +818,13 @@ impl From> for Vec { } /// Create a matrix row (vector) of Rational64 numbers passed as integers. -/// Uses ri! macro. -/// Used as helper macro for rm! macro. +/// # uses ri! macro. +/// # used as helper macro for rm! macro. /// rv stands for Rational Vector. /// Example: /// ``` +/// # use jp2gmd_lib::{rv, ri}; +/// # use num_rational::Rational64; /// rv!(1, 2, 3); // Creates a row vector [ri!(1), ri!(2), ri!(3)] /// ``` #[macro_export] @@ -820,10 +837,12 @@ macro_rules! rv { } /// Create a matrix of Rational64 numbers passed as integers. -/// Uses ri! and rv! macros. +/// # uses ri! and rv! macros. /// rm stands for Rational Matrix. /// Example: /// ``` +/// # use jp2gmd_lib::*; +/// # use num_rational::Rational64; /// // Creates a matrix /// // | 1 2 3 | /// // | 4 5 6 | @@ -843,6 +862,7 @@ macro_rules! rm { /// im stands for Integer Matrix. /// Example: /// ``` +/// # use jp2gmd_lib::{im, Matrix}; /// // Creates a matrix /// // | 1 2 3 | /// // | 4 5 6 |