186 lines
4.9 KiB
Rust
186 lines
4.9 KiB
Rust
mod components;
|
||
mod state;
|
||
|
||
use ruin_app::prelude::*;
|
||
use tracing_subscriber::layer::SubscriberExt;
|
||
use tracing_subscriber::util::SubscriberInitExt;
|
||
use tracing_subscriber::{fmt, EnvFilter};
|
||
|
||
use components::{Display, Keypad, ModeToggle, ScientificPanel};
|
||
use state::{CalcMode, CalcState};
|
||
|
||
#[ruin_runtime::async_main]
|
||
async fn main() -> ruin_app::Result<()> {
|
||
init_tracing();
|
||
|
||
App::new()
|
||
.window(
|
||
Window::new()
|
||
.title("Calculator")
|
||
.app_id("dev.ruin.calculator")
|
||
.base_color(Color::rgba(255, 255, 255, 80))
|
||
.resizable(false),
|
||
)
|
||
.mount(view! { CalculatorApp() {} })
|
||
.run()
|
||
.await
|
||
}
|
||
|
||
fn init_tracing() {
|
||
let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| {
|
||
EnvFilter::new(
|
||
"info,ruin_app::resize=trace,ruin_app::interaction=trace,\
|
||
ruin_ui_platform_wayland::resize=trace,ruin_ui_platform_wayland::perf=debug,\
|
||
ruin_ui_platform_wayland::event_bridge=trace",
|
||
)
|
||
});
|
||
|
||
let _ = tracing_subscriber::registry()
|
||
.with(fmt::layer().with_target(true).without_time())
|
||
.with(filter)
|
||
.try_init();
|
||
}
|
||
|
||
#[component]
|
||
fn CalculatorApp() -> impl IntoView {
|
||
let state = CalcState::new();
|
||
|
||
// ---- Keyboard shortcuts (application-scoped) ----
|
||
|
||
for ch in "0123456789.+-*/%()".chars() {
|
||
let s = state.clone();
|
||
let display = match ch {
|
||
'*' => "×",
|
||
'/' => "÷",
|
||
_ => "",
|
||
};
|
||
let text = if display.is_empty() {
|
||
ch.to_string()
|
||
} else {
|
||
display.to_string()
|
||
};
|
||
use_shortcut(
|
||
Shortcut::new(Key::Character(ch)),
|
||
ShortcutScope::Application,
|
||
{
|
||
let text = text.clone();
|
||
move || s.input(&text)
|
||
},
|
||
);
|
||
}
|
||
|
||
// = key also evaluates
|
||
{
|
||
let s = state.clone();
|
||
use_shortcut(
|
||
Shortcut::new(Key::Character('=')),
|
||
ShortcutScope::Application,
|
||
move || s.evaluate(),
|
||
);
|
||
}
|
||
{
|
||
let s = state.clone();
|
||
use_shortcut(
|
||
Shortcut::new(Key::Enter),
|
||
ShortcutScope::Application,
|
||
move || s.evaluate(),
|
||
);
|
||
}
|
||
{
|
||
let s = state.clone();
|
||
use_shortcut(
|
||
Shortcut::new(Key::Backspace),
|
||
ShortcutScope::Application,
|
||
move || s.backspace(),
|
||
);
|
||
}
|
||
{
|
||
let s = state.clone();
|
||
use_shortcut(
|
||
Shortcut::new(Key::Escape),
|
||
ShortcutScope::Application,
|
||
move || s.clear(),
|
||
);
|
||
}
|
||
{
|
||
let s = state.clone();
|
||
use_shortcut(
|
||
Shortcut::new(Key::Delete),
|
||
ShortcutScope::Application,
|
||
move || s.clear(),
|
||
);
|
||
}
|
||
|
||
// ---- Layout ----
|
||
|
||
let mode = state.mode.clone();
|
||
|
||
let text_style = PartialTextStyle {
|
||
color: Some(Color::BLACK),
|
||
..Default::default()
|
||
};
|
||
|
||
if mode.get() == CalcMode::Scientific {
|
||
view! {
|
||
column(
|
||
gap = 10.0,
|
||
padding = 12.0,
|
||
align_items = AlignItems::Stretch,
|
||
text_style = text_style,
|
||
) {
|
||
Display(state = state.clone()) {}
|
||
ModeToggle(state = state.clone()) {}
|
||
ScientificPanel(state = state.clone()) {}
|
||
Keypad(state = state.clone()) {}
|
||
}
|
||
}
|
||
} else {
|
||
view! {
|
||
column(
|
||
gap = 10.0,
|
||
padding = 12.0,
|
||
align_items = AlignItems::Stretch,
|
||
text_style = text_style,
|
||
) {
|
||
Display(state = state.clone()) {}
|
||
ModeToggle(state = state.clone()) {}
|
||
Keypad(state = state.clone()) {}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use std::cell::RefCell;
|
||
use std::rc::Rc;
|
||
|
||
use super::*;
|
||
|
||
#[component]
|
||
fn StateProbe(slot: Rc<RefCell<Option<CalcState>>>) -> impl IntoView {
|
||
let state = CalcState::new();
|
||
*slot.borrow_mut() = Some(state);
|
||
block().children(())
|
||
}
|
||
|
||
#[test]
|
||
fn successful_evaluation_replaces_expression_and_preserves_history() {
|
||
let slot = Rc::new(RefCell::new(None::<CalcState>));
|
||
let _ = ruin_app::__render_mountable_for_test(&view! {
|
||
StateProbe(slot = Rc::clone(&slot)) {}
|
||
});
|
||
|
||
let state = slot.borrow().clone().expect("state should be captured");
|
||
let _ = state.expression.set("2+2".to_string());
|
||
state.evaluate();
|
||
|
||
assert_eq!(state.expression.get(), "4");
|
||
assert_eq!(state.result.get(), "");
|
||
assert!(!state.is_error.get());
|
||
assert_eq!(state.history.get().len(), 1);
|
||
assert_eq!(state.history.get()[0].expression, "2+2");
|
||
assert_eq!(state.history.get()[0].result, "4");
|
||
}
|
||
}
|