calc/
calc.rs

1// Copyright 2018 The Druid Authors.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Simple calculator.
16
17// On Windows platform, don't show a console when opening the app.
18#![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    /// The number displayed. Generally a valid float.
30    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        // TODO: change hyphen-minus to actual minus
48        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}