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>>) -> 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::)); 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"); } }