1#![windows_subsystem = "windows"]
19
20use druid::{
21 theme, AppLauncher, Color, Data, Lens, LocalizedString, RenderContext, Widget, WidgetExt,
22 WindowDesc,
23};
24
25use druid::widget::{CrossAxisAlignment, Flex, Label, Painter};
26
27#[derive(Clone, Data, Lens)]
28struct CalcState {
29 value: String,
31 operand: f64,
32 operator: char,
33 in_num: bool,
34}
35
36impl CalcState {
37 fn digit(&mut self, digit: u8) {
38 if !self.in_num {
39 self.value.clear();
40 self.in_num = true;
41 }
42 let ch = (b'0' + digit) as char;
43 self.value.push(ch);
44 }
45
46 fn display(&mut self) {
47 self.value = self.operand.to_string();
49 }
50
51 fn compute(&mut self) {
52 if self.in_num {
53 let operand2 = self.value.parse().unwrap_or(0.0);
54 let result = match self.operator {
55 '+' => Some(self.operand + operand2),
56 '−' => Some(self.operand - operand2),
57 '×' => Some(self.operand * operand2),
58 '÷' => Some(self.operand / operand2),
59 _ => None,
60 };
61 if let Some(result) = result {
62 self.operand = result;
63 self.display();
64 self.in_num = false;
65 }
66 }
67 }
68
69 fn op(&mut self, op: char) {
70 match op {
71 '+' | '−' | '×' | '÷' | '=' => {
72 self.compute();
73 self.operand = self.value.parse().unwrap_or(0.0);
74 self.operator = op;
75 self.in_num = false;
76 }
77 '±' => {
78 if self.in_num {
79 if self.value.starts_with('−') {
80 self.value = self.value[3..].to_string();
81 } else {
82 self.value = ["−", &self.value].concat();
83 }
84 } else {
85 self.operand = -self.operand;
86 self.display();
87 }
88 }
89 '.' => {
90 if !self.in_num {
91 self.value = "0".to_string();
92 self.in_num = true;
93 }
94 if self.value.find('.').is_none() {
95 self.value.push('.');
96 }
97 }
98 'c' => {
99 self.value = "0".to_string();
100 self.in_num = false;
101 }
102 'C' => {
103 self.value = "0".to_string();
104 self.operator = 'C';
105 self.in_num = false;
106 }
107 '⌫' => {
108 if self.in_num {
109 self.value.pop();
110 if self.value.is_empty() || self.value == "−" {
111 self.value = "0".to_string();
112 self.in_num = false;
113 }
114 }
115 }
116 _ => unreachable!(),
117 }
118 }
119}
120
121fn op_button_label(op: char, label: String) -> impl Widget<CalcState> {
122 let painter = Painter::new(|ctx, _, env| {
123 let bounds = ctx.size().to_rect();
124
125 ctx.fill(bounds, &env.get(theme::PRIMARY_DARK));
126
127 if ctx.is_hot() {
128 ctx.stroke(bounds.inset(-0.5), &Color::WHITE, 1.0);
129 }
130
131 if ctx.is_active() {
132 ctx.fill(bounds, &env.get(theme::PRIMARY_LIGHT));
133 }
134 });
135
136 Label::new(label)
137 .with_text_size(24.)
138 .center()
139 .background(painter)
140 .expand()
141 .on_click(move |_ctx, data: &mut CalcState, _env| data.op(op))
142}
143
144fn op_button(op: char) -> impl Widget<CalcState> {
145 op_button_label(op, op.to_string())
146}
147
148fn digit_button(digit: u8) -> impl Widget<CalcState> {
149 let painter = Painter::new(|ctx, _, env| {
150 let bounds = ctx.size().to_rect();
151
152 ctx.fill(bounds, &env.get(theme::BACKGROUND_LIGHT));
153
154 if ctx.is_hot() {
155 ctx.stroke(bounds.inset(-0.5), &Color::WHITE, 1.0);
156 }
157
158 if ctx.is_active() {
159 ctx.fill(bounds, &Color::rgb8(0x71, 0x71, 0x71));
160 }
161 });
162
163 Label::new(format!("{digit}"))
164 .with_text_size(24.)
165 .center()
166 .background(painter)
167 .expand()
168 .on_click(move |_ctx, data: &mut CalcState, _env| data.digit(digit))
169}
170
171fn flex_row<T: Data>(
172 w1: impl Widget<T> + 'static,
173 w2: impl Widget<T> + 'static,
174 w3: impl Widget<T> + 'static,
175 w4: impl Widget<T> + 'static,
176) -> impl Widget<T> {
177 Flex::row()
178 .with_flex_child(w1, 1.0)
179 .with_spacer(1.0)
180 .with_flex_child(w2, 1.0)
181 .with_spacer(1.0)
182 .with_flex_child(w3, 1.0)
183 .with_spacer(1.0)
184 .with_flex_child(w4, 1.0)
185}
186
187fn build_calc() -> impl Widget<CalcState> {
188 let display = Label::new(|data: &String, _env: &_| data.clone())
189 .with_text_size(32.0)
190 .lens(CalcState::value)
191 .padding(5.0);
192 Flex::column()
193 .with_flex_spacer(0.2)
194 .with_child(display)
195 .with_flex_spacer(0.2)
196 .cross_axis_alignment(CrossAxisAlignment::End)
197 .with_flex_child(
198 flex_row(
199 op_button_label('c', "CE".to_string()),
200 op_button('C'),
201 op_button('⌫'),
202 op_button('÷'),
203 ),
204 1.0,
205 )
206 .with_spacer(1.0)
207 .with_flex_child(
208 flex_row(
209 digit_button(7),
210 digit_button(8),
211 digit_button(9),
212 op_button('×'),
213 ),
214 1.0,
215 )
216 .with_spacer(1.0)
217 .with_flex_child(
218 flex_row(
219 digit_button(4),
220 digit_button(5),
221 digit_button(6),
222 op_button('−'),
223 ),
224 1.0,
225 )
226 .with_spacer(1.0)
227 .with_flex_child(
228 flex_row(
229 digit_button(1),
230 digit_button(2),
231 digit_button(3),
232 op_button('+'),
233 ),
234 1.0,
235 )
236 .with_spacer(1.0)
237 .with_flex_child(
238 flex_row(
239 op_button('±'),
240 digit_button(0),
241 op_button('.'),
242 op_button('='),
243 ),
244 1.0,
245 )
246}
247
248pub fn main() {
249 let window = WindowDesc::new(build_calc())
250 .window_size((223., 300.))
251 .resizable(false)
252 .title(
253 LocalizedString::new("calc-demo-window-title").with_placeholder("Simple Calculator"),
254 );
255 let calc_state = CalcState {
256 value: "0".to_string(),
257 operand: 0.0,
258 operator: 'C',
259 in_num: false,
260 };
261 AppLauncher::with_window(window)
262 .log_to_console()
263 .launch(calc_state)
264 .expect("launch failed");
265}