calculator/
calculator.rs

1//! Simple calculator. Spurred by <https://discord.com/channels/691052431525675048/885021580353237032/1263661461364932639>.
2
3mod utils;
4use utils::*;
5
6use bevy::prelude::*;
7use calc::*;
8use haalka::prelude::*;
9use rust_decimal::prelude::*;
10
11fn main() {
12    App::new()
13        .add_plugins(examples_plugin)
14        .add_systems(
15            Startup,
16            (
17                |world: &mut World| {
18                    ui_root().spawn(world);
19                },
20                camera,
21            ),
22        )
23        .run();
24}
25
26const BLUE: Color = Color::srgb(91. / 255., 206. / 255., 250. / 255.);
27const PINK: Color = Color::srgb(245. / 255., 169. / 255., 184. / 255.);
28const FONT_SIZE: f32 = 50.0;
29const WIDTH: f32 = 500.;
30const BUTTON_SIZE: f32 = WIDTH / 5.;
31const GAP: f32 = BUTTON_SIZE / 5.;
32const HEIGHT: f32 = BUTTON_SIZE * 5. + GAP * 6.;
33
34fn textable_element(text_signal: impl Signal<Item = impl Into<String> + 'static> + Send + 'static) -> El<Node> {
35    El::<Node>::new()
36        .with_node(|mut node| node.border = UiRect::all(Val::Px(2.0)))
37        .border_color(BorderColor(Color::WHITE))
38        .child(
39            El::<Text>::new()
40                .text_font(TextFont::from_font_size(FONT_SIZE))
41                .text_color(TextColor(Color::WHITE))
42                .text_signal(text_signal.map(Text::new)),
43        )
44}
45
46#[rustfmt::skip]
47fn buttons() -> [&'static str; 16] {
48    [
49        "7", "8", "9", "/",
50        "4", "5", "6", "*",
51        "1", "2", "3", "-",
52        "0", ".", "=", "+",
53    ]
54}
55
56fn button(symbol: &'static str) -> El<Node> {
57    textable_element(always(symbol))
58        .with_node(|mut node| {
59            node.width = Val::Px(BUTTON_SIZE);
60            node.height = Val::Px(BUTTON_SIZE);
61        })
62        .align_content(Align::center())
63}
64
65fn input_button(symbol: &'static str) -> impl Element {
66    let hovered = Mutable::new(false);
67    let f = move || {
68        if symbol == "=" {
69            let mut output = OUTPUT.lock_mut();
70            if let Ok(result) = Context::<f64>::default().evaluate(&output)
71                && let Some(result) = Decimal::from_f64((result * 100.).round() / 100.)
72            {
73                *output = result.normalize().to_string();
74                return;
75            }
76            ERROR.set_neq(true);
77        } else {
78            *OUTPUT.lock_mut() += symbol;
79        }
80    };
81    button(symbol)
82        .cursor(CursorIcon::System(SystemCursorIcon::Pointer))
83        .background_color_signal(hovered.signal().map_bool(|| BLUE, || PINK).map(BackgroundColor))
84        .hovered_sync(hovered)
85        .on_click(f)
86}
87
88static OUTPUT: LazyLock<Mutable<String>> = LazyLock::new(default);
89static ERROR: LazyLock<Mutable<bool>> = LazyLock::new(default);
90
91fn display() -> impl Element {
92    textable_element(OUTPUT.signal_cloned())
93        .update_raw_el(|raw_el| {
94            raw_el.component_signal::<Outline, _>(
95                ERROR
96                    .signal()
97                    .map_true(|| Outline::new(Val::Px(4.0), Val::ZERO, bevy::color::palettes::basic::RED.into())),
98            )
99        })
100        .with_node(|mut node| {
101            node.width = Val::Px(BUTTON_SIZE * 3. + GAP * 2.);
102            node.height = Val::Px(BUTTON_SIZE);
103            node.padding = UiRect::all(Val::Px(GAP));
104            node.overflow = Overflow::clip();
105        })
106        .background_color(BackgroundColor(BLUE))
107        .align_content(Align::new().right().center_y())
108}
109
110fn clear_button() -> impl Element {
111    let hovered = Mutable::new(false);
112    let output_empty = OUTPUT.signal_ref(String::is_empty).broadcast();
113    button("c")
114        .background_color_signal(
115            map_ref! {
116                let output_empty = output_empty.signal(),
117                let hovered = hovered.signal() => {
118                    if *output_empty {
119                        BLUE
120                    } else if *hovered {
121                        bevy::color::palettes::basic::RED.into()
122                    } else {
123                        PINK
124                    }
125                }
126            }
127            .dedupe()
128            .map(BackgroundColor),
129        )
130        .cursor_disableable_signal(CursorIcon::System(SystemCursorIcon::Pointer), output_empty.signal())
131        .hovered_sync(hovered)
132        .on_click(|| OUTPUT.lock_mut().clear())
133}
134
135fn ui_root() -> impl Element {
136    let error_clearer = OUTPUT.signal_ref(|_| ERROR.set_neq(false)).to_future().apply(spawn);
137    El::<Node>::new()
138        .update_raw_el(|raw_el| raw_el.hold_tasks([error_clearer]))
139        .with_node(|mut node| {
140            node.width = Val::Percent(100.);
141            node.height = Val::Percent(100.);
142        })
143        .cursor(CursorIcon::default())
144        .align_content(Align::center())
145        .child(
146            Column::<Node>::new()
147                .background_color(BackgroundColor(PINK))
148                .align(Align::center())
149                .with_node(|mut node| {
150                    node.height = Val::Px(HEIGHT);
151                    node.width = Val::Px(WIDTH);
152                    node.row_gap = Val::Px(GAP);
153                    node.padding = UiRect::all(Val::Px(GAP));
154                })
155                .item(
156                    Row::<Node>::new()
157                        .align(Align::center())
158                        .with_node(|mut node| node.column_gap = Val::Px(GAP))
159                        .item(clear_button())
160                        .item(display()),
161                )
162                .item(
163                    Row::<Node>::new()
164                        .multiline()
165                        .align_content(Align::center())
166                        .with_node(|mut node| {
167                            node.row_gap = Val::Px(GAP);
168                            node.column_gap = Val::Px(GAP);
169                        })
170                        .items(buttons().into_iter().map(input_button)),
171                ),
172        )
173}
174
175fn camera(mut commands: Commands) {
176    commands.spawn(Camera2d);
177}