Files
ruin/examples/calculator/src/main.rs
Will Temple 4347232b98 Clippy clean
2026-05-16 16:59:09 -04:00

186 lines
4.9 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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");
}
}