1#![expect(clippy::cast_possible_truncation, reason = "Deferred: Noisy")]
6
7use masonry::properties::types::{AsUnit, CrossAxisAlignment, Length, MainAxisAlignment};
8use masonry::widgets::GridParams;
9use winit::dpi::LogicalSize;
10use winit::error::EventLoopError;
11use xilem::style::Style;
12use xilem::view::{
13 Flex, FlexSequence, FlexSpacer, GridExt, GridSequence, button, flex_row, grid, label,
14 sized_box, text_button,
15};
16use xilem::{Color, EventLoop, EventLoopBuilder, WidgetView, WindowOptions, Xilem, palette};
17
18#[derive(Copy, Clone)]
19enum MathOperator {
20 Add,
21 Subtract,
22 Multiply,
23 Divide,
24}
25
26impl MathOperator {
27 fn as_str(self) -> &'static str {
28 match self {
29 Self::Add => "+",
30 Self::Subtract => "\u{2212}",
31 Self::Multiply => "×",
32 Self::Divide => "÷",
33 }
34 }
35
36 fn perform_op(self, num1: f64, num2: f64) -> f64 {
37 match self {
38 Self::Add => num1 + num2,
39 Self::Subtract => num1 - num2,
40 Self::Multiply => num1 * num2,
41 Self::Divide => num1 / num2,
42 }
43 }
44}
45
46struct Calculator {
47 current_num_index: usize,
48 clear_current_entry_on_input: bool, numbers: [String; 2],
50 result: Option<String>,
51 operation: Option<MathOperator>,
52}
53
54impl Calculator {
55 fn get_current_number(&self) -> String {
56 self.current_number().to_string()
57 }
58
59 fn current_number(&self) -> &str {
60 &self.numbers[self.current_num_index]
61 }
62
63 fn set_current_number(&mut self, new_num: String) {
64 self.numbers[self.current_num_index] = new_num;
65 }
66
67 fn clear_all(&mut self) {
68 self.current_num_index = 0;
69 self.result = None;
70 self.operation = None;
71 for num in self.numbers.iter_mut() {
72 *num = "".into();
73 }
74 }
75
76 fn clear_entry(&mut self) {
77 self.clear_current_entry_on_input = false;
78 if self.result.is_some() {
79 self.clear_all();
80 return;
81 }
82 self.set_current_number("".into());
83 }
84
85 fn on_entered_digit(&mut self, digit: &str) {
86 if self.result.is_some() {
87 self.clear_all();
88 } else if self.clear_current_entry_on_input {
89 self.clear_entry();
90 }
91 let mut num = self.get_current_number();
92 if digit == "." {
94 if num.contains('.') {
95 return;
97 }
98 if num.is_empty() {
100 num = "0".into();
101 }
102 num += ".";
103 } else if num == "0" || num.is_empty() {
104 num = digit.to_string();
105 } else {
106 num += digit;
107 }
108 self.set_current_number(num);
109 }
110
111 fn on_entered_operator(&mut self, operator: MathOperator) {
112 self.clear_current_entry_on_input = false;
113 if self.operation.is_some() && !self.numbers[1].is_empty() {
114 if self.result.is_none() {
115 self.on_equals();
117 }
118 self.move_result_to_left();
120 self.current_num_index = 1;
121 } else if self.current_num_index == 0 {
122 if self.numbers[0].is_empty() {
123 return;
126 } else {
127 self.current_num_index = 1;
128 }
129 }
130 self.operation = Some(operator);
131 }
132
133 fn move_result_to_left(&mut self) {
135 self.clear_current_entry_on_input = true;
136 self.numbers[0] = self.result.clone().expect("expected result");
137 self.numbers[1] = "".into();
138 self.operation = None;
139 self.current_num_index = 0; self.result = None; }
142
143 fn on_equals(&mut self) {
144 if self.numbers[0].is_empty() || self.numbers[1].is_empty() {
146 return; } else if self.result.is_some() {
149 self.numbers[0] = self.result.clone().unwrap();
151 }
152 self.current_num_index = 0;
153 let num1 = self.numbers[0].parse::<f64>();
154 let num2 = self.numbers[1].parse::<f64>();
155 self.result = Some(match (num1, num2) {
157 (Ok(num1), Ok(num2)) => self.operation.unwrap().perform_op(num1, num2).to_string(),
158 (Err(err), _) => err.to_string(),
159 (_, Err(err)) => err.to_string(),
160 });
161 }
162
163 fn on_delete(&mut self) {
164 if self.result.is_some() {
165 return;
167 }
168 let mut num = self.get_current_number();
169 if !num.is_empty() {
170 num.remove(num.len() - 1);
171 self.set_current_number(num);
172 } }
174
175 fn negate(&mut self) {
176 if self.result.is_some() {
178 self.move_result_to_left();
179 }
180 let mut num = self.get_current_number();
181 if num.is_empty() {
182 return;
184 }
185 if num.starts_with('-') {
186 num.remove(0);
187 } else {
188 num = format!("-{num}");
189 }
190 self.set_current_number(num);
191 }
192}
193
194fn num_row(nums: [&'static str; 3], row: i32) -> impl GridSequence<Calculator> {
195 let mut views: Vec<_> = vec![];
196 for (i, num) in nums.iter().enumerate() {
197 views.push(digit_button(num).grid_pos(i as i32, row));
198 }
199 views
200}
201
202const DISPLAY_FONT_SIZE: f32 = 30.;
203const GRID_GAP: Length = Length::const_px(2.);
204fn app_logic(data: &mut Calculator) -> impl WidgetView<Calculator> + use<> {
205 grid(
206 (
207 centered_flex_row((
209 FlexSpacer::Flex(0.1),
210 display_label(data.numbers[0].as_ref()),
211 data.operation
212 .map(|operation| display_label(operation.as_str())),
213 display_label(data.numbers[1].as_ref()),
214 data.result.is_some().then(|| display_label("=")),
215 data.result
216 .as_ref()
217 .map(|result| display_label(result.as_ref())),
218 FlexSpacer::Flex(0.1),
219 ))
220 .grid_item(GridParams::new(0, 0, 4, 1)),
221 expanded_button(
223 label("CE").color(if data.get_current_number().is_empty() {
224 palette::css::MEDIUM_VIOLET_RED
225 } else {
226 palette::css::WHITE
227 }),
228 Calculator::clear_entry,
229 )
230 .grid_pos(0, 1),
231 expanded_button(label("C"), Calculator::clear_all).grid_pos(1, 1),
232 expanded_button(label("DEL"), Calculator::on_delete).grid_pos(2, 1),
233 operator_button(MathOperator::Divide).grid_pos(3, 1),
234 num_row(["7", "8", "9"], 2),
235 operator_button(MathOperator::Multiply).grid_pos(3, 2),
236 num_row(["4", "5", "6"], 3),
237 operator_button(MathOperator::Subtract).grid_pos(3, 3),
238 num_row(["1", "2", "3"], 4),
239 operator_button(MathOperator::Add).grid_pos(3, 4),
240 expanded_button(label("±"), Calculator::negate).grid_pos(0, 5),
242 digit_button("0").grid_pos(1, 5),
243 expanded_button(label("."), |data: &mut Calculator| {
244 data.on_entered_digit(".");
245 })
246 .grid_pos(2, 5),
247 expanded_button(label("="), Calculator::on_equals).grid_pos(3, 5),
248 ),
249 4,
250 6,
251 )
252 .spacing(GRID_GAP)
253}
254
255pub fn centered_flex_row<State, Seq: FlexSequence<State>>(sequence: Seq) -> Flex<Seq, State> {
257 flex_row(sequence)
258 .cross_axis_alignment(CrossAxisAlignment::Center)
259 .main_axis_alignment(MainAxisAlignment::Start)
260 .gap(5.px())
261}
262
263fn display_label(text: &str) -> impl WidgetView<Calculator> + use<> {
266 label(text).text_size(DISPLAY_FONT_SIZE)
267}
268
269fn expanded_button(
272 content: impl WidgetView<Calculator>,
273 callback: impl Fn(&mut Calculator) + Send + Sync + 'static,
274) -> impl WidgetView<Calculator> {
275 const BLUE: Color = Color::from_rgb8(0x00, 0x8d, 0xdd);
276
277 sized_box(
278 button(content, callback)
279 .background_color(BLUE)
280 .corner_radius(10.)
281 .border_color(Color::TRANSPARENT)
282 .hovered_border_color(Color::WHITE),
283 )
284 .expand()
285}
286
287fn operator_button(math_operator: MathOperator) -> impl WidgetView<Calculator> {
290 expanded_button(
291 label(math_operator.as_str()),
292 move |data: &mut Calculator| {
293 data.on_entered_operator(math_operator);
294 },
295 )
296}
297
298fn digit_button(digit: &'static str) -> impl WidgetView<Calculator> {
300 const GRAY: Color = Color::from_rgb8(0x3a, 0x3a, 0x3a);
301
302 sized_box(
303 text_button(digit, |data: &mut Calculator| {
304 data.on_entered_digit(digit);
305 })
306 .background_color(GRAY)
307 .corner_radius(10.)
308 .border_color(Color::TRANSPARENT),
309 )
310 .expand()
311}
312
313fn run(event_loop: EventLoopBuilder) -> Result<(), EventLoopError> {
314 let data = Calculator {
315 current_num_index: 0,
316 clear_current_entry_on_input: false,
317 numbers: ["".into(), "".into()],
318 result: None,
319 operation: None,
320 };
321
322 let min_window_size = LogicalSize::new(200., 200.);
323 let window_size = LogicalSize::new(400., 500.);
324 let window_options = WindowOptions::new("Calculator").with_min_inner_size(min_window_size);
325 #[cfg(not(target_os = "ios"))]
328 let window_options = window_options.with_initial_inner_size(window_size);
329 let app = Xilem::new_simple(data, app_logic, window_options);
330 app.run_in(event_loop)?;
331 Ok(())
332}
333
334#[expect(clippy::allow_attributes, reason = "No way to specify the condition")]
337#[allow(dead_code, reason = "False positive: needed in not-_android version")]
338fn main() -> Result<(), EventLoopError> {
342 run(EventLoop::with_user_event())
343}
344#[cfg(target_os = "android")]
345#[expect(
347 unsafe_code,
348 reason = "We believe that there are no other declarations using this name in the compiled objects here"
349)]
350#[unsafe(no_mangle)]
351fn android_main(app: winit::platform::android::activity::AndroidApp) {
352 use winit::platform::android::EventLoopBuilderExtAndroid;
353
354 let mut event_loop = EventLoop::with_user_event();
355 event_loop.with_android_app(app);
356
357 run(event_loop).expect("Can create app");
358}