1mod 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}