summaryrefslogtreecommitdiff
path: root/source/ui/Ui.mc
blob: a8d1ec76bba050619820e4fcb643b80331f0ff73 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
// Author:  cirakg & claude ai
// Summary: Shared drawing helpers: screen shape, usable width on round screens, theme colours, fonts, icons, text fitting.
//
// Every view draws through these helpers, so round, semi-round, rectangular
// and semi-octagon screens and the dark and light themes are handled in one
// place. Nothing here allocates in the per-second redraw path except fit(),
// which only shortens text that doesn't fit.

import Toybox.Graphics;
import Toybox.Lang;
import Toybox.Math;
import Toybox.System;

module Ui {

    const ELLIPSIS = "…";
    const WARN_COLOR = 0xFF5555;
    const ACCENT_COLOR = 0x00AAAA; // the teal of the launcher icon
    const LIGHT_ACCENT_COLOR = 0x007777;

    // Round and semi-round screens get rings and narrower rows near the top
    // and bottom; the others get bars and full-width rows.
    function isRound() as Boolean {
        var shape = System.getDeviceSettings().screenShape;
        return shape == System.SCREEN_SHAPE_ROUND || shape == System.SCREEN_SHAPE_SEMI_ROUND;
    }

    function isTouch() as Boolean {
        return System.getDeviceSettings().isTouchScreen;
    }

    // Half the usable width at height y: on round screens the half chord of
    // the circle there (minus a margin), elsewhere half the screen width.
    function halfWidthAt(dc as Graphics.Dc, y as Number) as Number {
        var w = dc.getWidth();
        if (!isRound()) {
            return w / 2 - 4;
        }
        var r = w / 2;
        var dy = (y - dc.getHeight() / 2).abs();
        if (dy >= r) {
            return 0;
        }
        return (Math.sqrt(r * r - dy * dy) as Float).toNumber() - 6;
    }

    // The usable half width for a band from y0 to y1 (the narrower end).
    function halfWidthBetween(dc as Graphics.Dc, y0 as Number, y1 as Number) as Number {
        var a = halfWidthAt(dc, y0);
        var b = halfWidthAt(dc, y1);
        return a < b ? a : b;
    }

    function dark() as Boolean {
        return AppState.options.theme == Options.THEME_DARK;
    }

    function fg() as Number {
        return dark() ? Graphics.COLOR_WHITE : Graphics.COLOR_BLACK;
    }

    function bg() as Number {
        return dark() ? Graphics.COLOR_BLACK : Graphics.COLOR_WHITE;
    }

    function dim() as Number {
        return dark() ? Graphics.COLOR_LT_GRAY : Graphics.COLOR_DK_GRAY;
    }

    function accent() as Number {
        return dark() ? ACCENT_COLOR : LIGHT_ACCENT_COLOR;
    }

    // Clear the screen in the theme's background.
    function clear(dc as Graphics.Dc) as Void {
        dc.setColor(fg(), bg());
        dc.clear();
    }

    // The digit font for codes on the detail screen, by the code-size option.
    // Number fonts only hold digits, so Steam codes (letters) use text fonts.
    function codeFont(letters as Boolean) as Graphics.FontType {
        var size = AppState.options.codeSize;
        if (letters) {
            return size == Options.CODE_HUGE ? Graphics.FONT_LARGE : Graphics.FONT_MEDIUM;
        }
        if (size == Options.CODE_NORMAL) {
            return Graphics.FONT_NUMBER_MILD;
        }
        return size == Options.CODE_HUGE ? Graphics.FONT_NUMBER_HOT : Graphics.FONT_NUMBER_MEDIUM;
    }

    // Text shortened with an ellipsis to fit maxWidth pixels.
    function fit(dc as Graphics.Dc, text as String, font as Graphics.FontType, maxWidth as Number) as String {
        if (maxWidth <= 0) {
            return "";
        }
        if (dc.getTextWidthInPixels(text, font) <= maxWidth) {
            return text;
        }
        var length = text.length();
        while (length > 0) {
            length--;
            var shorter = text.substring(0, length) + ELLIPSIS;
            if (dc.getTextWidthInPixels(shorter, font) <= maxWidth) {
                return shorter;
            }
        }
        return "";
    }

    // A small padlock whose body is `size` pixels wide, top-left at (x, y).
    function drawLock(dc as Graphics.Dc, x as Number, y as Number, size as Number, color as Number) as Void {
        dc.setColor(color, Graphics.COLOR_TRANSPARENT);
        var bodyTop = y + size * 2 / 5;
        dc.setPenWidth(size / 6 > 1 ? size / 6 : 1);
        dc.drawArc(x + size / 2, bodyTop, size * 3 / 10, Graphics.ARC_COUNTER_CLOCKWISE, 0, 180);
        dc.fillRectangle(x, bodyTop, size, size * 3 / 5);
        dc.setPenWidth(1);
    }

    // A small barcode icon (vertical bars), `size` pixels square.
    function drawBarcode(dc as Graphics.Dc, x as Number, y as Number, size as Number, color as Number) as Void {
        dc.setColor(color, Graphics.COLOR_TRANSPARENT);
        var widths = [2, 1, 3, 1, 1, 2, 1, 3];
        var unit = size / 16 > 0 ? size / 16 : 1;
        var cx = x;
        for (var i = 0; i < widths.size() && cx < x + size; i++) {
            var w = (widths[i] as Number) * unit;
            if (i % 2 == 0) {
                dc.fillRectangle(cx, y, w, size);
            }
            cx += w + unit;
        }
    }

    // A countdown pie: a ring with the remaining fraction filled.
    function drawCountdown(dc as Graphics.Dc, cx as Number, cy as Number, r as Number, remaining as Number, period as Number) as Void {
        dc.setColor(dim(), Graphics.COLOR_TRANSPARENT);
        dc.setPenWidth(1);
        dc.drawCircle(cx, cy, r);
        if (period <= 0 || remaining <= 0) {
            return;
        }
        dc.setColor(remaining <= 5 ? WARN_COLOR : accent(), Graphics.COLOR_TRANSPARENT);
        var pen = r / 2 > 1 ? r / 2 : 1;
        dc.setPenWidth(pen);
        var sweep = 360 * remaining / period;
        if (sweep >= 360) {
            dc.drawCircle(cx, cy, r - pen / 2);
        } else {
            dc.drawArc(cx, cy, r - pen / 2, Graphics.ARC_CLOCKWISE, 90, (450 - sweep) % 360);
        }
        dc.setPenWidth(1);
    }

    // "m:ss" for a number of seconds.
    function minutesSeconds(seconds as Number) as String {
        return (seconds / 60) + ":" + (seconds % 60).format("%02d");
    }

    // Split text into lines no wider than maxWidth, at spaces where possible.
    function wrap(dc as Graphics.Dc, text as String, font as Graphics.FontType, maxWidth as Number) as Array<String> {
        var lines = [] as Array<String>;
        var words = split(text, " ");
        var line = "";
        for (var i = 0; i < words.size(); i++) {
            var candidate = line.length() == 0 ? words[i] : line + " " + words[i];
            if (dc.getTextWidthInPixels(candidate, font) <= maxWidth || line.length() == 0) {
                line = candidate;
            } else {
                lines.add(line);
                line = words[i];
            }
        }
        if (line.length() > 0) {
            lines.add(line);
        }
        // A single word wider than the line is shortened.
        for (var i = 0; i < lines.size(); i++) {
            lines[i] = fit(dc, lines[i], font, maxWidth);
        }
        return lines;
    }

    // Split text at every occurrence of separator.
    function split(text as String, separator as String) as Array<String> {
        var parts = [] as Array<String>;
        var rest = text;
        var at = rest.find(separator);
        while (at != null) {
            parts.add(rest.substring(0, at) as String);
            rest = rest.substring(at + separator.length(), rest.length()) as String;
            at = rest.find(separator);
        }
        parts.add(rest);
        return parts;
    }

    // True when text is non-empty and only ASCII digits and spaces.
    function isDigits(text as String) as Boolean {
        var chars = text.toCharArray();
        for (var i = 0; i < chars.size(); i++) {
            var c = chars[i];
            if ((c < '0' || c > '9') && c != ' ') {
                return false;
            }
        }
        return chars.size() > 0;
    }
}