Initial commit
This commit is contained in:
commit
1798c29c6b
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/target
|
8
.idea/.gitignore
vendored
Normal file
8
.idea/.gitignore
vendored
Normal 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
11
.idea/mcalc.iml
Normal 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
6
.idea/misc.xml
Normal 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
8
.idea/modules.xml
Normal 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
6
.idea/vcs.xml
Normal 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
2435
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
30
Cargo.toml
Normal file
30
Cargo.toml
Normal 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
5
build.rs
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
extern crate lalrpop;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
lalrpop::process_root().unwrap();
|
||||||
|
}
|
43
src/grammar.lalrpop
Normal file
43
src/grammar.lalrpop
Normal 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
256
src/main.rs
Normal 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
48
src/parser.rs
Normal 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)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user