Lots of owork on a calculator example, transparency, etc.

This commit is contained in:
2026-05-16 16:18:55 -04:00
parent e2c2563b7e
commit 6dcc2d0d00
40 changed files with 5783 additions and 125 deletions

View File

@@ -0,0 +1,161 @@
mod components;
mod state;
use ruin_app::prelude::*;
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;
use tracing_subscriber::{EnvFilter, fmt};
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_eq!(state.is_error.get(), false);
assert_eq!(state.history.get().len(), 1);
assert_eq!(state.history.get()[0].expression, "2+2");
assert_eq!(state.history.get()[0].result, "4");
}
}