aboutsummaryrefslogtreecommitdiffstats
path: root/src/displayapp
diff options
context:
space:
mode:
authorMax Friedrich <minacode@users.noreply.github.com>2025-05-13 00:32:03 +0200
committerGitHub <noreply@github.com>2025-05-12 23:32:03 +0100
commit5b20e8e2ba8dae54c7ff5c2d26534b9746df2d15 (patch)
tree8f4337f6a30ac75748e159f96ef4461607b696a4 /src/displayapp
parent6a6981c91251b96cdf33fe9b094b0833b00ebd8f (diff)
Simple calculator (#1483)
Co-authored-by: minacode <minamoto9@web.de> Co-authored-by: Finlay Davidson <finlay.davidson@coderclass.nl> Co-authored-by: SuIƓng N. <Boteium@users.noreply.github.com> Co-authored-by: mark9064 <30447455+mark9064@users.noreply.github.com>
Diffstat (limited to 'src/displayapp')
-rw-r--r--src/displayapp/DisplayApp.cpp1
-rw-r--r--src/displayapp/apps/Apps.h.in1
-rw-r--r--src/displayapp/apps/CMakeLists.txt1
-rw-r--r--src/displayapp/fonts/fonts.json2
-rw-r--r--src/displayapp/screens/Calculator.cpp375
-rw-r--r--src/displayapp/screens/Calculator.h83
-rw-r--r--src/displayapp/screens/Symbols.h2
7 files changed, 464 insertions, 1 deletions
diff --git a/src/displayapp/DisplayApp.cpp b/src/displayapp/DisplayApp.cpp
index 6671ac9e..50d6ce45 100644
--- a/src/displayapp/DisplayApp.cpp
+++ b/src/displayapp/DisplayApp.cpp
@@ -30,6 +30,7 @@
#include "displayapp/screens/Weather.h"
#include "displayapp/screens/PassKey.h"
#include "displayapp/screens/Error.h"
+#include "displayapp/screens/Calculator.h"
#include "drivers/Cst816s.h"
#include "drivers/St7789.h"
diff --git a/src/displayapp/apps/Apps.h.in b/src/displayapp/apps/Apps.h.in
index 2104a267..e697096a 100644
--- a/src/displayapp/apps/Apps.h.in
+++ b/src/displayapp/apps/Apps.h.in
@@ -26,6 +26,7 @@ namespace Pinetime {
StopWatch,
Metronome,
Motion,
+ Calculator,
Steps,
Dice,
Weather,
diff --git a/src/displayapp/apps/CMakeLists.txt b/src/displayapp/apps/CMakeLists.txt
index d7858760..33e54323 100644
--- a/src/displayapp/apps/CMakeLists.txt
+++ b/src/displayapp/apps/CMakeLists.txt
@@ -13,6 +13,7 @@ else ()
set(DEFAULT_USER_APP_TYPES "${DEFAULT_USER_APP_TYPES}, Apps::Dice")
set(DEFAULT_USER_APP_TYPES "${DEFAULT_USER_APP_TYPES}, Apps::Metronome")
set(DEFAULT_USER_APP_TYPES "${DEFAULT_USER_APP_TYPES}, Apps::Navigation")
+ set(DEFAULT_USER_APP_TYPES "${DEFAULT_USER_APP_TYPES}, Apps::Calculator")
set(DEFAULT_USER_APP_TYPES "${DEFAULT_USER_APP_TYPES}, Apps::Weather")
#set(DEFAULT_USER_APP_TYPES "${DEFAULT_USER_APP_TYPES}, Apps::Motion")
set(USERAPP_TYPES "${DEFAULT_USER_APP_TYPES}" CACHE STRING "List of user apps to build into the firmware")
diff --git a/src/displayapp/fonts/fonts.json b/src/displayapp/fonts/fonts.json
index 41c383c0..fea31605 100644
--- a/src/displayapp/fonts/fonts.json
+++ b/src/displayapp/fonts/fonts.json
@@ -7,7 +7,7 @@
},
{
"file": "FontAwesome5-Solid+Brands+Regular.woff",
- "range": "0xf294, 0xf242, 0xf54b, 0xf21e, 0xf1e6, 0xf017, 0xf129, 0xf03a, 0xf185, 0xf560, 0xf001, 0xf3fd, 0xf1fc, 0xf45d, 0xf59f, 0xf5a0, 0xf027, 0xf028, 0xf6a9, 0xf04b, 0xf04c, 0xf048, 0xf051, 0xf095, 0xf3dd, 0xf04d, 0xf2f2, 0xf024, 0xf252, 0xf569, 0xf06e, 0xf015, 0xf00c, 0xf0f3, 0xf522, 0xf743"
+ "range": "0xf294, 0xf242, 0xf54b, 0xf21e, 0xf1e6, 0xf017, 0xf129, 0xf03a, 0xf185, 0xf560, 0xf001, 0xf3fd, 0xf1fc, 0xf45d, 0xf59f, 0xf5a0, 0xf027, 0xf028, 0xf6a9, 0xf04b, 0xf04c, 0xf048, 0xf051, 0xf095, 0xf3dd, 0xf04d, 0xf2f2, 0xf024, 0xf252, 0xf569, 0xf06e, 0xf015, 0xf00c, 0xf0f3, 0xf522, 0xf743, 0xf1ec, 0xf55a"
}
],
"bpp": 1,
diff --git a/src/displayapp/screens/Calculator.cpp b/src/displayapp/screens/Calculator.cpp
new file mode 100644
index 00000000..a1f09383
--- /dev/null
+++ b/src/displayapp/screens/Calculator.cpp
@@ -0,0 +1,375 @@
+#include <cmath>
+#include <cinttypes>
+#include "Calculator.h"
+#include "displayapp/InfiniTimeTheme.h"
+#include "Symbols.h"
+
+using namespace Pinetime::Applications::Screens;
+
+static void eventHandler(lv_obj_t* obj, lv_event_t event) {
+ auto app = static_cast<Calculator*>(obj->user_data);
+ app->OnButtonEvent(obj, event);
+}
+
+Calculator::~Calculator() {
+ lv_obj_clean(lv_scr_act());
+}
+
+constexpr const char* const buttonMap[] = {
+ "7", "8", "9", Symbols::backspace, "\n", "4", "5", "6", "+ -", "\n", "1", "2", "3", "* /", "\n", "0", ".", "(-)", "=", ""};
+
+Calculator::Calculator() {
+ resultLabel = lv_label_create(lv_scr_act(), nullptr);
+ lv_label_set_long_mode(resultLabel, LV_LABEL_LONG_CROP);
+ lv_label_set_align(resultLabel, LV_LABEL_ALIGN_RIGHT);
+ lv_label_set_text_fmt(resultLabel, "%" PRId64, result);
+ lv_obj_set_size(resultLabel, 200, 20);
+ lv_obj_set_pos(resultLabel, 10, 5);
+
+ valueLabel = lv_label_create(lv_scr_act(), nullptr);
+ lv_label_set_long_mode(valueLabel, LV_LABEL_LONG_CROP);
+ lv_label_set_align(valueLabel, LV_LABEL_ALIGN_RIGHT);
+ lv_label_set_text_fmt(valueLabel, "%" PRId64, value);
+ lv_obj_set_size(valueLabel, 200, 20);
+ lv_obj_set_pos(valueLabel, 10, 35);
+
+ buttonMatrix = lv_btnmatrix_create(lv_scr_act(), nullptr);
+ buttonMatrix->user_data = this;
+ lv_obj_set_event_cb(buttonMatrix, eventHandler);
+ lv_btnmatrix_set_map(buttonMatrix, const_cast<const char**>(buttonMap));
+ lv_btnmatrix_set_one_check(buttonMatrix, true);
+ lv_obj_set_size(buttonMatrix, 238, 180);
+ lv_obj_set_style_local_bg_color(buttonMatrix, LV_BTNMATRIX_PART_BTN, LV_STATE_DEFAULT, Colors::bgAlt);
+ lv_obj_set_style_local_pad_inner(buttonMatrix, LV_BTNMATRIX_PART_BG, LV_STATE_DEFAULT, 1);
+ lv_obj_set_style_local_pad_top(buttonMatrix, LV_BTNMATRIX_PART_BG, LV_STATE_DEFAULT, 1);
+ lv_obj_set_style_local_pad_bottom(buttonMatrix, LV_BTNMATRIX_PART_BG, LV_STATE_DEFAULT, 1);
+ lv_obj_set_style_local_pad_left(buttonMatrix, LV_BTNMATRIX_PART_BG, LV_STATE_DEFAULT, 1);
+ lv_obj_set_style_local_pad_right(buttonMatrix, LV_BTNMATRIX_PART_BG, LV_STATE_DEFAULT, 1);
+ lv_obj_align(buttonMatrix, nullptr, LV_ALIGN_IN_BOTTOM_MID, 0, 0);
+
+ lv_obj_set_style_local_bg_opa(buttonMatrix, LV_BTNMATRIX_PART_BTN, LV_STATE_CHECKED, LV_OPA_COVER);
+ lv_obj_set_style_local_bg_grad_stop(buttonMatrix, LV_BTNMATRIX_PART_BTN, LV_STATE_CHECKED, 128);
+ lv_obj_set_style_local_bg_main_stop(buttonMatrix, LV_BTNMATRIX_PART_BTN, LV_STATE_CHECKED, 128);
+}
+
+void Calculator::OnButtonEvent(lv_obj_t* obj, lv_event_t event) {
+ if ((obj == buttonMatrix) && (event == LV_EVENT_PRESSED)) {
+ HandleInput();
+ }
+}
+
+void Calculator::HandleInput() {
+ const char* buttonText = lv_btnmatrix_get_active_btn_text(buttonMatrix);
+
+ if (buttonText == nullptr) {
+ return;
+ }
+
+ if ((equalSignPressedBefore && (*buttonText != '=')) || (error != Error::None)) {
+ ResetInput();
+ UpdateOperation();
+ }
+
+ // we only compare the first char because it is enough
+ switch (*buttonText) {
+ case '0':
+ case '1':
+ case '2':
+ case '3':
+ case '4':
+ case '5':
+ case '6':
+ case '7':
+ case '8':
+ case '9': {
+ // *buttonText is the first char in buttonText
+ // "- '0'" results in the int value of the char
+ uint8_t digit = (*buttonText) - '0';
+ int8_t sign = (value < 0) ? -1 : 1;
+
+ // if this is true, we already pressed the . button
+ if (offset < FIXED_POINT_OFFSET) {
+ value += sign * offset * digit;
+ offset /= 10;
+ } else if (value <= MAX_VALUE / 10) {
+ value *= 10;
+ value += sign * offset * digit;
+ }
+ } break;
+
+ // unary minus
+ case '(':
+ value = -value;
+ break;
+
+ case '.':
+ if (offset == FIXED_POINT_OFFSET) {
+ offset /= 10;
+ }
+ break;
+
+ // for every operator we:
+ // - eval the current operator if value > FIXED_POINT_OFFSET
+ // - then set the new operator
+ // - + and - as well as * and / cycle on the same button
+ case '+':
+ if (value != 0) {
+ Eval();
+ ResetInput();
+ }
+
+ switch (operation) {
+ case '+':
+ operation = '-';
+ break;
+ case '-':
+ operation = ' ';
+ break;
+ default:
+ operation = '+';
+ break;
+ }
+ UpdateOperation();
+ break;
+
+ case '*':
+ if (value != 0) {
+ Eval();
+ ResetInput();
+ }
+
+ switch (operation) {
+ case '*':
+ operation = '/';
+ break;
+ case '/':
+ operation = ' ';
+ break;
+ default:
+ operation = '*';
+ break;
+ }
+ UpdateOperation();
+ break;
+
+ // this is a little hacky because it matches only the first char
+ case Symbols::backspace[0]:
+ if (value != 0) {
+ // delete one value digit
+ if (offset < FIXED_POINT_OFFSET) {
+ if (offset == 0) {
+ offset = 1;
+ } else {
+ offset *= 10;
+ }
+ } else {
+ value /= 10;
+ }
+ if (offset < FIXED_POINT_OFFSET) {
+ value -= value % (10 * offset);
+ } else {
+ value -= value % offset;
+ }
+ } else if (offset < FIXED_POINT_OFFSET) {
+ if (offset == 0) {
+ offset = 1;
+ } else {
+ offset *= 10;
+ }
+ } else {
+ // reset the result
+ result = 0;
+ }
+
+ if (value == 0) {
+ operation = ' ';
+ UpdateOperation();
+ }
+ break;
+
+ case '=':
+ equalSignPressedBefore = true;
+ Eval();
+ // If the operation is ' ' then we move the value to the result.
+ // We reset the input after this.
+ // This seems more convenient.
+ if (operation == ' ') {
+ ResetInput();
+ }
+ break;
+ }
+
+ UpdateValueLabel();
+ UpdateResultLabel();
+}
+
+void Calculator::UpdateOperation() const {
+ switch (operation) {
+ case '+':
+ lv_obj_set_style_local_bg_grad_dir(buttonMatrix, LV_BTNMATRIX_PART_BTN, LV_STATE_CHECKED, LV_GRAD_DIR_HOR);
+ lv_obj_set_style_local_bg_color(buttonMatrix, LV_BTNMATRIX_PART_BTN, LV_STATE_CHECKED, Colors::deepOrange);
+ lv_obj_set_style_local_bg_grad_color(buttonMatrix, LV_BTNMATRIX_PART_BTN, LV_STATE_CHECKED, Colors::bgAlt);
+ lv_btnmatrix_set_btn_ctrl(buttonMatrix, 7, LV_BTNMATRIX_CTRL_CHECK_STATE);
+ break;
+ case '-':
+ lv_obj_set_style_local_bg_grad_dir(buttonMatrix, LV_BTNMATRIX_PART_BTN, LV_STATE_CHECKED, LV_GRAD_DIR_HOR);
+ lv_obj_set_style_local_bg_color(buttonMatrix, LV_BTNMATRIX_PART_BTN, LV_STATE_CHECKED, Colors::bgAlt);
+ lv_obj_set_style_local_bg_grad_color(buttonMatrix, LV_BTNMATRIX_PART_BTN, LV_STATE_CHECKED, Colors::deepOrange);
+ lv_btnmatrix_set_btn_ctrl(buttonMatrix, 7, LV_BTNMATRIX_CTRL_CHECK_STATE);
+ break;
+ case '*':
+ lv_obj_set_style_local_bg_grad_dir(buttonMatrix, LV_BTNMATRIX_PART_BTN, LV_STATE_CHECKED, LV_GRAD_DIR_HOR);
+ lv_obj_set_style_local_bg_color(buttonMatrix, LV_BTNMATRIX_PART_BTN, LV_STATE_CHECKED, Colors::deepOrange);
+ lv_obj_set_style_local_bg_grad_color(buttonMatrix, LV_BTNMATRIX_PART_BTN, LV_STATE_CHECKED, Colors::bgAlt);
+ lv_btnmatrix_set_btn_ctrl(buttonMatrix, 11, LV_BTNMATRIX_CTRL_CHECK_STATE);
+ break;
+ case '/':
+ lv_obj_set_style_local_bg_grad_dir(buttonMatrix, LV_BTNMATRIX_PART_BTN, LV_STATE_CHECKED, LV_GRAD_DIR_HOR);
+ lv_obj_set_style_local_bg_color(buttonMatrix, LV_BTNMATRIX_PART_BTN, LV_STATE_CHECKED, Colors::bgAlt);
+ lv_obj_set_style_local_bg_grad_color(buttonMatrix, LV_BTNMATRIX_PART_BTN, LV_STATE_CHECKED, Colors::deepOrange);
+ lv_btnmatrix_set_btn_ctrl(buttonMatrix, 11, LV_BTNMATRIX_CTRL_CHECK_STATE);
+ break;
+ default:
+ lv_btnmatrix_clear_btn_ctrl_all(buttonMatrix, LV_BTNMATRIX_CTRL_CHECK_STATE);
+ break;
+ }
+}
+
+void Calculator::ResetInput() {
+ value = 0;
+ offset = FIXED_POINT_OFFSET;
+ operation = ' ';
+ equalSignPressedBefore = false;
+ error = Error::None;
+}
+
+void Calculator::UpdateResultLabel() const {
+ int64_t integer = result / FIXED_POINT_OFFSET;
+ int64_t remainder = result % FIXED_POINT_OFFSET;
+ bool negative = (remainder < 0);
+
+ if (remainder == 0) {
+ lv_label_set_text_fmt(resultLabel, "%" PRId64, integer);
+ return;
+ }
+
+ if (remainder < 0) {
+ remainder = -remainder;
+ }
+
+ uint8_t minWidth = N_DECIMALS;
+
+ // cut "0"-digits on the right
+ while ((remainder > 0) && (remainder % 10 == 0)) {
+ remainder /= 10;
+ minWidth--;
+ }
+
+ if ((integer == 0) && negative) {
+ lv_label_set_text_fmt(resultLabel, "-0.%0*" PRId64, minWidth, remainder);
+ } else {
+ lv_label_set_text_fmt(resultLabel, "%" PRId64 ".%0*" PRId64, integer, minWidth, remainder);
+ }
+}
+
+void Calculator::UpdateValueLabel() {
+ switch (error) {
+ case Error::TooLarge:
+ lv_label_set_text_static(valueLabel, "too large");
+ break;
+ case Error::ZeroDivision:
+ lv_label_set_text_static(valueLabel, "zero division");
+ break;
+ case Error::None:
+ default: {
+ int64_t integer = value / FIXED_POINT_OFFSET;
+ int64_t remainder = value % FIXED_POINT_OFFSET;
+ bool negative = (remainder < 0);
+
+ int64_t printRemainder = remainder < 0 ? -remainder : remainder;
+
+ uint8_t minWidth = 0;
+ int64_t tmpOffset = offset;
+
+ if (tmpOffset == 0) {
+ tmpOffset = 1;
+ minWidth = 1;
+ }
+ while (tmpOffset < FIXED_POINT_OFFSET) {
+ tmpOffset *= 10;
+ minWidth++;
+ }
+ minWidth--;
+
+ for (uint8_t i = minWidth; i < N_DECIMALS; i++) {
+ printRemainder /= 10;
+ }
+
+ if ((integer == 0) && negative) {
+ lv_label_set_text_fmt(valueLabel, "-0.%0*" PRId64, minWidth, printRemainder);
+ } else if (offset == FIXED_POINT_OFFSET) {
+ lv_label_set_text_fmt(valueLabel, "%" PRId64, integer);
+ } else if ((offset == (FIXED_POINT_OFFSET / 10)) && (remainder == 0)) {
+ lv_label_set_text_fmt(valueLabel, "%" PRId64 ".", integer);
+ } else {
+ lv_label_set_text_fmt(valueLabel, "%" PRId64 ".%0*" PRId64, integer, minWidth, printRemainder);
+ }
+ } break;
+ }
+}
+
+// update the result based on value and operation
+void Calculator::Eval() {
+ switch (operation) {
+ case ' ':
+ result = value;
+ break;
+
+ case '+':
+ // check for overflow
+ if (((result > 0) && (value > (MAX_VALUE - result))) || ((result < 0) && (value < (MIN_VALUE - result)))) {
+ error = Error::TooLarge;
+ break;
+ }
+
+ result += value;
+ break;
+ case '-':
+ // check for overflow
+ if (((result < 0) && (value > (MAX_VALUE + result))) || ((result > 0) && (value < (MIN_VALUE + result)))) {
+ error = Error::TooLarge;
+ break;
+ }
+
+ result -= value;
+ break;
+ case '*':
+ // check for overflow
+ // while dividing we eliminate the fixed point offset
+ // therefore we have to multiply it again for the comparison with value
+ // we also assume here that MAX_VALUE == -MIN_VALUE
+ if ((result != 0) && (std::abs(value) > (FIXED_POINT_OFFSET * (MAX_VALUE / std::abs(result))))) {
+ error = Error::TooLarge;
+ break;
+ }
+
+ result *= value;
+ // fixed point offset was multiplied too
+ result /= FIXED_POINT_OFFSET;
+ break;
+ case '/':
+ // check for zero division
+ if (value == 0) {
+ error = Error::ZeroDivision;
+ break;
+ }
+
+ // fixed point offset will be divided too
+ result *= FIXED_POINT_OFFSET;
+ result /= value;
+ break;
+
+ default:
+ break;
+ }
+}
diff --git a/src/displayapp/screens/Calculator.h b/src/displayapp/screens/Calculator.h
new file mode 100644
index 00000000..9971f275
--- /dev/null
+++ b/src/displayapp/screens/Calculator.h
@@ -0,0 +1,83 @@
+#pragma once
+
+#include "displayapp/screens/Screen.h"
+#include "displayapp/apps/Apps.h"
+#include "displayapp/Controllers.h"
+#include "Symbols.h"
+
+namespace {
+ constexpr int64_t powi(int64_t base, uint8_t exponent) {
+ int64_t value = 1;
+ while (exponent) {
+ value *= base;
+ exponent--;
+ }
+ return value;
+ }
+}
+
+namespace Pinetime {
+ namespace Applications {
+ namespace Screens {
+ class Calculator : public Screen {
+ public:
+ ~Calculator() override;
+
+ Calculator();
+
+ void OnButtonEvent(lv_obj_t* obj, lv_event_t event);
+
+ private:
+ lv_obj_t* buttonMatrix {};
+ lv_obj_t* valueLabel {};
+ lv_obj_t* resultLabel {};
+
+ void Eval();
+ void ResetInput();
+ void HandleInput();
+ void UpdateValueLabel();
+ void UpdateResultLabel() const;
+ void UpdateOperation() const;
+
+ // change this if you want to change the number of decimals
+ // keep in mind, that we only have 12 digits in total (see MAX_DIGITS)
+ // due to the fixed point implementation
+ static constexpr uint8_t N_DECIMALS = 3;
+ // this is the constant default offset
+ static constexpr int64_t FIXED_POINT_OFFSET = powi(10, N_DECIMALS);
+ // this is the current offset, may vary after pressing '.'
+ int64_t offset = FIXED_POINT_OFFSET;
+
+ // the screen can show 12 chars
+ // but two are needed for '.' and '-'
+ static constexpr uint8_t MAX_DIGITS = 12;
+ static constexpr int64_t MAX_VALUE = powi(10, MAX_DIGITS) - 1;
+ // this is assumed in the multiplication overflow!
+ static constexpr int64_t MIN_VALUE = -MAX_VALUE;
+
+ int64_t value = 0;
+ int64_t result = 0;
+ char operation = ' ';
+ bool equalSignPressedBefore = false;
+
+ enum Error {
+ TooLarge,
+ ZeroDivision,
+ None,
+ };
+
+ Error error = Error::None;
+ };
+ }
+
+ template <>
+ struct AppTraits<Apps::Calculator> {
+ static constexpr Apps app = Apps::Calculator;
+ static constexpr const char* icon = Screens::Symbols::calculator;
+
+ static Screens::Screen* Create(AppControllers& /* controllers */) {
+ return new Screens::Calculator();
+ };
+ };
+ }
+}
diff --git a/src/displayapp/screens/Symbols.h b/src/displayapp/screens/Symbols.h
index bd958b28..40699b3d 100644
--- a/src/displayapp/screens/Symbols.h
+++ b/src/displayapp/screens/Symbols.h
@@ -39,6 +39,8 @@ namespace Pinetime {
static constexpr const char* eye = "\xEF\x81\xAE";
static constexpr const char* home = "\xEF\x80\x95";
static constexpr const char* sleep = "\xEE\xBD\x84";
+ static constexpr const char* calculator = "\xEF\x87\xAC";
+ static constexpr const char* backspace = "\xEF\x95\x9A";
// fontawesome_weathericons.c
// static constexpr const char* sun = "\xEF\x86\x85";