Initial commit

This commit is contained in:
Mutzi 2023-04-30 22:03:11 +02:00
commit 1798c29c6b
12 changed files with 2857 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/target

8
.idea/.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

11
.idea/mcalc.iml Normal file
View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="CPP_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/target" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

6
.idea/misc.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="MarkdownSettingsMigration">
<option name="stateVersion" value="1" />
</component>
</project>

8
.idea/modules.xml Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/mcalc.iml" filepath="$PROJECT_DIR$/.idea/mcalc.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

2435
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

30
Cargo.toml Normal file
View File

@ -0,0 +1,30 @@
[package]
name = "mcalc"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
glium = "0.32.1"
imgui-glium-renderer = "0.11.0"
imgui-winit-support = "0.11.0"
rfd = "0.11.3"
serde_json = "1.0.96"
thousands = "0.2.0"
[dependencies.serde]
version = "1.0.160"
features = ["derive"]
[dependencies.lalrpop-util]
version = "0.19.12"
features = ["lexer"]
[dependencies.imgui]
version = "0.11.0"
features = ["docking", "tables-api"]
[build-dependencies]
lalrpop = "0.19.12"

5
build.rs Normal file
View File

@ -0,0 +1,5 @@
extern crate lalrpop;
fn main() {
lalrpop::process_root().unwrap();
}

43
src/grammar.lalrpop Normal file
View File

@ -0,0 +1,43 @@
use crate::parser::Node;
use std::str::FromStr;
grammar;
Num: f64 = {
r"[0-9]+(\.[0-9]+)?" => f64::from_str(<>).unwrap_or_else(|e| { println!("{}", <>); panic!("{:#?}", e); })
}
Ident: String = {
r"[a-zA-Z_][a-zA-Z0-9_]*" => String::from(<>)
}
Var: Node = {
Ident => Node::Var(<>),
Num => Node::Num(<>),
}
E3: Node = {
Var,
"(" <e:E0> ")" => e,
}
E2: Node = {
"-" <e:E3> => Node::Neg(Box::new(e)),
E3
}
E1: Node = {
<l:E1> "*" <r:E2> => Node::Mul(Box::new(l), Box::new(r)),
<l:E1> "/" <r:E2> => Node::Div(Box::new(l), Box::new(r)),
E2
}
E0: Node = {
<l:E0> "+" <r:E1> => Node::Plus(Box::new(l), Box::new(r)),
<l:E0> "-" <r:E1> => Node::Minus(Box::new(l), Box::new(r)),
E1
}
pub Assign: (String, Node) = {
<i:Ident> "=" <e:E0> => (i, e)
}

256
src/main.rs Normal file
View File

@ -0,0 +1,256 @@
mod parser;
use glium::Surface;
use imgui::{Condition, Context, FocusedWidget, TableFlags, Ui, WindowFlags};
use lalrpop_util::lalrpop_mod;
use thousands::Separable;
use crate::parser::Node;
lalrpop_mod!(grammar);
#[derive(Debug, serde::Deserialize, serde::Serialize)]
struct Variable {
name: String,
expr: String,
#[serde(skip)]
value: Option<f64>,
parsed_expr: Node,
used: Vec<String>
}
#[derive(Debug, Default)]
struct State {
inputs: Vec<String>,
input_buf: String,
error_msg: Option<String>,
variables: Vec<Variable>,
last_eval: Option<std::time::Duration>
}
fn eval_vars(state: &mut State) {
let start = std::time::Instant::now();
let var_map = state.variables.iter()
.map(|v| (v.name.clone(), v.parsed_expr.clone()))
.collect();
for v in &mut state.variables {
let val = v.parsed_expr.eval(&var_map);
v.value = val;
}
state.last_eval = Some(start.elapsed());
}
fn handle_input(input: String, state: &mut State) -> bool {
state.error_msg.take();
let node: Result<(String, Node), lalrpop_util::ParseError<usize, lalrpop_util::lexer::Token<'_>, &str>>
= grammar::AssignParser::new().parse(&input);
match node {
Ok((name, expr)) => {
let used_vars = expr.used_vars();
if used_vars.contains(&name) {
state.error_msg = Some("Variable can't depend on itself".to_string());
return false;
}
let seen = vec![name.clone()];
if let Some(mut e) = used_vars.iter().filter_map(|v| find_dependency_recursive(state, v, seen.clone()).err()).next() {
e.push(name.clone());
e.reverse();
e.push(name.clone());
state.error_msg = Some(format!("Found recursive dependency:\n{}", e.join(" -> ")));
return false;
}
if let Some(v) = state.variables.iter_mut().filter(|v| v.name == name).next() {
v.parsed_expr = expr;
v.expr = input.clone();
v.used = used_vars;
} else {
state.variables.push(Variable {
name,
expr: input.clone(),
value: None,
parsed_expr: expr,
used: used_vars
});
}
}
Err(e) => {
state.error_msg = Some(e.to_string());
return false;
}
}
eval_vars(state);
state.inputs.push(input);
true
}
fn find_dependency_recursive(state: &State, name: &String, mut seen: Vec<String>) -> Result<(), Vec<String>> {
if let Some(var) = state.variables.iter().filter(|v| v.name == *name).next() {
seen.push(name.clone());
if var.used.iter().any(|v| seen.contains(v)) {
return Err(vec![name.clone()]);
}
if let Some(mut e) = var.used.iter().filter_map(|v| find_dependency_recursive(state, v, seen.clone()).err()).next() {
e.push(name.clone());
return Err(e);
}
}
Ok(())
}
fn render(main_pos_size: ([f32; 2], [f32; 2]), state: &mut State, ui: &mut Ui) {
if let Some(t) = ui.begin_main_menu_bar() {
if ui.menu_item("Clear") {
*state = State::default();
}
if ui.menu_item("Open") {
if let Some(f) = rfd::FileDialog::new().add_filter("json", &["json"]).pick_file() {
let data = std::fs::read_to_string(f).unwrap_or("}{".to_string());
let vars: Result<Vec<Variable>, _> = serde_json::from_str(&data);
match vars {
Ok(v) => {
*state = State::default();
state.variables = v;
eval_vars(state);
},
Err(e) => println!("Error while loading:\n{:#?}", e)
}
}
}
if ui.menu_item("Save") {
if let Some(f) = rfd::FileDialog::new().add_filter("json", &["json"]).save_file() {
if let Err(e) = std::fs::write(f, serde_json::to_string(&state.variables).unwrap()) {
println!("Error while saving:\n{:#?}", e);
}
}
}
t.end();
}
let wflags = WindowFlags::NO_SCROLLBAR | WindowFlags::NO_MOVE | WindowFlags::NO_DECORATION | WindowFlags::NO_RESIZE | WindowFlags::NO_TITLE_BAR | WindowFlags::NO_COLLAPSE | WindowFlags::NO_DOCKING;
if let Some(t) = ui.window("MainWindow")
.position(main_pos_size.0, Condition::Always)
.size(main_pos_size.1, Condition::Always)
.flags(wflags)
.begin() {
ui.child_window("InputText")
.size([0.0, 300.0])
.always_vertical_scrollbar(true)
.border(true)
.build(|| state.inputs.iter().for_each(|i| ui.text(i)));
if let Some(err) = &state.error_msg {
ui.text_colored([1.0, 0.0, 0.0, 1.0], "Error: ");
ui.text(err);
}
ui.text("Input: ");
ui.same_line();
if ui.input_text("##Input", &mut state.input_buf)
.enter_returns_true(true)
.build() {
if handle_input(state.input_buf.clone(), state) {
state.input_buf.clear();
}
ui.set_keyboard_focus_here_with_offset(FocusedWidget::Previous);
}
if let Some(last_eval) = &state.last_eval {
ui.same_line();
ui.text(format!("Eval took: {:?}", last_eval));
}
ui.spacing();
ui.separator();
ui.spacing();
if let Some(t) = ui.child_window("Variables").begin() {
if let Some(t) = ui.begin_table_with_flags("VariableTable", 4, TableFlags::BORDERS | TableFlags::ROW_BG | TableFlags::RESIZABLE) {
ui.table_setup_column("Name");
ui.table_setup_column("Value");
ui.table_setup_column("Expression");
ui.table_setup_column("Actions");
ui.table_headers_row();
let mut to_remove = vec![];
for v in &state.variables {
ui.table_next_row();
ui.table_next_column();
ui.text(&v.name);
ui.table_next_column();
ui.text(v.value.map_or("-".to_string(), |v| v.separate_with_spaces()));
ui.table_next_column();
ui.text(&v.expr);
ui.table_next_column();
if ui.button(format!("Remove##{}", v.name)) {
to_remove.push(v.name.clone());
}
}
if !to_remove.is_empty() {
state.variables.retain(|v| !to_remove.contains(&v.name));
eval_vars(state);
}
t.end();
}
t.end();
}
t.end();
}
}
fn main() {
let mut state = State::default();
let ev = glium::glutin::event_loop::EventLoop::new();
let ctx = glium::glutin::ContextBuilder::new().with_vsync(false);
let builder = glium::glutin::window::WindowBuilder::new()
.with_title("MCalc");
let display = glium::Display::new(builder, ctx, &ev).unwrap();
let mut imgui = Context::create();
imgui.set_ini_filename(None);
let mut platform = imgui_winit_support::WinitPlatform::init(&mut imgui);
{
let glw = display.gl_window();
let window = glw.window();
platform.attach_window(imgui.io_mut(), window, imgui_winit_support::HiDpiMode::Locked(1.0));
}
let mut renderer = imgui_glium_renderer::Renderer::init(&mut imgui, &display).unwrap();
let mut last_frame = std::time::Instant::now();
ev.run(move |e, _, flow| match e {
glium::glutin::event::Event::NewEvents(_) => {
let now = std::time::Instant::now();
imgui.io_mut().update_delta_time(now - last_frame);
last_frame = now;
}
glium::glutin::event::Event::MainEventsCleared => {
let glw = display.gl_window();
platform.prepare_frame(imgui.io_mut(), glw.window()).unwrap();
glw.window().request_redraw();
}
glium::glutin::event::Event::RedrawRequested(_) => {
let main_pos_size = {
let v = imgui.main_viewport();
(v.work_pos, v.work_size)
};
let ui = imgui.new_frame();
render(main_pos_size, &mut state, ui);
let glw = display.gl_window();
let mut target = display.draw();
target.clear_color(0.0, 0.0, 0.0, 1.0);
platform.prepare_render(ui, glw.window());
renderer.render(&mut target, imgui.render()).unwrap();
target.finish().unwrap();
}
glium::glutin::event::Event::WindowEvent { event: glium::glutin::event::WindowEvent::CloseRequested, .. } => {
*flow = glium::glutin::event_loop::ControlFlow::Exit
}
_ => {
let glw = display.gl_window();
platform.handle_event(imgui.io_mut(), glw.window(), &e);
}
})
}

48
src/parser.rs Normal file
View File

@ -0,0 +1,48 @@
use std::collections::HashMap;
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
pub enum Node {
Num(f64),
Var(String),
Neg(Box<Node>),
Plus(Box<Node>, Box<Node>),
Minus(Box<Node>, Box<Node>),
Mul(Box<Node>, Box<Node>),
Div(Box<Node>, Box<Node>)
}
fn join_vec<T>(mut l: Vec<T>, mut r: Vec<T>) -> Vec<T> {
l.append(&mut r);
l
}
impl Node {
pub fn used_vars(&self) -> Vec<String> {
match self {
Node::Num(_) => vec![],
Node::Var(v) => vec![v.clone()],
Node::Neg(n) => n.used_vars(),
Node::Plus(l, r) => join_vec(l.used_vars(), r.used_vars()),
Node::Minus(l, r) => join_vec(l.used_vars(), r.used_vars()),
Node::Mul(l, r) => join_vec(l.used_vars(), r.used_vars()),
Node::Div(l, r) => join_vec(l.used_vars(), r.used_vars())
}
}
pub fn eval(&self, vars: &HashMap<String, Node>) -> Option<f64> {
match self {
Node::Num(v) => Some(v.clone()),
Node::Var(v) => vars.get(v)?.eval(vars),
Node::Neg(n) => n.eval(vars),
Node::Plus(l, r) =>
l.eval(vars).and_then(|l| r.eval(vars).and_then(|r| Some(l + r))),
Node::Minus(l, r) =>
l.eval(vars).and_then(|l| r.eval(vars).and_then(|r| Some(l - r))),
Node::Mul(l, r) =>
l.eval(vars).and_then(|l| r.eval(vars).and_then(|r| Some(l * r))),
Node::Div(l, r) =>
l.eval(vars).and_then(|l| r.eval(vars).and_then(|r| Some(l / r)))
}
}
}