Files
calctext/calcpad-engine/src/types.rs
C. Cassel 0d38bd3108 feat(web): implement complete workspace with themes, tabs, sidebar, and mobile
Transform CalcText from a single-document calculator into a full workspace
application with multi-document support, theming, and responsive mobile experience.

- Theme system: 5 presets (Light, Dark, Matrix, Midnight, Warm) + accent colors
- Document model with localStorage persistence and auto-save
- Tab bar with keyboard shortcuts (Ctrl+N/W/Tab/1-9), rename, close
- Sidebar with search, recent, favorites, folders, templates, drag-and-drop
- 5 templates: Budget, Invoice, Unit Converter, Trip Planner, Loan Calculator
- Status bar with cursor position, engine status, dedication to Igor Cassel
- Results panel: type-specific colors, click-to-copy, error hints
- Format toolbar: H, B, I, //, color labels with live preview toggle
- Syntax highlighting using theme CSS variables
- Error hover tooltips
- Mobile: bottom results tray, sidebar drawer, touch targets, safe areas
- Docker multi-stage build (Rust WASM + Vite + Nginx)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 09:12:05 -04:00

270 lines
7.9 KiB
Rust

use crate::span::Span;
use chrono::NaiveDate;
use serde::{Deserialize, Serialize};
use std::fmt;
/// The result type tag for metadata purposes.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ResultType {
Number,
UnitValue,
CurrencyValue,
DateTime,
TimeDelta,
Boolean,
Empty,
NonCalculable,
Error,
}
impl fmt::Display for ResultType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ResultType::Number => write!(f, "Number"),
ResultType::UnitValue => write!(f, "UnitValue"),
ResultType::CurrencyValue => write!(f, "CurrencyValue"),
ResultType::DateTime => write!(f, "DateTime"),
ResultType::TimeDelta => write!(f, "TimeDelta"),
ResultType::Boolean => write!(f, "Boolean"),
ResultType::Empty => write!(f, "Empty"),
ResultType::NonCalculable => write!(f, "NonCalculable"),
ResultType::Error => write!(f, "Error"),
}
}
}
/// Serializable span for JSON output.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct SerializableSpan {
pub start: usize,
pub end: usize,
}
impl From<Span> for SerializableSpan {
fn from(s: Span) -> Self {
SerializableSpan {
start: s.start,
end: s.end,
}
}
}
/// Metadata attached to every evaluation result.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ResultMetadata {
/// The source span of the expression that produced this result.
pub span: SerializableSpan,
/// The type tag of the result.
pub result_type: ResultType,
/// A display-formatted string of the result.
pub display: String,
/// The raw numeric value, if applicable.
pub raw_value: Option<f64>,
}
/// The value payload of a calculation result.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "kind")]
pub enum CalcValue {
Number { value: f64 },
UnitValue { value: f64, unit: String },
CurrencyValue { amount: f64, currency: String },
DateTime { date: String },
TimeDelta { days: i64, description: String },
Boolean { value: bool },
Error { message: String, span: SerializableSpan },
}
/// A complete calculation result: value + metadata.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CalcResult {
pub value: CalcValue,
pub metadata: ResultMetadata,
}
impl CalcResult {
pub fn number(val: f64, span: Span) -> Self {
let display = format_number(val);
CalcResult {
value: CalcValue::Number { value: val },
metadata: ResultMetadata {
span: span.into(),
result_type: ResultType::Number,
display,
raw_value: Some(val),
},
}
}
pub fn unit_value(val: f64, unit: &str, span: Span) -> Self {
let display = format!("{} {}", format_number(val), unit);
CalcResult {
value: CalcValue::UnitValue {
value: val,
unit: unit.to_string(),
},
metadata: ResultMetadata {
span: span.into(),
result_type: ResultType::UnitValue,
display,
raw_value: Some(val),
},
}
}
pub fn currency_value(amount: f64, currency: &str, span: Span) -> Self {
let display = format_currency(amount, currency);
CalcResult {
value: CalcValue::CurrencyValue {
amount,
currency: currency.to_string(),
},
metadata: ResultMetadata {
span: span.into(),
result_type: ResultType::CurrencyValue,
display,
raw_value: Some(amount),
},
}
}
pub fn datetime(date: NaiveDate, span: Span) -> Self {
let display = date.format("%Y-%m-%d").to_string();
CalcResult {
value: CalcValue::DateTime {
date: display.clone(),
},
metadata: ResultMetadata {
span: span.into(),
result_type: ResultType::DateTime,
display,
raw_value: None,
},
}
}
pub fn time_delta(days: i64, description: &str, span: Span) -> Self {
let display = description.to_string();
CalcResult {
value: CalcValue::TimeDelta {
days,
description: description.to_string(),
},
metadata: ResultMetadata {
span: span.into(),
result_type: ResultType::TimeDelta,
display,
raw_value: Some(days as f64),
},
}
}
pub fn boolean(val: bool, span: Span) -> Self {
let display = val.to_string();
CalcResult {
value: CalcValue::Boolean { value: val },
metadata: ResultMetadata {
span: span.into(),
result_type: ResultType::Boolean,
display,
raw_value: None,
},
}
}
pub fn empty(span: Span) -> Self {
CalcResult {
value: CalcValue::Error {
message: "empty input".to_string(),
span: span.into(),
},
metadata: ResultMetadata {
span: span.into(),
result_type: ResultType::Empty,
display: String::new(),
raw_value: None,
},
}
}
pub fn non_calculable(span: Span) -> Self {
CalcResult {
value: CalcValue::Error {
message: "no expression found".to_string(),
span: span.into(),
},
metadata: ResultMetadata {
span: span.into(),
result_type: ResultType::NonCalculable,
display: String::new(),
raw_value: None,
},
}
}
pub fn error(message: &str, span: Span) -> Self {
CalcResult {
value: CalcValue::Error {
message: message.to_string(),
span: span.into(),
},
metadata: ResultMetadata {
span: span.into(),
result_type: ResultType::Error,
display: format!("Error: {}", message),
raw_value: None,
},
}
}
pub fn result_type(&self) -> ResultType {
self.metadata.result_type
}
/// Returns true if this result is a computed value (not empty, non-calculable, or error).
pub fn is_calculable(&self) -> bool {
!matches!(
self.metadata.result_type,
ResultType::Empty | ResultType::NonCalculable | ResultType::Error
)
}
}
fn format_number(val: f64) -> String {
if val == val.floor() && val.abs() < 1e15 {
format!("{}", val as i64)
} else {
// Round to 10 significant figures to avoid f64 noise
let abs = val.abs();
let magnitude = if abs > 0.0 { abs.log10().floor() as i32 } else { 0 };
let precision = (10 - magnitude - 1).max(0) as usize;
let formatted = format!("{:.prec$}", val, prec = precision);
// Strip trailing zeros after decimal point
if formatted.contains('.') {
formatted
.trim_end_matches('0')
.trim_end_matches('.')
.to_string()
} else {
formatted
}
}
}
fn format_currency(amount: f64, currency: &str) -> String {
let symbol = match currency {
"USD" => "$",
"EUR" => "",
"GBP" => "£",
"JPY" => "¥",
"BRL" => "R$",
_ => "",
};
if symbol.is_empty() {
format!("{:.2} {}", amount, currency)
} else {
format!("{}{:.2}", symbol, amount)
}
}