// Copyright 2018 the Xilem Authors and the Druid Authors // SPDX-License-Identifier: Apache-2.0 //! Simple calculator. // On Windows platform, don't show a console when opening the app. #![windows_subsystem = "windows"] #![allow(variant_size_differences, clippy::single_match)] use accesskit::{DefaultActionVerb, Role}; use masonry::app_driver::{AppDriver, DriverCtx}; use masonry::dpi::LogicalSize; use masonry::widget::{Align, CrossAxisAlignment, Flex, Label, RootWidget, SizedBox}; use masonry::{ AccessCtx, AccessEvent, Action, BoxConstraints, Color, EventCtx, LayoutCtx, LifeCycle, LifeCycleCtx, PaintCtx, Point, PointerEvent, Size, StatusChange, TextEvent, Widget, WidgetId, WidgetPod, }; use smallvec::{smallvec, SmallVec}; use tracing::{trace, trace_span, Span}; use vello::Scene; use winit::window::Window; #[derive(Clone)] struct CalcState { /// The number displayed. Generally a valid float. value: String, operand: f64, operator: char, in_num: bool, } #[derive(Clone, Copy)] enum CalcAction { Digit(u8), Op(char), } struct CalcButton { inner: WidgetPod, action: CalcAction, base_color: Color, active_color: Color, } // --- impl CalcState { fn digit(&mut self, digit: u8) { if !self.in_num { self.value.clear(); self.in_num = true; } let ch = (b'0' + digit) as char; self.value.push(ch); } fn display(&mut self) { self.value = self.operand.to_string(); } fn compute(&mut self) { if self.in_num { let operand2 = self.value.parse().unwrap_or(0.0); let result = match self.operator { '+' => Some(self.operand + operand2), '−' => Some(self.operand - operand2), '×' => Some(self.operand * operand2), '÷' => Some(self.operand / operand2), _ => None, }; if let Some(result) = result { self.operand = result; self.display(); self.in_num = false; } } } fn op(&mut self, op: char) { match op { '+' | '−' | '×' | '÷' | '=' => { self.compute(); self.operand = self.value.parse().unwrap_or(0.0); self.operator = op; self.in_num = false; } '±' => { if self.in_num { if self.value.starts_with('−') { self.value = self.value[3..].to_string(); } else { self.value = ["−", &self.value].concat(); } } else { self.operand = -self.operand; self.display(); } } '.' => { if !self.in_num { self.value = "0".to_string(); self.in_num = true; } if self.value.find('.').is_none() { self.value.push('.'); } } 'c' => { self.value = "0".to_string(); self.in_num = false; } 'C' => { self.value = "0".to_string(); self.operator = 'C'; self.in_num = false; } '⌫' => { if self.in_num { self.value.pop(); if self.value.is_empty() || self.value == "−" { self.value = "0".to_string(); self.in_num = false; } } } _ => unreachable!(), } } } impl CalcButton { fn new(inner: SizedBox, action: CalcAction, base_color: Color, active_color: Color) -> Self { Self { inner: WidgetPod::new(inner), action, base_color, active_color, } } } impl Widget for CalcButton { fn on_pointer_event(&mut self, ctx: &mut EventCtx, event: &PointerEvent) { match event { PointerEvent::PointerDown(_, _) => { if !ctx.is_disabled() { let color = self.active_color; // See on_status_change for why we use `mutate_later` here. ctx.mutate_later(&mut self.inner, move |mut inner| { inner.set_background(color); }); ctx.set_active(true); trace!("CalcButton {:?} pressed", ctx.widget_id()); } } PointerEvent::PointerUp(_, _) => { if ctx.is_active() && !ctx.is_disabled() { let color = self.base_color; // See on_status_change for why we use `mutate_later` here. ctx.mutate_later(&mut self.inner, move |mut inner| { inner.set_background(color); }); ctx.submit_action(Action::Other(Box::new(self.action))); trace!("CalcButton {:?} released", ctx.widget_id()); } ctx.set_active(false); } _ => (), } } fn on_text_event(&mut self, _ctx: &mut EventCtx, _event: &TextEvent) {} fn on_access_event(&mut self, ctx: &mut EventCtx, event: &AccessEvent) { if event.target == ctx.widget_id() { match event.action { accesskit::Action::Default => { ctx.submit_action(Action::Other(Box::new(self.action))); ctx.request_paint(); } _ => {} } } ctx.skip_child(&mut self.inner); } fn on_status_change(&mut self, ctx: &mut LifeCycleCtx, event: &StatusChange) { // Masonry doesn't let us change a widget's attributes directly. // We use `mutate_later` to get a mutable reference to the inner widget // and change its border color. This is a simple way to implement a // "hovered" visual effect, but it's somewhat non-idiomatic compared to // implementing the effect inside the "paint" method. match event { StatusChange::HotChanged(true) => { ctx.mutate_later(&mut self.inner, move |mut inner| { inner.set_border(Color::WHITE, 3.0); }); // FIXME - This is a monkey-patch for a problem where the mutate pass isn't run after this. // Should be fixed once the pass spec RFC is implemented. ctx.request_anim_frame(); } StatusChange::HotChanged(false) => { ctx.mutate_later(&mut self.inner, move |mut inner| { inner.set_border(Color::TRANSPARENT, 3.0); }); // FIXME - This is a monkey-patch for a problem where the mutate pass isn't run after this. // Should be fixed once the pass spec RFC is implemented. ctx.request_anim_frame(); } _ => (), } } fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle) { self.inner.lifecycle(ctx, event); } fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size { let size = self.inner.layout(ctx, bc); ctx.place_child(&mut self.inner, Point::ORIGIN); size } fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut Scene) {} fn accessibility_role(&self) -> Role { Role::Button } fn accessibility(&mut self, ctx: &mut AccessCtx) { let _name = match self.action { CalcAction::Digit(digit) => digit.to_string(), CalcAction::Op(op) => op.to_string(), }; // We may want to add a name if it doesn't interfere with the child label // ctx.current_node().set_name(name); ctx.current_node() .set_default_action_verb(DefaultActionVerb::Click); } fn children_ids(&self) -> SmallVec<[WidgetId; 16]> { smallvec![self.inner.id()] } fn make_trace_span(&self) -> Span { trace_span!("CalcButton") } } impl AppDriver for CalcState { fn on_action(&mut self, ctx: &mut DriverCtx<'_>, _widget_id: WidgetId, action: Action) { match action { Action::Other(payload) => match payload.downcast_ref::().unwrap() { CalcAction::Digit(digit) => self.digit(*digit), CalcAction::Op(op) => self.op(*op), }, _ => unreachable!(), } ctx.get_root::>() .get_element() .child_mut(1) .unwrap() .downcast::