summaryrefslogtreecommitdiff
path: root/src/ui
diff options
context:
space:
mode:
Diffstat (limited to 'src/ui')
-rw-r--r--src/ui/color.c27
-rw-r--r--src/ui/color.h48
-rw-r--r--src/ui/command.c89
-rw-r--r--src/ui/command.h16
-rw-r--r--src/ui/inputwidget.c280
-rw-r--r--src/ui/inputwidget.h20
-rw-r--r--src/ui/labelwidget.c281
-rw-r--r--src/ui/labelwidget.h22
-rw-r--r--src/ui/macos.h9
-rw-r--r--src/ui/macos.m434
-rw-r--r--src/ui/metrics.c16
-rw-r--r--src/ui/metrics.h9
-rw-r--r--src/ui/paint.c58
-rw-r--r--src/ui/paint.h33
-rw-r--r--src/ui/text.c533
-rw-r--r--src/ui/text.h52
-rw-r--r--src/ui/util.c604
-rw-r--r--src/ui/util.h110
-rw-r--r--src/ui/widget.c618
-rw-r--r--src/ui/widget.h131
-rw-r--r--src/ui/window.c441
-rw-r--r--src/ui/window.h34
22 files changed, 3865 insertions, 0 deletions
diff --git a/src/ui/color.c b/src/ui/color.c
new file mode 100644
index 00000000..365bf986
--- /dev/null
+++ b/src/ui/color.c
@@ -0,0 +1,27 @@
1#include "color.h"
2
3static const iColor transparent_;
4
5iColor get_Color(int color) {
6 static const iColor palette[] = {
7 { 0, 0, 0, 255 },
8 { 40, 40, 40, 255 },
9 { 80, 80, 80, 255 },
10 { 160, 160, 160, 255 },
11 { 255, 255, 255, 255 },
12 { 106, 80, 0, 255 },
13 { 255, 192, 0, 255 },
14 { 0, 96, 128, 255 },
15 { 0, 192, 255, 255 },
16 { 255, 255, 32, 255 },
17 { 255, 64, 64, 255 },
18 { 255, 0, 255, 255 },
19 { 132, 132, 255, 255 },
20 { 0, 200, 0, 255 },
21 };
22 const iColor *clr = &transparent_;
23 if (color >= 0 && color < (int) iElemCount(palette)) {
24 clr = &palette[color];
25 }
26 return *clr;
27}
diff --git a/src/ui/color.h b/src/ui/color.h
new file mode 100644
index 00000000..f0244fc2
--- /dev/null
+++ b/src/ui/color.h
@@ -0,0 +1,48 @@
1#pragma once
2
3#include <the_Foundation/defs.h>
4
5enum iColorId {
6 none_ColorId = -1,
7 black_ColorId,
8 gray25_ColorId,
9 gray50_ColorId,
10 gray75_ColorId,
11 white_ColorId,
12 brown_ColorId,
13 orange_ColorId,
14 teal_ColorId,
15 cyan_ColorId,
16 yellow_ColorId,
17 red_ColorId,
18 magenta_ColorId,
19 blue_ColorId,
20 green_ColorId,
21 max_ColorId
22};
23
24#define mask_ColorId 0x0f
25#define permanent_ColorId 0x80 /* cannot be changed via escapes */
26
27#define black_ColorEscape "\r0"
28#define gray25_ColorEscape "\r1"
29#define gray50_ColorEscape "\r2"
30#define gray75_ColorEscape "\r3"
31#define white_ColorEscape "\r4"
32#define brown_ColorEscape "\r5"
33#define orange_ColorEscape "\r6"
34#define teal_ColorEscape "\r7"
35#define cyan_ColorEscape "\r8"
36#define yellow_ColorEscape "\r9"
37#define red_ColorEscape "\r:"
38#define magenta_ColorEscape "\r;"
39#define blue_ColorEscape "\r<"
40#define green_ColorEscape "\r="
41
42iDeclareType(Color)
43
44struct Impl_Color {
45 uint8_t r, g, b, a;
46};
47
48iColor get_Color (int color);
diff --git a/src/ui/command.c b/src/ui/command.c
new file mode 100644
index 00000000..16a0d948
--- /dev/null
+++ b/src/ui/command.c
@@ -0,0 +1,89 @@
1#include "command.h"
2#include "app.h"
3
4#include <the_Foundation/string.h>
5#include <ctype.h>
6
7iBool equal_Command(const char *cmdWithArgs, const char *cmd) {
8 if (strchr(cmdWithArgs, ':')) {
9 return beginsWith_CStr(cmdWithArgs, cmd) && cmdWithArgs[strlen(cmd)] == ' ';
10 }
11 return equal_CStr(cmdWithArgs, cmd);
12}
13
14static const iString *tokenString_(const char *label) {
15 return collectNewFormat_String(" %s:", label);
16}
17
18int argLabel_Command(const char *cmd, const char *label) {
19 const iString *tok = tokenString_(label);
20 const char *ptr = strstr(cmd, cstr_String(tok));
21 if (ptr) {
22 return atoi(ptr + size_String(tok));
23 }
24 return 0;
25}
26
27int arg_Command(const char *cmd) {
28 return argLabel_Command(cmd, "arg");
29}
30
31float argf_Command(const char *cmd) {
32 const char *ptr = strstr(cmd, " arg:");
33 if (ptr) {
34 return strtof(ptr + 5, NULL);
35 }
36 return 0;
37}
38
39void *pointerLabel_Command(const char *cmd, const char *label) {
40 const iString *tok = tokenString_(label);
41 const char *ptr = strstr(cmd, cstr_String(tok));
42 if (ptr) {
43 void *val = NULL;
44 sscanf(ptr + size_String(tok), "%p", &val);
45 return val;
46 }
47 return NULL;
48}
49
50void *pointer_Command(const char *cmd) {
51 return pointerLabel_Command(cmd, "ptr");
52}
53
54const char *valuePtr_Command(const char *cmd, const char *label) {
55 const iString *tok = tokenString_(label);
56 const char *ptr = strstr(cmd, cstr_String(tok));
57 if (ptr) {
58 return ptr + size_String(tok);
59 }
60 return NULL;
61}
62
63const iString *string_Command(const char *cmd, const char *label) {
64 iRangecc val = { valuePtr_Command(cmd, label), NULL };
65 if (val.start) {
66 for (val.end = val.start; *val.end && !isspace(*val.end); val.end++) {}
67 return collect_String(newRange_String(&val));
68 }
69 return collectNew_String();
70}
71
72iInt2 dir_Command(const char *cmd) {
73 const char *ptr = strstr(cmd, " dir:");
74 if (ptr) {
75 iInt2 dir;
76 sscanf(ptr + 5, "%d%d", &dir.x, &dir.y);
77 return dir;
78 }
79 return zero_I2();
80}
81
82iInt2 coord_Command(const char *cmd) {
83 iInt2 coord = zero_I2();
84 const char *ptr = strstr(cmd, " coord:");
85 if (ptr) {
86 sscanf(ptr + 7, "%d%d", &coord.x, &coord.y);
87 }
88 return coord;
89}
diff --git a/src/ui/command.h b/src/ui/command.h
new file mode 100644
index 00000000..84533d77
--- /dev/null
+++ b/src/ui/command.h
@@ -0,0 +1,16 @@
1#pragma once
2
3#include <the_Foundation/vec2.h>
4
5iBool equal_Command (const char *commandWithArgs, const char *command);
6
7int arg_Command (const char *); /* arg: */
8float argf_Command (const char *); /* arg: */
9int argLabel_Command (const char *, const char *label);
10void * pointer_Command (const char *); /* ptr: */
11void * pointerLabel_Command (const char *, const char *label);
12iInt2 coord_Command (const char *);
13iInt2 dir_Command (const char *);
14
15const iString * string_Command (const char *, const char *label);
16const char * valuePtr_Command(const char *, const char *label);
diff --git a/src/ui/inputwidget.c b/src/ui/inputwidget.c
new file mode 100644
index 00000000..7a2b7d1c
--- /dev/null
+++ b/src/ui/inputwidget.c
@@ -0,0 +1,280 @@
1#include "inputwidget.h"
2#include "paint.h"
3#include "util.h"
4
5#include <the_Foundation/array.h>
6#include <SDL_timer.h>
7
8struct Impl_InputWidget {
9 iWidget widget;
10 enum iInputMode mode;
11 size_t maxLen;
12 iArray text; /* iChar[] */
13 iArray oldText; /* iChar[] */
14 size_t cursor;
15 int font;
16 iClick click;
17};
18
19iDefineObjectConstructionArgs(InputWidget, (size_t maxLen), maxLen)
20
21void init_InputWidget(iInputWidget *d, size_t maxLen) {
22 iWidget *w = &d->widget;
23 init_Widget(w);
24 setFlags_Widget(w, focusable_WidgetFlag | hover_WidgetFlag, iTrue);
25 init_Array(&d->text, sizeof(iChar));
26 init_Array(&d->oldText, sizeof(iChar));
27 d->font = uiInput_FontId;
28 d->cursor = 0;
29 setMaxLen_InputWidget(d, maxLen);
30 if (maxLen == 0) {
31 /* Caller must arrange the width. */
32 w->rect.size.y = lineHeight_Text(d->font) + 2 * gap_UI;
33 setFlags_Widget(w, fixedHeight_WidgetFlag, iTrue);
34 }
35 init_Click(&d->click, d, SDL_BUTTON_LEFT);
36}
37
38void deinit_InputWidget(iInputWidget *d) {
39 deinit_Array(&d->oldText);
40 deinit_Array(&d->text);
41}
42
43void setMode_InputWidget(iInputWidget *d, enum iInputMode mode) {
44 d->mode = mode;
45}
46
47const iString *text_InputWidget(const iInputWidget *d) {
48 return collect_String(newUnicodeN_String(constData_Array(&d->text), size_Array(&d->text)));
49}
50
51void setMaxLen_InputWidget(iInputWidget *d, size_t maxLen) {
52 d->maxLen = maxLen;
53 d->mode = (maxLen == 0 ? insert_InputMode : overwrite_InputMode);
54 resize_Array(&d->text, maxLen);
55 if (maxLen) {
56 /* Set a fixed size. */
57 iBlock *content = new_Block(maxLen);
58 fill_Block(content, 'M');
59 setSize_Widget(
60 as_Widget(d),
61 add_I2(measure_Text(d->font, cstr_Block(content)), init_I2(6 * gap_UI, 2 * gap_UI)));
62 delete_Block(content);
63 }
64}
65
66void setText_InputWidget(iInputWidget *d, const iString *text) {
67 clear_Array(&d->text);
68 iConstForEach(String, i, text) {
69 pushBack_Array(&d->text, &i.value);
70 }
71}
72
73void setCursor_InputWidget(iInputWidget *d, size_t pos) {
74 d->cursor = iMin(pos, size_Array(&d->text));
75}
76
77void begin_InputWidget(iInputWidget *d) {
78 iWidget *w = as_Widget(d);
79 if (flags_Widget(w) & selected_WidgetFlag) {
80 /* Already active. */
81 return;
82 }
83 setFlags_Widget(w, hidden_WidgetFlag | disabled_WidgetFlag, iFalse);
84 setCopy_Array(&d->oldText, &d->text);
85 if (d->mode == overwrite_InputMode) {
86 d->cursor = 0;
87 }
88 else {
89 d->cursor = iMin(size_Array(&d->text), d->maxLen - 1);
90 }
91 SDL_StartTextInput();
92 setFlags_Widget(w, selected_WidgetFlag, iTrue);
93}
94
95void end_InputWidget(iInputWidget *d, iBool accept) {
96 iWidget *w = as_Widget(d);
97 if (~flags_Widget(w) & selected_WidgetFlag) {
98 /* Was not active. */
99 return;
100 }
101 if (!accept) {
102 setCopy_Array(&d->text, &d->oldText);
103 }
104 SDL_StopTextInput();
105 setFlags_Widget(w, selected_WidgetFlag, iFalse);
106 const char *id = cstr_String(id_Widget(as_Widget(d)));
107 if (!*id) id = "_";
108 postCommand_Widget(w, "input.ended id:%s arg:%d", id, accept ? 1 : 0);
109}
110
111static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) {
112 if (isCommand_Widget(as_Widget(d), ev, "focus.gained")) {
113 begin_InputWidget(d);
114 return iTrue;
115 }
116 else if (isCommand_Widget(as_Widget(d), ev, "focus.lost")) {
117 end_InputWidget(d, iTrue);
118 return iTrue;
119 }
120 switch (processEvent_Click(&d->click, ev)) {
121 case none_ClickResult:
122 break;
123 case started_ClickResult:
124 case drag_ClickResult:
125 case double_ClickResult:
126 case aborted_ClickResult:
127 return iTrue;
128 case finished_ClickResult:
129 setFocus_Widget(as_Widget(d));
130 return iTrue;
131 }
132 if (ev->type == SDL_KEYUP) {
133 return iTrue;
134 }
135 const size_t curMax = iMin(size_Array(&d->text), d->maxLen - 1);
136 if (ev->type == SDL_KEYDOWN && isFocused_Widget(as_Widget(d))) {
137 const int key = ev->key.keysym.sym;
138 const int mods = keyMods_Sym(ev->key.keysym.mod);
139 switch (key) {
140 case SDLK_RETURN:
141 case SDLK_KP_ENTER:
142 setFocus_Widget(NULL);
143 return iTrue;
144 case SDLK_ESCAPE:
145 end_InputWidget(d, iFalse);
146 setFocus_Widget(NULL);
147 return iTrue;
148 case SDLK_BACKSPACE:
149 if (mods & KMOD_ALT) {
150 clear_Array(&d->text);
151 d->cursor = 0;
152 }
153 else if (d->cursor > 0) {
154 remove_Array(&d->text, --d->cursor);
155 }
156 return iTrue;
157 case SDLK_d:
158 if (mods != KMOD_CTRL) break;
159 case SDLK_DELETE:
160 if (d->cursor < size_Array(&d->text)) {
161 remove_Array(&d->text, d->cursor);
162 }
163 return iTrue;
164 case SDLK_k:
165 if (mods == KMOD_CTRL) {
166 removeN_Array(&d->text, d->cursor, size_Array(&d->text) - d->cursor);
167 return iTrue;
168 }
169 break;
170 case SDLK_HOME:
171 case SDLK_END:
172 d->cursor = (key == SDLK_HOME ? 0 : curMax);
173 return iTrue;
174 case SDLK_a:
175 case SDLK_e:
176 if (mods == KMOD_CTRL) {
177 d->cursor = (key == 'a' ? 0 : curMax);
178 return iTrue;
179 }
180 break;
181 case SDLK_LEFT:
182 if (mods & KMOD_PRIMARY) {
183 d->cursor = 0;
184 }
185 else if (d->cursor > 0) {
186 d->cursor--;
187 }
188 return iTrue;
189 case SDLK_RIGHT:
190 if (mods & KMOD_PRIMARY) {
191 d->cursor = curMax;
192 }
193 else if (d->cursor < curMax) {
194 d->cursor++;
195 }
196 return iTrue;
197 case SDLK_TAB:
198 /* Allow focus switching. */
199 return processEvent_Widget(as_Widget(d), ev);
200 }
201 if (mods & (KMOD_PRIMARY | KMOD_SECONDARY)) {
202 return iFalse;
203 }
204 return iTrue;
205 }
206 else if (ev->type == SDL_TEXTINPUT && isFocused_Widget(as_Widget(d))) {
207 const iString *uni = collectNewCStr_String(ev->text.text);
208 const iChar chr = first_String(uni);
209 if (d->mode == insert_InputMode) {
210 insert_Array(&d->text, d->cursor, &chr);
211 d->cursor++;
212 }
213 else {
214 if (d->cursor >= size_Array(&d->text)) {
215 resize_Array(&d->text, d->cursor + 1);
216 }
217 set_Array(&d->text, d->cursor++, &chr);
218 if (d->maxLen && d->cursor == d->maxLen) {
219 setFocus_Widget(NULL);
220 }
221 }
222 return iTrue;
223 }
224 return processEvent_Widget(as_Widget(d), ev);
225}
226
227static void draw_InputWidget_(const iInputWidget *d) {
228 const uint32_t time = frameTime_Window(get_Window());
229 const iInt2 padding = init_I2(3 * gap_UI, gap_UI);
230 iRect bounds = adjusted_Rect(bounds_Widget(constAs_Widget(d)), padding, neg_I2(padding));
231 const iBool isFocused = isFocused_Widget(constAs_Widget(d));
232 const iBool isHover = isHover_Widget(constAs_Widget(d)) &&
233 contains_Widget(constAs_Widget(d), mouseCoord_Window(get_Window()));
234 iPaint p;
235 init_Paint(&p);
236 iString text;
237 initUnicodeN_String(&text, constData_Array(&d->text), size_Array(&d->text));
238 fillRect_Paint(&p, bounds, black_ColorId);
239 drawRect_Paint(&p,
240 adjusted_Rect(bounds, neg_I2(one_I2()), zero_I2()),
241 isFocused ? orange_ColorId : isHover ? cyan_ColorId : gray50_ColorId);
242 setClip_Paint(&p, bounds);
243 const iInt2 emSize = advance_Text(d->font, "M");
244 const int textWidth = advance_Text(d->font, cstr_String(&text)).x;
245 const int cursorX = advanceN_Text(d->font, cstr_String(&text), d->cursor).x;
246 int xOff = 0;
247 if (d->maxLen == 0) {
248 if (textWidth > width_Rect(bounds) - emSize.x) {
249 xOff = width_Rect(bounds) - emSize.x - textWidth;
250 }
251 if (cursorX + xOff < width_Rect(bounds) / 2) {
252 xOff = width_Rect(bounds) / 2 - cursorX;
253 }
254 xOff = iMin(xOff, 0);
255 }
256 draw_Text(d->font, addX_I2(topLeft_Rect(bounds), xOff), white_ColorId, cstr_String(&text));
257 clearClip_Paint(&p);
258 /* Cursor blinking. */
259 if (isFocused && (time & 256)) {
260 const iInt2 prefixSize = advanceN_Text(d->font, cstr_String(&text), d->cursor);
261 const iInt2 curPos = init_I2(xOff + left_Rect(bounds) + prefixSize.x, top_Rect(bounds));
262 const iRect curRect = { curPos, addX_I2(emSize, 1) };
263 iString cur;
264 if (d->cursor < size_Array(&d->text)) {
265 initUnicodeN_String(&cur, constAt_Array(&d->text, d->cursor), 1);
266 }
267 else {
268 initCStr_String(&cur, " ");
269 }
270 fillRect_Paint(&p, curRect, orange_ColorId);
271 draw_Text(d->font, curPos, black_ColorId, cstr_String(&cur));
272 deinit_String(&cur);
273 }
274 deinit_String(&text);
275}
276
277iBeginDefineSubclass(InputWidget, Widget)
278 .processEvent = (iAny *) processEvent_InputWidget_,
279 .draw = (iAny *) draw_InputWidget_,
280iEndDefineSubclass(InputWidget)
diff --git a/src/ui/inputwidget.h b/src/ui/inputwidget.h
new file mode 100644
index 00000000..b606f974
--- /dev/null
+++ b/src/ui/inputwidget.h
@@ -0,0 +1,20 @@
1#pragma once
2
3#include "widget.h"
4
5iDeclareWidgetClass(InputWidget)
6iDeclareObjectConstructionArgs(InputWidget, size_t maxLen)
7
8enum iInputMode {
9 insert_InputMode,
10 overwrite_InputMode,
11};
12
13void setMode_InputWidget (iInputWidget *, enum iInputMode mode);
14void setMaxLen_InputWidget (iInputWidget *, size_t maxLen);
15void setText_InputWidget (iInputWidget *, const iString *text);
16void setCursor_InputWidget (iInputWidget *, size_t pos);
17void begin_InputWidget (iInputWidget *);
18void end_InputWidget (iInputWidget *, iBool accept);
19
20const iString * text_InputWidget (const iInputWidget *);
diff --git a/src/ui/labelwidget.c b/src/ui/labelwidget.c
new file mode 100644
index 00000000..7b64857b
--- /dev/null
+++ b/src/ui/labelwidget.c
@@ -0,0 +1,281 @@
1#include "labelwidget.h"
2#include "text.h"
3#include "color.h"
4#include "paint.h"
5#include "app.h"
6#include "util.h"
7
8iLocalDef iInt2 padding_(void) { return init_I2(3 * gap_UI, gap_UI); }
9
10struct Impl_LabelWidget {
11 iWidget widget;
12 iString label;
13 int font;
14 int key;
15 int kmods;
16 iString command;
17 iClick click;
18};
19
20iDefineObjectConstructionArgs(LabelWidget,
21 (const char *label, int key, int kmods, const char *cmd),
22 label, key, kmods, cmd)
23
24static iBool checkModifiers_(int have, int req) {
25 return keyMods_Sym(req) == keyMods_Sym(have);
26}
27
28static void trigger_LabelWidget_(const iLabelWidget *d) {
29 postCommand_Widget(&d->widget, "%s", cstr_String(&d->command));
30}
31
32static iBool processEvent_LabelWidget_(iLabelWidget *d, const SDL_Event *ev) {
33 if (isCommand_UserEvent(ev, "metrics.changed")) {
34 updateSize_LabelWidget(d);
35 }
36 if (!isEmpty_String(&d->command)) {
37 switch (processEvent_Click(&d->click, ev)) {
38 case started_ClickResult:
39 setFlags_Widget(&d->widget, pressed_WidgetFlag, iTrue);
40 return iTrue;
41 case aborted_ClickResult:
42 setFlags_Widget(&d->widget, pressed_WidgetFlag, iFalse);
43 return iTrue;
44 case finished_ClickResult:
45 setFlags_Widget(&d->widget, pressed_WidgetFlag, iFalse);
46 trigger_LabelWidget_(d);
47 return iTrue;
48 case double_ClickResult:
49 return iTrue;
50 default:
51 break;
52 }
53 switch (ev->type) {
54 case SDL_KEYDOWN: {
55 const int mods = ev->key.keysym.mod;
56 if (d->key && ev->key.keysym.sym == d->key && checkModifiers_(mods, d->kmods)) {
57 trigger_LabelWidget_(d);
58 return iTrue;
59 }
60 break;
61 }
62 }
63 }
64 return processEvent_Widget(&d->widget, ev);
65}
66
67static void keyStr_LabelWidget_(const iLabelWidget *d, iString *str) {
68#if defined (iPlatformApple)
69 if (d->kmods & KMOD_CTRL) {
70 appendChar_String(str, 0x2303);
71 }
72 if (d->kmods & KMOD_ALT) {
73 appendChar_String(str, 0x2325);
74 }
75 if (d->kmods & KMOD_SHIFT) {
76 appendChar_String(str, 0x21e7);
77 }
78 if (d->kmods & KMOD_GUI) {
79 appendChar_String(str, 0x2318);
80 }
81#else
82 if (d->kmods & KMOD_CTRL) {
83 appendCStr_String(str, "Ctrl+");
84 }
85 if (d->kmods & KMOD_ALT) {
86 appendCStr_String(str, "Alt+");
87 }
88 if (d->kmods & KMOD_SHIFT) {
89 appendCStr_String(str, "Shift+");
90 }
91 if (d->kmods & KMOD_GUI) {
92 appendCStr_String(str, "Meta+");
93 }
94#endif
95 if (d->key == 0x20) {
96 appendCStr_String(str, "Space");
97 }
98 else if (d->key == SDLK_LEFT) {
99 appendChar_String(str, 0x2190);
100 }
101 else if (d->key == SDLK_RIGHT) {
102 appendChar_String(str, 0x2192);
103 }
104 else if (d->key < 128 && (isalnum(d->key) || ispunct(d->key))) {
105 appendChar_String(str, upper_Char(d->key));
106 }
107 else if (d->key == SDLK_BACKSPACE) {
108 appendChar_String(str, 0x232b); /* Erase to the Left */
109 }
110 else if (d->key == SDLK_DELETE) {
111 appendChar_String(str, 0x2326); /* Erase to the Right */
112 }
113 else {
114 appendCStr_String(str, SDL_GetKeyName(d->key));
115 }
116}
117
118static void draw_LabelWidget_(const iLabelWidget *d) {
119 const iWidget *w = constAs_Widget(d);
120 draw_Widget(w);
121 const iBool isButton = d->click.button != 0;
122 const int flags = flags_Widget(w);
123 const iRect bounds = bounds_Widget(w);
124 iRect rect = bounds;
125 if (isButton) {
126 shrink_Rect(&rect, divi_I2(gap2_UI, 4));
127 adjustEdges_Rect(&rect, gap_UI / 8, 0, -gap_UI / 8, 0);
128 }
129 iPaint p;
130 init_Paint(&p);
131 int bg = 0;
132 int fg = gray75_ColorId;
133 int frame = isButton ? gray50_ColorId : gray25_ColorId;
134 int frame2 = isButton ? black_ColorId : frame;
135 if (flags & selected_WidgetFlag) {
136 bg = teal_ColorId;
137 fg = white_ColorId;
138 frame = isButton ? cyan_ColorId : frame;
139 }
140 if (isHover_Widget(w)) {
141 if (flags & frameless_WidgetFlag) {
142 bg = teal_ColorId;
143 fg = white_ColorId;
144 if (isButton && flags & selected_WidgetFlag) frame = white_ColorId;
145 }
146 else {
147 if (frame != cyan_ColorId) {
148 if (startsWith_String(&d->label, orange_ColorEscape)) {
149 frame = orange_ColorId;
150 frame2 = brown_ColorId;
151 }
152 else {
153 frame = cyan_ColorId;
154 frame2 = teal_ColorId;
155 }
156 }
157 else {
158 frame = white_ColorId;
159 frame2 = cyan_ColorId;
160 }
161 }
162 }
163 if (flags & pressed_WidgetFlag) {
164 bg = orange_ColorId | permanent_ColorId;
165 if (isButton) frame = bg;
166 fg = black_ColorId | permanent_ColorId;
167 }
168 if (bg) {
169 fillRect_Paint(&p, rect, bg);
170 }
171 if (~flags & frameless_WidgetFlag) {
172 iRect frameRect = adjusted_Rect(rect, zero_I2(), init1_I2(-1));
173 if (isButton) {
174 iInt2 points[] = {
175 bottomLeft_Rect(frameRect), topLeft_Rect(frameRect), topRight_Rect(frameRect),
176 bottomRight_Rect(frameRect), bottomLeft_Rect(frameRect)
177 };
178 drawLines_Paint(&p, points + 2, 3, frame2);
179 drawLines_Paint(&p, points, 3, frame);
180 }
181 else {
182 drawRect_Paint(&p, frameRect, frame);
183 }
184 }
185 setClip_Paint(&p, rect);
186 if (flags & alignLeft_WidgetFlag) {
187 draw_Text(d->font, add_I2(bounds.pos, padding_()), fg, cstr_String(&d->label));
188 if ((flags & drawKey_WidgetFlag) && d->key) {
189 iString str;
190 init_String(&str);
191 keyStr_LabelWidget_(d, &str);
192 draw_Text(uiShortcuts_FontId, negX_I2(add_I2(topRight_Rect(bounds), negX_I2(padding_()))),
193 flags & pressed_WidgetFlag ? fg : cyan_ColorId, cstr_String(&str));
194 deinit_String(&str);
195 }
196 }
197 else if (flags & alignRight_WidgetFlag) {
198 draw_Text(
199 d->font,
200 mul_I2(init_I2(-1, 1), add_I2(topRight_Rect(bounds), negX_I2(padding_()))),
201 fg,
202 cstr_String(&d->label));
203 }
204 else {
205 drawCentered_Text(d->font, bounds, fg, cstr_String(&d->label));
206 }
207 clearClip_Paint(&p);
208}
209
210void updateSize_LabelWidget(iLabelWidget *d) {
211 iWidget *w = as_Widget(d);
212 const int flags = flags_Widget(w);
213 iInt2 size = add_I2(measure_Text(d->font, cstr_String(&d->label)), muli_I2(padding_(), 2));
214 if ((flags & drawKey_WidgetFlag) && d->key) {
215 iString str;
216 init_String(&str);
217 keyStr_LabelWidget_(d, &str);
218 size.x += 2 * gap_UI + measure_Text(uiShortcuts_FontId, cstr_String(&str)).x;
219 deinit_String(&str);
220 }
221 if (~flags & fixedWidth_WidgetFlag) {
222 w->rect.size.x = size.x;
223 }
224 if (~flags & fixedHeight_WidgetFlag) {
225 w->rect.size.y = size.y;
226 }
227}
228
229void init_LabelWidget(iLabelWidget *d, const char *label, int key, int kmods, const char *cmd) {
230 init_Widget(&d->widget);
231 d->font = default_FontId;
232 initCStr_String(&d->label, label);
233 if (cmd) {
234 initCStr_String(&d->command, cmd);
235 }
236 else {
237 init_String(&d->command);
238 }
239 d->key = key;
240 d->kmods = kmods;
241 init_Click(&d->click, d, !isEmpty_String(&d->command) ? SDL_BUTTON_LEFT : 0);
242 setFlags_Widget(&d->widget, hover_WidgetFlag, d->click.button != 0);
243 updateSize_LabelWidget(d);
244}
245
246void deinit_LabelWidget(iLabelWidget *d) {
247 deinit_String(&d->label);
248 deinit_String(&d->command);
249}
250
251void setFont_LabelWidget(iLabelWidget *d, int fontId) {
252 d->font = fontId;
253 updateSize_LabelWidget(d);
254}
255
256void setText_LabelWidget(iLabelWidget *d, const iString *text) {
257 updateText_LabelWidget(d, text);
258 updateSize_LabelWidget(d);
259}
260
261void updateText_LabelWidget(iLabelWidget *d, const iString *text) {
262 set_String(&d->label, text);
263}
264
265void updateTextCStr_LabelWidget(iLabelWidget *d, const char *text) {
266 setCStr_String(&d->label, text);
267}
268
269void setTextCStr_LabelWidget(iLabelWidget *d, const char *text) {
270 setCStr_String(&d->label, text);
271 updateSize_LabelWidget(d);
272}
273
274const iString *command_LabelWidget(const iLabelWidget *d) {
275 return &d->command;
276}
277
278iBeginDefineSubclass(LabelWidget, Widget)
279 .processEvent = (iAny *) processEvent_LabelWidget_,
280 .draw = (iAny *) draw_LabelWidget_,
281iEndDefineSubclass(LabelWidget)
diff --git a/src/ui/labelwidget.h b/src/ui/labelwidget.h
new file mode 100644
index 00000000..0563728e
--- /dev/null
+++ b/src/ui/labelwidget.h
@@ -0,0 +1,22 @@
1#pragma once
2
3/* Text label/button. */
4
5#include "widget.h"
6
7iDeclareWidgetClass(LabelWidget)
8iDeclareObjectConstructionArgs(LabelWidget, const char *label, int key, int kmods, const char *command)
9
10iLocalDef iLabelWidget *newEmpty_LabelWidget(void) {
11 return new_LabelWidget("", 0, 0, NULL);
12}
13
14void setFont_LabelWidget (iLabelWidget *, int fontId);
15void setText_LabelWidget (iLabelWidget *, const iString *text); /* resizes widget */
16void setTextCStr_LabelWidget (iLabelWidget *, const char *text);
17
18void updateSize_LabelWidget (iLabelWidget *);
19void updateText_LabelWidget (iLabelWidget *, const iString *text); /* not resized */
20void updateTextCStr_LabelWidget (iLabelWidget *, const char *text); /* not resized */
21
22const iString *command_LabelWidget (const iLabelWidget *);
diff --git a/src/ui/macos.h b/src/ui/macos.h
new file mode 100644
index 00000000..bb991a61
--- /dev/null
+++ b/src/ui/macos.h
@@ -0,0 +1,9 @@
1#pragma once
2
3#include "util.h"
4
5/* Platform-specific functionality for macOS */
6
7void setupApplication_MacOS (void);
8void insertMenuItems_MacOS (const char *menuLabel, const iMenuItem *items, size_t count);
9void handleCommand_MacOS (const char *cmd);
diff --git a/src/ui/macos.m b/src/ui/macos.m
new file mode 100644
index 00000000..73e17a4c
--- /dev/null
+++ b/src/ui/macos.m
@@ -0,0 +1,434 @@
1#include "macos.h"
2#include "app.h"
3#include "command.h"
4#include "widget.h"
5#include "color.h"
6
7#import <AppKit/AppKit.h>
8
9#if 0
10static NSTouchBarItemIdentifier play_TouchId_ = @"fi.skyjake.BitwiseHarmony.play";
11static NSTouchBarItemIdentifier restart_TouchId_ = @"fi.skyjake.BitwiseHarmony.restart";
12
13static NSTouchBarItemIdentifier seqMoveUp_TouchId_ = @"fi.skyjake.BitwiseHarmony.sequence.move.up";
14static NSTouchBarItemIdentifier seqMoveDown_TouchId_ = @"fi.skyjake.BitwiseHarmony.sequence.move.down";
15
16static NSTouchBarItemIdentifier goto_TouchId_ = @"fi.skyjake.BitwiseHarmony.goto";
17static NSTouchBarItemIdentifier mute_TouchId_ = @"fi.skyjake.BitwiseHarmony.mute";
18static NSTouchBarItemIdentifier solo_TouchId_ = @"fi.skyjake.BitwiseHarmony.solo";
19static NSTouchBarItemIdentifier color_TouchId_ = @"fi.skyjake.BitwiseHarmony.color";
20static NSTouchBarItemIdentifier event_TouchId_ = @"fi.skyjake.BitwiseHarmony.event";
21
22static NSTouchBarItemIdentifier eventList_TouchId_ = @"fi.skyjake.BitwiseHarmony.eventlist";
23static NSTouchBarItemIdentifier masterGainEvent_TouchId_ = @"fi.skyjake.BitwiseHarmony.event.mastergain";
24static NSTouchBarItemIdentifier resetEvent_TouchId_ = @"fi.skyjake.BitwiseHarmony.event.reset";
25static NSTouchBarItemIdentifier voiceEvent_TouchId_ = @"fi.skyjake.BitwiseHarmony.event.voice";
26static NSTouchBarItemIdentifier panEvent_TouchId_ = @"fi.skyjake.BitwiseHarmony.event.pan";
27static NSTouchBarItemIdentifier gainEvent_TouchId_ = @"fi.skyjake.BitwiseHarmony.event.gain";
28static NSTouchBarItemIdentifier fadeEvent_TouchId_ = @"fi.skyjake.BitwiseHarmony.event.fade";
29static NSTouchBarItemIdentifier pitchSpeedEvent_TouchId_ = @"fi.skyjake.BitwiseHarmony.event.pitchspeed";
30static NSTouchBarItemIdentifier pitchBendUpEvent_TouchId_ = @"fi.skyjake.BitwiseHarmony.event.pitchbendup";
31static NSTouchBarItemIdentifier pitchBendDownEvent_TouchId_ = @"fi.skyjake.BitwiseHarmony.event.pitchbenddown";
32static NSTouchBarItemIdentifier tremoloEvent_TouchId_ = @"fi.skyjake.BitwiseHarmony.event.tremolo";
33#endif
34
35enum iTouchBarVariant {
36 default_TouchBarVariant,
37 sequence_TouchBarVariant,
38 tracker_TouchBarVariant,
39 wide_TouchBarVariant,
40};
41
42@interface CommandButton : NSButtonTouchBarItem {
43 NSString *command;
44 iWidget *widget;
45}
46- (id)initWithIdentifier:(NSTouchBarItemIdentifier)identifier
47 title:(NSString *)title
48 command:(NSString *)cmd;
49- (id)initWithIdentifier:(NSTouchBarItemIdentifier)identifier
50 title:(NSString *)title
51 widget:(iWidget *)widget
52 command:(NSString *)cmd;
53- (void)dealloc;
54@end
55
56@implementation CommandButton
57
58- (id)initWithIdentifier:(NSTouchBarItemIdentifier)identifier
59 title:(NSString *)title
60 command:(NSString *)cmd {
61 [super initWithIdentifier:identifier];
62 self.title = title;
63 self.target = self;
64 self.action = @selector(buttonPressed);
65 command = cmd;
66 return self;
67}
68
69- (id)initWithIdentifier:(NSTouchBarItemIdentifier)identifier
70 title:(NSString *)title
71 widget:(iWidget *)aWidget
72 command:(NSString *)cmd {
73 [self initWithIdentifier:identifier title:title command:[cmd retain]];
74 widget = aWidget;
75 return self;
76}
77
78- (void)dealloc {
79 [command release];
80 [super dealloc];
81}
82
83- (void)buttonPressed {
84 const char *cmd = [command cStringUsingEncoding:NSUTF8StringEncoding];
85 if (widget) {
86 postCommand_Widget(widget, "%s", cmd);
87 }
88 else {
89 postCommand_App(cmd);
90 }
91}
92
93@end
94
95@interface MyDelegate : NSResponder<NSApplicationDelegate, NSTouchBarDelegate> {
96 enum iTouchBarVariant touchBarVariant;
97 NSObject<NSApplicationDelegate> *sdlDelegate;
98 NSMutableDictionary<NSString *, NSString*> *menuCommands;
99}
100- (id)initWithSDLDelegate:(NSObject<NSApplicationDelegate> *)sdl;
101- (NSTouchBar *)makeTouchBar;
102/* SDL needs to do its own thing. */
103- (BOOL)application:(NSApplication *)theApplication openFile:(NSString *)filename;
104- (void)applicationDidFinishLaunching:(NSNotification *)notificatiosn;
105@end
106
107@implementation MyDelegate
108
109- (id)initWithSDLDelegate:(NSObject<NSApplicationDelegate> *)sdl {
110 [super init];
111 menuCommands = [[NSMutableDictionary<NSString *, NSString *> alloc] init];
112 touchBarVariant = default_TouchBarVariant;
113 sdlDelegate = sdl;
114 return self;
115}
116
117- (void)dealloc {
118 [menuCommands release];
119 [super dealloc];
120}
121
122- (void)setTouchBarVariant:(enum iTouchBarVariant)variant {
123 touchBarVariant = variant;
124 self.touchBar = nil;
125}
126
127- (void)setCommand:(NSString *)command forMenuItem:(NSMenuItem *)menuItem {
128 [menuCommands setObject:command forKey:[menuItem title]];
129}
130
131- (BOOL)application:(NSApplication *)theApplication openFile:(NSString *)filename {
132 return [sdlDelegate application:theApplication openFile:filename];
133}
134
135- (void)applicationDidFinishLaunching:(NSNotification *)notification {
136 [sdlDelegate applicationDidFinishLaunching:notification];
137}
138
139- (NSTouchBar *)makeTouchBar {
140 NSTouchBar *bar = [[NSTouchBar alloc] init];
141 bar.delegate = self;
142#if 0
143 switch (touchBarVariant) {
144 case default_TouchBarVariant:
145 bar.defaultItemIdentifiers = @[ play_TouchId_, restart_TouchId_,
146 NSTouchBarItemIdentifierFixedSpaceSmall,
147 NSTouchBarItemIdentifierOtherItemsProxy ];
148 break;
149 case sequence_TouchBarVariant:
150 bar.defaultItemIdentifiers = @[ play_TouchId_, restart_TouchId_,
151 NSTouchBarItemIdentifierFlexibleSpace,
152 seqMoveUp_TouchId_, seqMoveDown_TouchId_,
153 NSTouchBarItemIdentifierFlexibleSpace,
154 NSTouchBarItemIdentifierOtherItemsProxy];
155 break;
156 case tracker_TouchBarVariant:
157 bar.defaultItemIdentifiers = @[ play_TouchId_, restart_TouchId_,
158 NSTouchBarItemIdentifierFlexibleSpace,
159 goto_TouchId_,
160 event_TouchId_,
161 NSTouchBarItemIdentifierFlexibleSpace,
162 solo_TouchId_, mute_TouchId_, color_TouchId_,
163 NSTouchBarItemIdentifierFlexibleSpace,
164 NSTouchBarItemIdentifierOtherItemsProxy ];
165 break;
166 case wide_TouchBarVariant:
167 bar.defaultItemIdentifiers = @[ play_TouchId_, restart_TouchId_,
168 NSTouchBarItemIdentifierFlexibleSpace,
169 event_TouchId_,
170 NSTouchBarItemIdentifierFlexibleSpace,
171 solo_TouchId_, mute_TouchId_, color_TouchId_,
172 NSTouchBarItemIdentifierFlexibleSpace,
173 seqMoveUp_TouchId_, seqMoveDown_TouchId_,
174 NSTouchBarItemIdentifierFlexibleSpace,
175 NSTouchBarItemIdentifierOtherItemsProxy ];
176 break;
177 }
178#endif
179 return bar;
180}
181
182- (void)playPressed {
183 postCommand_App("playback.toggle");
184}
185
186- (void)restartPressed {
187 postCommand_App("playback.restart");
188}
189
190- (void)showPreferences {
191 postCommand_App("preferences");
192}
193
194- (void)postMenuItemCommand:(id)sender {
195 NSString *command = [menuCommands objectForKey:[(NSMenuItem *)sender title]];
196 if (command) {
197 postCommand_App([command cStringUsingEncoding:NSUTF8StringEncoding]);
198 }
199}
200
201- (nullable NSTouchBarItem *)touchBar:(NSTouchBar *)touchBar
202 makeItemForIdentifier:(NSTouchBarItemIdentifier)identifier {
203 iUnused(touchBar);
204#if 0
205 if ([identifier isEqualToString:play_TouchId_]) {
206 return [NSButtonTouchBarItem
207 buttonTouchBarItemWithIdentifier:identifier
208 image:[NSImage imageNamed:NSImageNameTouchBarPlayPauseTemplate]
209 target:self
210 action:@selector(playPressed)];
211 }
212 else if ([identifier isEqualToString:restart_TouchId_]) {
213 return [NSButtonTouchBarItem
214 buttonTouchBarItemWithIdentifier:identifier
215 image:[NSImage imageNamed:NSImageNameTouchBarSkipToStartTemplate]
216 target:self
217 action:@selector(restartPressed)];
218 }
219 else if ([identifier isEqualToString:seqMoveUp_TouchId_]) {
220 return [[CommandButton alloc] initWithIdentifier:identifier
221 title:@"Seq\u2b06"
222 widget:findWidget_App("sequence")
223 command:@"sequence.swap arg:-1"];
224 }
225 else if ([identifier isEqualToString:seqMoveDown_TouchId_]) {
226 return [[CommandButton alloc] initWithIdentifier:identifier
227 title:@"Seq\u2b07"
228 widget:findWidget_App("sequence")
229 command:@"sequence.swap arg:1"];
230 }
231 else if ([identifier isEqualToString:goto_TouchId_]) {
232 return [[CommandButton alloc] initWithIdentifier:identifier
233 title:@"Go to…"
234 command:@"pattern.goto arg:-1"];
235 }
236 else if ([identifier isEqualToString:event_TouchId_]) {
237 NSTouchBar *events = [[NSTouchBar alloc] init];
238 events.delegate = self;
239 events.defaultItemIdentifiers = @[ eventList_TouchId_ ];
240 NSPopoverTouchBarItem *pop = [[NSPopoverTouchBarItem alloc] initWithIdentifier:identifier];
241 pop.collapsedRepresentationLabel = @"Event";
242 pop.popoverTouchBar = events;
243 [events release];
244 return pop;
245 }
246 else if ([identifier isEqualToString:eventList_TouchId_]) {
247 const struct {
248 NSTouchBarItemIdentifier id;
249 const char *title;
250 const char *command;
251 } buttonDefs_[] = {
252 { voiceEvent_TouchId_, "Voice", "tracker.setevent type:2" },
253 { panEvent_TouchId_, "Pan", "tracker.setevent type:3 arg:128" },
254 { gainEvent_TouchId_, "Gain", "tracker.setevent type:4 arg:128" },
255 { fadeEvent_TouchId_, "Fade", "tracker.setevent type:5" },
256 { tremoloEvent_TouchId_, "Trem", "tracker.setevent type:9" },
257 { pitchSpeedEvent_TouchId_, "P.Spd", "tracker.setevent type:6" },
258 { pitchBendUpEvent_TouchId_, "BnUp", "tracker.setevent type:7" },
259 { pitchBendDownEvent_TouchId_, "BnDn", "tracker.setevent type:8" },
260 { masterGainEvent_TouchId_, "M.Gain", "tracker.setevent type:10 arg:64" },
261 { resetEvent_TouchId_, "Reset", "tracker.setevent type:1" },
262 };
263 NSMutableArray *items = [[NSMutableArray alloc] init];
264 iForIndices(i, buttonDefs_) {
265 CommandButton *button = [[CommandButton alloc]
266 initWithIdentifier:buttonDefs_[i].id
267 title:[NSString stringWithUTF8String:buttonDefs_[i].title]
268 widget:findWidget_App("tracker")
269 command:[NSString stringWithUTF8String:buttonDefs_[i].command]
270 ];
271 [items addObject:button];
272 }
273 NSGroupTouchBarItem *group = [NSGroupTouchBarItem groupItemWithIdentifier:identifier
274 items:items];
275 [items release];
276 return group;
277 }
278 else if ([identifier isEqualToString:mute_TouchId_]) {
279 return [[CommandButton alloc] initWithIdentifier:identifier
280 title:@"Mute"
281 widget:findWidget_App("tracker")
282 command:@"tracker.mute"];
283 }
284 else if ([identifier isEqualToString:solo_TouchId_]) {
285 return [[CommandButton alloc] initWithIdentifier:identifier
286 title:@"Solo"
287 widget:findWidget_App("tracker")
288 command:@"tracker.solo"];
289 }
290 else if ([identifier isEqualToString:color_TouchId_]) {
291 NSTouchBar *colors = [[NSTouchBar alloc] init];
292 colors.delegate = self;
293 colors.defaultItemIdentifiers = @[ NSTouchBarItemIdentifierFlexibleSpace,
294 whiteColor_TouchId_,
295 yellowColor_TouchId_,
296 orangeColor_TouchId_,
297 redColor_TouchId_,
298 magentaColor_TouchId_,
299 blueColor_TouchId_,
300 cyanColor_TouchId_,
301 greenColor_TouchId_,
302 NSTouchBarItemIdentifierFlexibleSpace ];
303 NSPopoverTouchBarItem *pop = [[NSPopoverTouchBarItem alloc] initWithIdentifier:identifier];
304 pop.collapsedRepresentationImage = [NSImage imageNamed:NSImageNameTouchBarColorPickerFill];
305 pop.popoverTouchBar = colors;
306 [colors release];
307 return pop;
308 }
309 else if ([identifier isEqualToString:whiteColor_TouchId_]) {
310 return [[ColorButton alloc] initWithIdentifier:identifier
311 trackColor:white_TrackColor];
312 }
313 else if ([identifier isEqualToString:yellowColor_TouchId_]) {
314 return [[ColorButton alloc] initWithIdentifier:identifier
315 trackColor:yellow_TrackColor];
316 }
317 else if ([identifier isEqualToString:orangeColor_TouchId_]) {
318 return [[ColorButton alloc] initWithIdentifier:identifier
319 trackColor:orange_TrackColor];
320 }
321 else if ([identifier isEqualToString:redColor_TouchId_]) {
322 return [[ColorButton alloc] initWithIdentifier:identifier
323 trackColor:red_TrackColor];
324 }
325 else if ([identifier isEqualToString:magentaColor_TouchId_]) {
326 return [[ColorButton alloc] initWithIdentifier:identifier
327 trackColor:magenta_TrackColor];
328 }
329 else if ([identifier isEqualToString:blueColor_TouchId_]) {
330 return [[ColorButton alloc] initWithIdentifier:identifier
331 trackColor:blue_TrackColor];
332 }
333 else if ([identifier isEqualToString:cyanColor_TouchId_]) {
334 return [[ColorButton alloc] initWithIdentifier:identifier
335 trackColor:cyan_TrackColor];
336 }
337 else if ([identifier isEqualToString:greenColor_TouchId_]) {
338 return [[ColorButton alloc] initWithIdentifier:identifier
339 trackColor:green_TrackColor];
340 }
341#endif
342 return nil;
343}
344
345@end
346
347void setupApplication_MacOS(void) {
348 NSApplication *app = [NSApplication sharedApplication];
349 /* Our delegate will override SDL's delegate. */
350 MyDelegate *myDel = [[MyDelegate alloc] initWithSDLDelegate:app.delegate];
351 app.delegate = myDel;
352 NSMenu *appMenu = [[[NSApp mainMenu] itemAtIndex:0] submenu];
353 NSMenuItem *prefsItem = [appMenu itemWithTitle:@"Preferences…"];
354 prefsItem.target = myDel;
355 prefsItem.action = @selector(showPreferences);
356}
357
358void insertMenuItems_MacOS(const char *menuLabel, const iMenuItem *items, size_t count) {
359 NSApplication *app = [NSApplication sharedApplication];
360 MyDelegate *myDel = (MyDelegate *) app.delegate;
361 NSMenu *appMenu = [app mainMenu];
362 NSMenuItem *mainItem = [appMenu insertItemWithTitle:[NSString stringWithUTF8String:menuLabel]
363 action:nil
364 keyEquivalent:@""
365 atIndex:(iCmpStr(menuLabel, "File") == 0 ? 1 :
366 iCmpStr(menuLabel, "Edit") == 0 ? 2 : 3)];
367 NSMenu *menu = [[NSMenu alloc] initWithTitle:[NSString stringWithUTF8String:menuLabel]];
368 for (size_t i = 0; i < count; ++i) {
369 const char *label = items[i].label;
370 if (label[0] == '\r') {
371 /* Skip the formatting escape. */
372 label += 2;
373 }
374 if (equal_CStr(label, "---")) {
375 [menu addItem:[NSMenuItem separatorItem]];
376 }
377 else {
378 const iBool hasCommand = (items[i].command && items[i].command[0]);
379 iString key;
380 init_String(&key);
381 if (items[i].key == SDLK_LEFT) {
382 appendChar_String(&key, 0x2190);
383 }
384 else if (items[i].key == SDLK_RIGHT) {
385 appendChar_String(&key, 0x2192);
386 }
387 else if (items[i].key) {
388 appendChar_String(&key, items[i].key);
389 }
390 NSMenuItem *item = [menu addItemWithTitle:[NSString stringWithUTF8String:label]
391 action:(hasCommand ? @selector(postMenuItemCommand:) : nil)
392 keyEquivalent:[NSString stringWithUTF8String:cstr_String(&key)]];
393 NSEventModifierFlags modMask = 0;
394 if (items[i].kmods & KMOD_GUI) {
395 modMask |= NSEventModifierFlagCommand;
396 }
397 if (items[i].kmods & KMOD_ALT) {
398 modMask |= NSEventModifierFlagOption;
399 }
400 if (items[i].kmods & KMOD_CTRL) {
401 modMask |= NSEventModifierFlagControl;
402 }
403 if (items[i].kmods & KMOD_SHIFT) {
404 modMask |= NSEventModifierFlagShift;
405 }
406 [item setKeyEquivalentModifierMask:modMask];
407 if (hasCommand) {
408 [myDel setCommand:[NSString stringWithUTF8String:items[i].command] forMenuItem:item];
409 }
410 deinit_String(&key);
411 }
412 }
413 [mainItem setSubmenu:menu];
414 [menu release];
415}
416
417void handleCommand_MacOS(const char *cmd) {
418 if (equal_Command(cmd, "tabs.changed")) {
419 MyDelegate *myDel = (MyDelegate *) [[NSApplication sharedApplication] delegate];
420 const char *tabId = valuePtr_Command(cmd, "id");
421 if (equal_CStr(tabId, "tracker")) {
422 [myDel setTouchBarVariant:tracker_TouchBarVariant];
423 }
424 else if (equal_CStr(tabId, "sequence")) {
425 [myDel setTouchBarVariant:sequence_TouchBarVariant];
426 }
427 else if (equal_CStr(tabId, "trackertab")) {
428 [myDel setTouchBarVariant:wide_TouchBarVariant];
429 }
430 else {
431 [myDel setTouchBarVariant:default_TouchBarVariant];
432 }
433 }
434}
diff --git a/src/ui/metrics.c b/src/ui/metrics.c
new file mode 100644
index 00000000..38ae5955
--- /dev/null
+++ b/src/ui/metrics.c
@@ -0,0 +1,16 @@
1#include "metrics.h"
2
3#include <the_Foundation/math.h>
4
5#define defaultFontSize_Metrics 16
6#define defaultGap_Metrics 4
7
8int gap_UI = defaultGap_Metrics;
9iInt2 gap2_UI = { defaultGap_Metrics, defaultGap_Metrics };
10int fontSize_UI = defaultFontSize_Metrics;
11
12void setPixelRatio_Metrics(float pixelRatio) {
13 gap_UI = iRound(defaultGap_Metrics * pixelRatio);
14 gap2_UI = init1_I2(gap_UI);
15 fontSize_UI = iRound(defaultFontSize_Metrics * pixelRatio);
16}
diff --git a/src/ui/metrics.h b/src/ui/metrics.h
new file mode 100644
index 00000000..d59f726a
--- /dev/null
+++ b/src/ui/metrics.h
@@ -0,0 +1,9 @@
1#pragma once
2
3#include <the_Foundation/vec2.h>
4
5extern int gap_UI;
6extern int fontSize_UI;
7extern iInt2 gap2_UI;
8
9void setPixelRatio_Metrics (float pixelRatio);
diff --git a/src/ui/paint.c b/src/ui/paint.c
new file mode 100644
index 00000000..0a2e6cd3
--- /dev/null
+++ b/src/ui/paint.c
@@ -0,0 +1,58 @@
1#include "paint.h"
2
3iLocalDef SDL_Renderer *renderer_Paint_(const iPaint *d) {
4 iAssert(d->dst);
5 return d->dst->render;
6}
7
8static void setColor_Paint_(const iPaint *d, int color) {
9 const iColor clr = get_Color(color & mask_ColorId);
10 SDL_SetRenderDrawColor(renderer_Paint_(d), clr.r, clr.g, clr.b, clr.a);
11}
12
13void init_Paint(iPaint *d) {
14 d->dst = get_Window();
15}
16
17void setClip_Paint(iPaint *d, iRect rect) {
18 SDL_RenderSetClipRect(renderer_Paint_(d), (const SDL_Rect *) &rect);
19}
20
21void clearClip_Paint(iPaint *d) {
22 const SDL_Rect winRect = { 0, 0, d->dst->root->rect.size.x, d->dst->root->rect.size.y };
23 SDL_RenderSetClipRect(renderer_Paint_(d), &winRect);
24}
25
26void drawRect_Paint(const iPaint *d, iRect rect, int color) {
27 iInt2 br = bottomRight_Rect(rect);
28 /* Keep the right/bottom edge visible in the window. */
29 if (br.x == d->dst->root->rect.size.x) br.x--;
30 if (br.y == d->dst->root->rect.size.y) br.y--;
31 const SDL_Point edges[] = {
32 { left_Rect(rect), top_Rect(rect) },
33 { br.x, top_Rect(rect) },
34 { br.x, br.y },
35 { left_Rect(rect), br.y },
36 { left_Rect(rect), top_Rect(rect) }
37 };
38 setColor_Paint_(d, color);
39 SDL_RenderDrawLines(renderer_Paint_(d), edges, iElemCount(edges));
40}
41
42void drawRectThickness_Paint(const iPaint *d, iRect rect, int thickness, int color) {
43 thickness = iClamp(thickness, 1, 4);
44 while (thickness--) {
45 drawRect_Paint(d, rect, color);
46 shrink_Rect(&rect, one_I2());
47 }
48}
49
50void fillRect_Paint(const iPaint *d, iRect rect, int color) {
51 setColor_Paint_(d, color);
52 SDL_RenderFillRect(renderer_Paint_(d), (SDL_Rect *) &rect);
53}
54
55void drawLines_Paint(const iPaint *d, const iInt2 *points, size_t count, int color) {
56 setColor_Paint_(d, color);
57 SDL_RenderDrawLines(renderer_Paint_(d), (const SDL_Point *) points, count);
58}
diff --git a/src/ui/paint.h b/src/ui/paint.h
new file mode 100644
index 00000000..9535b142
--- /dev/null
+++ b/src/ui/paint.h
@@ -0,0 +1,33 @@
1#pragma once
2
3#include <the_Foundation/rect.h>
4#include "color.h"
5#include "text.h"
6#include "window.h"
7
8iDeclareType(Paint)
9
10struct Impl_Paint {
11 iWindow *dst;
12};
13
14void init_Paint (iPaint *);
15
16void setClip_Paint (iPaint *, iRect rect);
17void clearClip_Paint (iPaint *);
18
19void drawRect_Paint (const iPaint *, iRect rect, int color);
20void drawRectThickness_Paint (const iPaint *, iRect rect, int thickness, int color);
21void fillRect_Paint (const iPaint *, iRect rect, int color);
22
23void drawLines_Paint (const iPaint *, const iInt2 *points, size_t count, int color);
24
25iLocalDef void drawLine_Paint(const iPaint *d, iInt2 a, iInt2 b, int color) {
26 drawLines_Paint(d, (iInt2[]){ a, b }, 2, color);
27}
28iLocalDef void drawHLine_Paint(const iPaint *d, iInt2 pos, int len, int color) {
29 drawLine_Paint(d, pos, addX_I2(pos, len), color);
30}
31iLocalDef void drawVLine_Paint(const iPaint *d, iInt2 pos, int len, int color) {
32 drawLine_Paint(d, pos, addY_I2(pos, len), color);
33}
diff --git a/src/ui/text.c b/src/ui/text.c
new file mode 100644
index 00000000..ac211481
--- /dev/null
+++ b/src/ui/text.c
@@ -0,0 +1,533 @@
1#include "text.h"
2#include "color.h"
3#include "metrics.h"
4#include "embedded.h"
5#include "app.h"
6
7#define STB_TRUETYPE_IMPLEMENTATION
8#include "../stb_truetype.h"
9
10#include <the_Foundation/array.h>
11#include <the_Foundation/file.h>
12#include <the_Foundation/hash.h>
13#include <the_Foundation/math.h>
14#include <the_Foundation/path.h>
15#include <the_Foundation/vec2.h>
16
17#include <SDL_surface.h>
18#include <stdarg.h>
19
20iDeclareType(Glyph)
21iDeclareTypeConstructionArgs(Glyph, iChar ch)
22
23struct Impl_Glyph {
24 iHashNode node;
25 iRect rect;
26 int advance;
27 int dx, dy;
28};
29
30void init_Glyph(iGlyph *d, iChar ch) {
31 d->node.key = ch;
32 d->rect = zero_Rect();
33 d->advance = 0;
34}
35
36void deinit_Glyph(iGlyph *d) {
37 iUnused(d);
38}
39
40iChar char_Glyph(const iGlyph *d) {
41 return d->node.key;
42}
43
44iDefineTypeConstructionArgs(Glyph, (iChar ch), ch)
45
46/*-----------------------------------------------------------------------------------------------*/
47
48iDeclareType(Font)
49
50struct Impl_Font {
51 iBlock * data;
52 stbtt_fontinfo font;
53 float scale;
54 int height;
55 int baseline;
56 iHash glyphs;
57};
58
59static void init_Font(iFont *d, const iBlock *data, int height) {
60 init_Hash(&d->glyphs);
61 d->data = NULL;
62 d->height = height;
63 iZap(d->font);
64 stbtt_InitFont(&d->font, constData_Block(data), 0);
65 d->scale = stbtt_ScaleForPixelHeight(&d->font, height);
66 int ascent;
67 stbtt_GetFontVMetrics(&d->font, &ascent, 0, 0);
68 d->baseline = (int) ascent * d->scale;
69}
70
71static void deinit_Font(iFont *d) {
72 iForEach(Hash, i, &d->glyphs) {
73 delete_Glyph((iGlyph *) i.value);
74 }
75 deinit_Hash(&d->glyphs);
76 delete_Block(d->data);
77}
78
79iDeclareType(Text)
80
81struct Impl_Text {
82 iFont fonts[max_FontId];
83 SDL_Renderer *render;
84 SDL_Texture * cache;
85 iInt2 cacheSize;
86 iInt2 cachePos;
87 int cacheRowHeight;
88 SDL_Palette * grayscale;
89};
90
91static iText text_;
92
93void init_Text(SDL_Renderer *render) {
94 iText *d = &text_;
95 d->render = render;
96 /* A grayscale palette for rasterized glyphs. */ {
97 SDL_Color colors[256];
98 for (int i = 0; i < 256; ++i) {
99 colors[i] = (SDL_Color){ 255, 255, 255, i };
100 }
101 d->grayscale = SDL_AllocPalette(256);
102 SDL_SetPaletteColors(d->grayscale, colors, 0, 256);
103 }
104 /* Initialize the glyph cache. */ {
105 d->cacheSize = init1_I2(fontSize_UI * 16);
106 d->cachePos = zero_I2();
107 d->cache = SDL_CreateTexture(render,
108 SDL_PIXELFORMAT_RGBA8888,
109 SDL_TEXTUREACCESS_STATIC | SDL_TEXTUREACCESS_TARGET,
110 d->cacheSize.x,
111 d->cacheSize.y);
112 SDL_SetTextureBlendMode(d->cache, SDL_BLENDMODE_BLEND);
113 d->cacheRowHeight = 0;
114 }
115 /* Load the fonts. */ {
116 const struct { const iBlock *ttf; int size; } fontData[max_FontId] = {
117 { &fontFiraSansRegular_Embedded, fontSize_UI },
118 { &fontFiraSansRegular_Embedded, fontSize_UI },
119 { &fontFiraMonoRegular_Embedded, fontSize_UI },
120 { &fontFiraSansRegular_Embedded, fontSize_UI },
121 { &fontFiraSansRegular_Embedded, fontSize_UI * 1.5f },
122 { &fontFiraMonoRegular_Embedded, fontSize_UI },
123 { &fontFiraSansLightItalic_Embedded, fontSize_UI },
124 { &fontFiraSansRegular_Embedded, fontSize_UI * 2.5f },
125 { &fontFiraSansRegular_Embedded, fontSize_UI * 2.0f },
126 { &fontFiraSansRegular_Embedded, fontSize_UI * 1.5f },
127 };
128 iForIndices(i, fontData) {
129 init_Font(&d->fonts[i], fontData[i].ttf, fontData[i].size);
130 }
131 }
132}
133
134void deinit_Text(void) {
135 iText *d = &text_;
136 SDL_FreePalette(d->grayscale);
137 iForIndices(i, d->fonts) {
138 deinit_Font(&d->fonts[i]);
139 }
140 SDL_DestroyTexture(d->cache);
141 d->render = NULL;
142}
143
144static SDL_Surface *rasterizeGlyph_Font_(const iFont *d, iChar ch) {
145 int w, h;
146 uint8_t *bmp = stbtt_GetCodepointBitmap(&d->font, d->scale, d->scale, ch, &w, &h, 0, 0);
147 /* Note: `bmp` must be freed afterwards. */
148 SDL_Surface *surface =
149 SDL_CreateRGBSurfaceWithFormatFrom(bmp, w, h, 8, w, SDL_PIXELFORMAT_INDEX8);
150 SDL_SetSurfacePalette(surface, text_.grayscale);
151 return surface;
152}
153
154static iBool isSpecialChar_(iChar ch) {
155 return ch >= specialSymbol_Text && ch < 0x20;
156}
157
158static float symbolEmWidth_(int symbol) {
159 return 1.5f;
160}
161
162static float symbolAdvance_(int symbol) {
163 return 1.5f;
164}
165
166static int specialChar_(iChar ch) {
167 return ch - specialSymbol_Text;
168}
169
170iLocalDef SDL_Rect sdlRect_(const iRect rect) {
171 return (SDL_Rect){ rect.pos.x, rect.pos.y, rect.size.x, rect.size.y };
172}
173
174static void fillTriangle_(SDL_Surface *surface, const SDL_Rect *rect, int dir) {
175 const uint32_t color = 0xffffffff;
176 SDL_LockSurface(surface);
177 uint32_t *row = surface->pixels;
178 row += rect->x + rect->y * surface->pitch / 4;
179 for (int y = 0; y < rect->h; y++, row += surface->pitch / 4) {
180 float norm = (float) y / (float) (rect->h - 1) * 2.0f;
181 if (norm > 1.0f) norm = 2.0f - norm;
182 const int len = norm * rect->w;
183 const float fract = norm * rect->w - len;
184 const uint32_t fractColor = 0xffffff00 | (int) (fract * 0xff);
185 if (dir > 0) {
186 for (int x = 0; x < len; x++) {
187 row[x] = color;
188 }
189 if (len < rect->w) {
190 row[len] = fractColor;
191 }
192 }
193 else {
194 for (int x = 0; x < len; x++) {
195 row[rect->w - len + x] = color;
196 }
197 if (len < rect->w) {
198 row[rect->w - len - 1] = fractColor;
199 }
200 }
201 }
202 SDL_UnlockSurface(surface);
203}
204
205static void cache_Font_(iFont *d, iGlyph *glyph) {
206 iText *txt = &text_;
207 SDL_Renderer *render = txt->render;
208 SDL_Texture *tex = NULL;
209 SDL_Surface *surface = NULL;
210 const iChar ch = char_Glyph(glyph);
211 iBool fromStb = iFalse;
212 if (!isSpecialChar_(ch)) {
213 /* Rasterize the glyph using stbtt. */
214 surface = rasterizeGlyph_Font_(d, ch);
215 stbtt_GetCodepointBitmapBox(
216 &d->font, ch, d->scale, d->scale, &glyph->dx, &glyph->dy, NULL, NULL);
217 fromStb = iTrue;
218 tex = SDL_CreateTextureFromSurface(render, surface);
219 glyph->rect.size = init_I2(surface->w, surface->h);
220 }
221 else {
222 /* Metrics for special symbols. */
223 int em, lsb;
224 const int symbol = specialChar_(ch);
225 stbtt_GetCodepointHMetrics(&d->font, 'M', &em, &lsb);
226 glyph->dx = d->baseline / 10;
227 glyph->dy = -d->baseline;
228 glyph->rect.size = init_I2(symbolEmWidth_(symbol) * em * d->scale, d->height);
229#if 0
230 if (isRasterizedSymbol_(ch)) {
231 /* Rasterize manually. */
232 surface = SDL_CreateRGBSurfaceWithFormat(
233 0, width_Rect(glyph->rect), height_Rect(glyph->rect), 32, SDL_PIXELFORMAT_RGBA8888);
234 SDL_FillRect(surface, NULL, 0);
235 const uint32_t white = 0xffffffff;
236 switch (specialChar_(ch)) {
237 case play_SpecialSymbol:
238 fillTriangle_(surface, &(SDL_Rect){ 0, 0, surface->w, d->baseline }, 1);
239 break;
240 case pause_SpecialSymbol: {
241 const int w = surface->w * 4 / 11;
242 SDL_FillRect(surface, &(SDL_Rect){ 0, 0, w, d->baseline }, white);
243 SDL_FillRect(surface, &(SDL_Rect){ surface->w - w, 0, w, d->baseline }, white);
244 break;
245 }
246 case rewind_SpecialSymbol: {
247 const int w1 = surface->w / 7;
248 const int w2 = surface->w * 3 / 7;
249 const int h = d->baseline * 4 / 5;
250 const int off = (d->baseline - h) / 2;
251 SDL_FillRect(surface, &(SDL_Rect){ 0, off, w1, h}, white);
252 fillTriangle_(surface, &(SDL_Rect){ w1, off, w2, h }, -1);
253 fillTriangle_(surface, &(SDL_Rect){ surface->w * 4 / 7, off, w2, h }, -1);
254 break;
255 }
256 }
257 tex = SDL_CreateTextureFromSurface(render, surface);
258 }
259#endif
260 }
261 /* Determine placement in the glyph cache texture, advancing in rows. */
262 if (txt->cachePos.x + glyph->rect.size.x > txt->cacheSize.x) {
263 txt->cachePos.x = 0;
264 txt->cachePos.y += txt->cacheRowHeight;
265 txt->cacheRowHeight = 0;
266 }
267 glyph->rect.pos = txt->cachePos;
268 SDL_SetRenderTarget(render, txt->cache);
269 const SDL_Rect dstRect = sdlRect_(glyph->rect);
270 if (surface) {
271 SDL_RenderCopy(render, tex, &(SDL_Rect){ 0, 0, dstRect.w, dstRect.h }, &dstRect);
272 }
273 else {
274 /* Draw a special symbol. */
275 SDL_SetRenderDrawColor(render, 255, 255, 255, 255);
276 const iInt2 tl = init_I2(dstRect.x, dstRect.y);
277 const iInt2 br = init_I2(dstRect.x + dstRect.w - 1, dstRect.y + dstRect.h - 1);
278 const int midX = tl.x + dstRect.w / 2;
279 const int midY = tl.y + dstRect.h / 2;
280 const int symH = dstRect.h * 2 / 6;
281#if 0
282 /* Frame. */
283 if (isFramedSymbol_(ch)) {
284 SDL_RenderDrawLines(
285 render,
286 (SDL_Point[]){
287 { tl.x, tl.y }, { br.x, tl.y }, { br.x, br.y }, { tl.x, br.y }, { tl.x, tl.y } },
288 5);
289 }
290 iArray points;
291 init_Array(&points, sizeof(SDL_Point));
292 switch (specialChar_(ch)) {
293 case 0: /* silence */
294 break;
295 case 1: /* sine */
296 for (int i = 0; i < dstRect.w; ++i) {
297 float rad = 2.0f * iMathPif * (float) i / dstRect.w;
298 SDL_Point pt = { tl.x + i, midY + sin(rad) * symH};
299 pushBack_Array(&points, &pt);
300 }
301 SDL_RenderDrawLines(render, constData_Array(&points), size_Array(&points));
302 break;
303 case 2: /* square */
304 SDL_RenderDrawLines(render,
305 (SDL_Point[]){ { tl.x, midY - symH },
306 { midX, midY - symH },
307 { midX, midY + symH },
308 { br.x, midY + symH } },
309 4);
310 break;
311 case 3: /* saw */
312 SDL_RenderDrawLines(render,
313 (SDL_Point[]){ { tl.x, midY },
314 { midX, midY - symH },
315 { midX, midY + symH },
316 { br.x, midY } },
317 4);
318 break;
319 case 4: /* triangle */
320 SDL_RenderDrawLines(render,
321 (SDL_Point[]){ { tl.x, midY },
322 { tl.x + dstRect.w / 4, midY - symH },
323 { br.x - dstRect.w / 4, midY + symH },
324 { br.x, midY } },
325 4);
326 break;
327 case 5: /* noise */
328 for (int i = 0; i < dstRect.w; ++i) {
329 for (int p = 0; p < 2; ++p) {
330 const float val = iRandomf() * 2.0f - 1.0f;
331 pushBack_Array(&points, &(SDL_Point){ tl.x + i, midY - val * symH });
332 }
333 }
334 SDL_RenderDrawPoints(render, constData_Array(&points), size_Array(&points));
335 break;
336 }
337 deinit_Array(&points);
338#endif
339 }
340 SDL_SetRenderTarget(render, NULL);
341 if (tex) {
342 SDL_DestroyTexture(tex);
343 iAssert(surface);
344 if (fromStb) stbtt_FreeBitmap(surface->pixels, NULL);
345 SDL_FreeSurface(surface);
346 }
347 /* Update cache cursor. */
348 txt->cachePos.x += glyph->rect.size.x;
349 txt->cacheRowHeight = iMax(txt->cacheRowHeight, glyph->rect.size.y);
350}
351
352static const iGlyph *glyph_Font_(iFont *d, iChar ch) {
353 const void *node = value_Hash(&d->glyphs, ch);
354 if (node) {
355 return node;
356 }
357 iGlyph *glyph = new_Glyph(ch);
358 cache_Font_(d, glyph);
359 insert_Hash(&d->glyphs, &glyph->node);
360 return glyph;
361}
362
363enum iRunMode { measure_RunMode, draw_RunMode, drawPermanentColor_RunMode };
364
365static iInt2 run_Font_(iFont *d, enum iRunMode mode, const char *text, size_t maxLen, iInt2 pos,
366 int *runAdvance_out) {
367 iInt2 size = zero_I2();
368 const iInt2 orig = pos;
369 const stbtt_fontinfo *info = &d->font;
370 float xpos = pos.x;
371 float xposMax = xpos;
372 const iString textStr = iStringLiteral(text);
373 iConstForEach(String, i, &textStr) {
374 iChar ch = i.value;
375 /* Special instructions. */ {
376 if (ch == '\n') {
377 xpos = pos.x;
378 pos.y += d->height;
379 continue;
380 }
381 if (ch == '\r') {
382 next_StringConstIterator(&i);
383 const iColor clr = get_Color(i.value - '0');
384 if (mode == draw_RunMode) {
385 SDL_SetTextureColorMod(text_.cache, clr.r, clr.g, clr.b);
386 }
387 continue;
388 }
389 }
390 const iGlyph *glyph = glyph_Font_(d, ch);
391 int x1 = iRound(xpos);
392 int x2 = x1 + glyph->rect.size.x;
393 size.x = iMax(size.x, x2 - orig.x);
394 size.y = iMax(size.y, pos.y + d->height - orig.y);
395 if (mode != measure_RunMode) {
396 SDL_Rect dst = { x1 + glyph->dx,
397 pos.y + d->baseline + glyph->dy,
398 glyph->rect.size.x,
399 glyph->rect.size.y };
400 SDL_RenderCopy(text_.render, text_.cache, (const SDL_Rect *) &glyph->rect, &dst);
401 }
402 int advance, lsb;
403 const iBool spec = isSpecialChar_(ch);
404 stbtt_GetCodepointHMetrics(info, spec ? 'M' : ch, &advance, &lsb);
405 if (spec) {
406 advance *= symbolAdvance_(specialChar_(ch));
407 }
408 xpos += d->scale * advance;
409 xposMax = iMax(xposMax, xpos);
410 /* Check the next character. */ {
411 iStringConstIterator j = i;
412 next_StringConstIterator(&j);
413 const iChar next = j.value;
414 if (next) {
415 xpos += d->scale * stbtt_GetCodepointKernAdvance(info, ch, next);
416 }
417 }
418 if (--maxLen == 0) {
419 break;
420 }
421 }
422 if (runAdvance_out) {
423 *runAdvance_out = xposMax - orig.x;
424 }
425 return size;
426}
427
428int lineHeight_Text(int fontId) {
429 return text_.fonts[fontId].height;
430}
431
432iInt2 measure_Text(int fontId, const char *text) {
433 if (!*text) {
434 return init_I2(0, lineHeight_Text(fontId));
435 }
436 return run_Font_(&text_.fonts[fontId], measure_RunMode, text, iInvalidSize, zero_I2(), NULL);
437}
438
439iInt2 advance_Text(int fontId, const char *text) {
440 int advance;
441 const int height =
442 run_Font_(&text_.fonts[fontId], measure_RunMode, text, iInvalidSize, zero_I2(), &advance).y;
443 return init_I2(advance, height); //lineHeight_Text(fontId));
444}
445
446iInt2 advanceN_Text(int fontId, const char *text, size_t n) {
447 if (n == 0) {
448 return init_I2(0, lineHeight_Text(fontId));
449 }
450 int advance;
451 run_Font_(&text_.fonts[fontId], measure_RunMode, text, n, zero_I2(), &advance);
452 return init_I2(advance, lineHeight_Text(fontId));
453}
454
455static void draw_Text_(int fontId, iInt2 pos, int color, const char *text) {
456 iText *d = &text_;
457 const iColor clr = get_Color(color & mask_ColorId);
458 SDL_SetTextureColorMod(d->cache, clr.r, clr.g, clr.b);
459 run_Font_(&d->fonts[fontId],
460 color & permanent_ColorId ? drawPermanentColor_RunMode : draw_RunMode,
461 text,
462 iInvalidSize,
463 pos,
464 NULL);
465}
466
467void draw_Text(int fontId, iInt2 pos, int color, const char *text, ...) {
468 iBlock chars;
469 init_Block(&chars, 0); {
470 va_list args;
471 va_start(args, text);
472 vprintf_Block(&chars, text, args);
473 va_end(args);
474 }
475 if (pos.x < 0) {
476 /* Right-aligned. */
477 pos.x = -pos.x - measure_Text(fontId, cstr_Block(&chars)).x;
478 }
479 if (pos.y < 0) {
480 /* Bottom-aligned. */
481 pos.y = -pos.y - lineHeight_Text(fontId);
482 }
483 draw_Text_(fontId, pos, color, cstr_Block(&chars));
484 deinit_Block(&chars);
485}
486
487void drawCentered_Text(int fontId, iRect rect, int color, const char *text, ...) {
488 iBlock chars;
489 init_Block(&chars, 0); {
490 va_list args;
491 va_start(args, text);
492 vprintf_Block(&chars, text, args);
493 va_end(args);
494 }
495 const iInt2 textSize = advance_Text(fontId, cstr_Block(&chars));
496 draw_Text_(fontId, sub_I2(mid_Rect(rect), divi_I2(textSize, 2)), color, cstr_Block(&chars));
497 deinit_Block(&chars);
498}
499
500SDL_Texture *glyphCache_Text(void) {
501 return text_.cache;
502}
503
504/*-----------------------------------------------------------------------------------------------*/
505
506iDefineTypeConstructionArgs(TextBuf, (int font, const char *text), font, text)
507
508void init_TextBuf(iTextBuf *d, int font, const char *text) {
509 SDL_Renderer *render = text_.render;
510 d->size = advance_Text(font, text);
511 d->texture = SDL_CreateTexture(render,
512 SDL_PIXELFORMAT_RGBA8888,
513 SDL_TEXTUREACCESS_STATIC | SDL_TEXTUREACCESS_TARGET,
514 d->size.x,
515 d->size.y);
516 SDL_SetTextureBlendMode(d->texture, SDL_BLENDMODE_BLEND);
517 SDL_SetRenderTarget(render, d->texture);
518 draw_Text_(font, zero_I2(), white_ColorId, text);
519 SDL_SetRenderTarget(render, NULL);
520}
521
522void deinit_TextBuf(iTextBuf *d) {
523 SDL_DestroyTexture(d->texture);
524}
525
526void draw_TextBuf(const iTextBuf *d, iInt2 pos, int color) {
527 const iColor clr = get_Color(color);
528 SDL_SetTextureColorMod(d->texture, clr.r, clr.g, clr.b);
529 SDL_RenderCopy(text_.render,
530 d->texture,
531 &(SDL_Rect){ 0, 0, d->size.x, d->size.y },
532 &(SDL_Rect){ pos.x, pos.y, d->size.x, d->size.y });
533}
diff --git a/src/ui/text.h b/src/ui/text.h
new file mode 100644
index 00000000..b689582e
--- /dev/null
+++ b/src/ui/text.h
@@ -0,0 +1,52 @@
1#pragma once
2
3#include <the_Foundation/rect.h>
4#include <the_Foundation/string.h>
5
6#include <SDL_render.h>
7
8enum iFontId {
9 default_FontId,
10 uiShortcuts_FontId,
11 uiInput_FontId,
12 /* Document fonts: */
13 paragraph_FontId,
14 firstParagraph_FontId,
15 preformatted_FontId,
16 quote_FontId,
17 header1_FontId,
18 header2_FontId,
19 header3_FontId,
20 max_FontId
21};
22
23#define specialSymbol_Text 0x10
24
25enum iSpecialSymbol {
26 silence_SpecialSymbol,
27};
28
29void init_Text (SDL_Renderer *);
30void deinit_Text (void);
31
32int lineHeight_Text (int font);
33iInt2 measure_Text (int font, const char *text);
34iInt2 advance_Text (int font, const char *text);
35iInt2 advanceN_Text (int font, const char *text, size_t n);
36
37void draw_Text (int font, iInt2 pos, int color, const char *text, ...); /* negative pos to switch alignment */
38void drawCentered_Text (int font, iRect rect, int color, const char *text, ...);
39
40SDL_Texture * glyphCache_Text (void);
41
42/*-----------------------------------------------------------------------------------------------*/
43
44iDeclareType(TextBuf)
45iDeclareTypeConstructionArgs(TextBuf, int font, const char *text)
46
47struct Impl_TextBuf {
48 SDL_Texture *texture;
49 iInt2 size;
50};
51
52void draw_TextBuf (const iTextBuf *, iInt2 pos, int color);
diff --git a/src/ui/util.c b/src/ui/util.c
new file mode 100644
index 00000000..9487e004
--- /dev/null
+++ b/src/ui/util.c
@@ -0,0 +1,604 @@
1#include "util.h"
2
3#include "app.h"
4#include "color.h"
5#include "command.h"
6#include "labelwidget.h"
7#include "inputwidget.h"
8#include "widget.h"
9#include "text.h"
10#include "window.h"
11
12#include <the_Foundation/math.h>
13#include <the_Foundation/path.h>
14
15iBool isCommand_UserEvent(const SDL_Event *d, const char *cmd) {
16 return d->type == SDL_USEREVENT && d->user.code == command_UserEventCode &&
17 equal_Command(d->user.data1, cmd);
18}
19
20const char *command_UserEvent(const SDL_Event *d) {
21 if (d->type == SDL_USEREVENT && d->user.code == command_UserEventCode) {
22 return d->user.data1;
23 }
24 return "";
25}
26
27int keyMods_Sym(int kmods) {
28 kmods &= (KMOD_SHIFT | KMOD_ALT | KMOD_CTRL | KMOD_GUI);
29 /* Don't treat left/right modifiers differently. */
30 if (kmods & KMOD_SHIFT) kmods |= KMOD_SHIFT;
31 if (kmods & KMOD_ALT) kmods |= KMOD_ALT;
32 if (kmods & KMOD_CTRL) kmods |= KMOD_CTRL;
33 if (kmods & KMOD_GUI) kmods |= KMOD_GUI;
34 return kmods;
35}
36
37/*-----------------------------------------------------------------------------------------------*/
38
39void init_Click(iClick *d, iAnyObject *widget, int button) {
40 d->isActive = iFalse;
41 d->button = button;
42 d->bounds = as_Widget(widget);
43 d->startPos = zero_I2();
44 d->pos = zero_I2();
45}
46
47enum iClickResult processEvent_Click(iClick *d, const SDL_Event *event) {
48 if (event->type == SDL_MOUSEMOTION) {
49 const iInt2 pos = init_I2(event->motion.x, event->motion.y);
50 if (d->isActive) {
51 d->pos = pos;
52 return drag_ClickResult;
53 }
54 }
55 if (event->type != SDL_MOUSEBUTTONDOWN && event->type != SDL_MOUSEBUTTONUP) {
56 return none_ClickResult;
57 }
58 const SDL_MouseButtonEvent *mb = &event->button;
59 if (mb->button != d->button) {
60 return none_ClickResult;
61 }
62 const iInt2 pos = init_I2(mb->x, mb->y);
63 if (event->type == SDL_MOUSEBUTTONDOWN && mb->clicks == 2) {
64 if (contains_Widget(d->bounds, pos)) {
65 d->pos = pos;
66 setMouseGrab_Widget(NULL);
67 return double_ClickResult;
68 }
69 }
70 if (!d->isActive) {
71 if (mb->state == SDL_PRESSED) {
72 if (contains_Widget(d->bounds, pos)) {
73 d->isActive = iTrue;
74 d->startPos = d->pos = pos;
75 //setFlags_Widget(d->bounds, hover_WidgetFlag, iFalse);
76 setMouseGrab_Widget(d->bounds);
77 return started_ClickResult;
78 }
79 }
80 }
81 else { /* Active. */
82 if (mb->state == SDL_RELEASED) {
83 enum iClickResult result = contains_Widget(d->bounds, pos)
84 ? finished_ClickResult
85 : aborted_ClickResult;
86 d->isActive = iFalse;
87 d->pos = pos;
88 setMouseGrab_Widget(NULL);
89 return result;
90 }
91 }
92 return none_ClickResult;
93}
94
95void cancel_Click(iClick *d) {
96 if (d->isActive) {
97 d->isActive = iFalse;
98 setMouseGrab_Widget(NULL);
99 }
100}
101
102iBool isMoved_Click(const iClick *d) {
103 return dist_I2(d->startPos, d->pos) > 2;
104}
105
106iInt2 pos_Click(const iClick *d) {
107 return d->pos;
108}
109
110iRect rect_Click(const iClick *d) {
111 return initCorners_Rect(min_I2(d->startPos, d->pos), max_I2(d->startPos, d->pos));
112}
113
114iInt2 delta_Click(const iClick *d) {
115 return sub_I2(d->pos, d->startPos);
116}
117
118/*-----------------------------------------------------------------------------------------------*/
119
120iWidget *makePadding_Widget(int size) {
121 iWidget *pad = new_Widget();
122 setSize_Widget(pad, init1_I2(size));
123 return pad;
124}
125
126iLabelWidget *makeHeading_Widget(const char *text) {
127 iLabelWidget *heading = new_LabelWidget(text, 0, 0, NULL);
128 setFlags_Widget(as_Widget(heading), frameless_WidgetFlag | fixedSize_WidgetFlag, iTrue);
129 return heading;
130}
131
132iWidget *makeVDiv_Widget(void) {
133 iWidget *div = new_Widget();
134 setFlags_Widget(div, resizeChildren_WidgetFlag | arrangeVertical_WidgetFlag, iTrue);
135 return div;
136}
137
138iWidget *makeHDiv_Widget(void) {
139 iWidget *div = new_Widget();
140 setFlags_Widget(div, resizeChildren_WidgetFlag | arrangeHorizontal_WidgetFlag, iTrue);
141 return div;
142}
143
144iWidget *addAction_Widget(iWidget *parent, int key, int kmods, const char *command) {
145 iLabelWidget *action = new_LabelWidget("", key, kmods, command);
146 setSize_Widget(as_Widget(action), zero_I2());
147 addChildFlags_Widget(parent, iClob(action), hidden_WidgetFlag);
148 return as_Widget(action);
149}
150
151/*-----------------------------------------------------------------------------------------------*/
152
153static iBool menuHandler_(iWidget *menu, const char *cmd) {
154 if (isVisible_Widget(menu)) {
155 if (equal_Command(cmd, "menu.open") && pointer_Command(cmd) == menu->parent) {
156 /* Don't reopen self; instead, root will close the menu. */
157 return iFalse;
158 }
159 if (!equal_Command(cmd, "window.resized")) {
160 closeMenu_Widget(menu);
161 }
162 }
163 return iFalse;
164}
165
166iWidget *makeMenu_Widget(iWidget *parent, const iMenuItem *items, size_t n) {
167 iWidget *menu = new_Widget();
168 setBackgroundColor_Widget(menu, gray25_ColorId);
169 setFlags_Widget(menu,
170 keepOnTop_WidgetFlag | hidden_WidgetFlag | arrangeVertical_WidgetFlag |
171 arrangeSize_WidgetFlag | resizeChildrenToWidestChild_WidgetFlag,
172 iTrue);
173 for (size_t i = 0; i < n; ++i) {
174 const iMenuItem *item = &items[i];
175 if (equal_CStr(item->label, "---")) {
176 iWidget *sep = addChild_Widget(menu, iClob(new_Widget()));
177 setBackgroundColor_Widget(sep, gray50_ColorId);
178 sep->rect.size.y = gap_UI / 3;
179 setFlags_Widget(sep, hover_WidgetFlag | fixedHeight_WidgetFlag, iTrue);
180 }
181 else {
182 iLabelWidget *label = addChildFlags_Widget(
183 menu,
184 iClob(new_LabelWidget(item->label, item->key, item->kmods, item->command)),
185 frameless_WidgetFlag | alignLeft_WidgetFlag | drawKey_WidgetFlag);
186 updateSize_LabelWidget(label); /* drawKey was set */
187 }
188 }
189 addChild_Widget(parent, iClob(menu));
190 setCommandHandler_Widget(menu, menuHandler_);
191 addAction_Widget(menu, SDLK_ESCAPE, 0, "cancel");
192 return menu;
193}
194
195void openMenu_Widget(iWidget *d, iInt2 coord) {
196 /* Menu closes when commands are emitted, so handle any pending ones beforehand. */
197 processEvents_App();
198 setFlags_Widget(d, hidden_WidgetFlag, iFalse);
199 arrange_Widget(d);
200 d->rect.pos = coord;
201 /* Ensure the full menu is visible. */
202 const iInt2 rootSize = rootSize_Window(get_Window());
203 const int bottomExcess = bottom_Rect(bounds_Widget(d)) - rootSize.y;
204 if (bottomExcess > 0) {
205 d->rect.pos.y -= bottomExcess;
206 }
207 if (top_Rect(d->rect) < 0) {
208 d->rect.pos.y += -top_Rect(d->rect);
209 }
210 if (right_Rect(bounds_Widget(d)) > rootSize.x) {
211 d->rect.pos.x = coord.x - d->rect.size.x;
212 }
213 if (left_Rect(d->rect) < 0) {
214 d->rect.pos.x = 0;
215 }
216}
217
218void closeMenu_Widget(iWidget *d) {
219 setFlags_Widget(d, hidden_WidgetFlag, iTrue);
220}
221
222iLabelWidget *makeMenuButton_LabelWidget(const char *label, const iMenuItem *items, size_t n) {
223 iLabelWidget *button = new_LabelWidget(label, 0, 0, "menu.open");
224 iWidget *menu = makeMenu_Widget(as_Widget(button), items, n);
225 setId_Widget(menu, "menu");
226 return button;
227}
228
229/*-----------------------------------------------------------------------------------------------*/
230
231static iBool isTabPage_Widget_(const iWidget *tabs, const iWidget *page) {
232 return page->parent == findChild_Widget(tabs, "tabs.pages");
233}
234
235static iBool tabSwitcher_(iWidget *tabs, const char *cmd) {
236 if (equal_Command(cmd, "tabs.switch")) {
237 iWidget *target = pointerLabel_Command(cmd, "page");
238 if (!target) {
239 const iString *id = string_Command(cmd, "id");
240 target = findChild_Widget(tabs, cstr_String(id));
241 }
242 if (!target) return iFalse;
243 if (flags_Widget(target) & focusable_WidgetFlag) {
244 setFocus_Widget(target);
245 }
246 if (isTabPage_Widget_(tabs, target)) {
247 showTabPage_Widget(tabs, target);
248 return iTrue;
249 }
250 else if (hasParent_Widget(target, tabs)) {
251 /* Some widget on a page. */
252 while (!isTabPage_Widget_(tabs, target)) {
253 target = target->parent;
254 }
255 showTabPage_Widget(tabs, target);
256 return iTrue;
257 }
258 }
259 else if (equal_Command(cmd, "tabs.next") || equal_Command(cmd, "tabs.prev")) {
260 iWidget *pages = findChild_Widget(tabs, "tabs.pages");
261 int tabIndex = 0;
262 iConstForEach(ObjectList, i, pages->children) {
263 const iWidget *child = constAs_Widget(i.object);
264 if (isVisible_Widget(child)) break;
265 tabIndex++;
266 }
267 tabIndex += (equal_Command(cmd, "tabs.next") ? +1 : -1);
268 showTabPage_Widget(tabs, child_Widget(pages, iWrap(tabIndex, 0, childCount_Widget(pages))));
269 return iTrue;
270 }
271 return iFalse;
272}
273
274iWidget *makeTabs_Widget(iWidget *parent) {
275 iWidget *tabs = makeVDiv_Widget();
276 iWidget *buttons = addChild_Widget(tabs, iClob(new_Widget()));
277 setFlags_Widget(buttons, arrangeHorizontal_WidgetFlag | arrangeHeight_WidgetFlag, iTrue);
278 setId_Widget(buttons, "tabs.buttons");
279 iWidget *pages = addChildFlags_Widget(
280 tabs, iClob(new_Widget()), expand_WidgetFlag | resizeChildren_WidgetFlag);
281 setId_Widget(pages, "tabs.pages");
282 addChild_Widget(parent, iClob(tabs));
283 setCommandHandler_Widget(tabs, tabSwitcher_);
284 return tabs;
285}
286
287static void addTabPage_Widget_(iWidget *tabs, enum iWidgetAddPos addPos, iWidget *page,
288 const char *label, int key, int kmods) {
289 iWidget * pages = findChild_Widget(tabs, "tabs.pages");
290 const iBool isSel = childCount_Widget(pages) == 0;
291 iWidget * button = addChildPos_Widget(
292 findChild_Widget(tabs, "tabs.buttons"),
293 iClob(new_LabelWidget(label, key, kmods, cstrFormat_String("tabs.switch page:%p", page))),
294 addPos);
295 setFlags_Widget(button, selected_WidgetFlag, isSel);
296 addChildPos_Widget(pages, page, addPos);
297 setFlags_Widget(page, hidden_WidgetFlag | disabled_WidgetFlag, !isSel);
298}
299
300void appendTabPage_Widget(iWidget *tabs, iWidget *page, const char *label, int key, int kmods) {
301 addTabPage_Widget_(tabs, back_WidgetAddPos, page, label, key, kmods);
302}
303
304void prependTabPage_Widget(iWidget *tabs, iWidget *page, const char *label, int key, int kmods) {
305 addTabPage_Widget_(tabs, front_WidgetAddPos, page, label, key, kmods);
306}
307
308iWidget *tabPage_Widget(iWidget *tabs, size_t index) {
309 iWidget *pages = findChild_Widget(tabs, "tabs.pages");
310 return child_Widget(pages, index);
311}
312
313iWidget *removeTabPage_Widget(iWidget *tabs, size_t index) {
314 iWidget *buttons = findChild_Widget(tabs, "tabs.buttons");
315 iWidget *pages = findChild_Widget(tabs, "tabs.pages");
316 iWidget *button = removeChild_Widget(buttons, child_Widget(buttons, index));
317 iRelease(button);
318 iWidget *page = child_Widget(pages, index);
319 ref_Object(page);
320 setFlags_Widget(page, hidden_WidgetFlag | disabled_WidgetFlag, iFalse);
321 removeChild_Widget(pages, page);
322 return page;
323}
324
325void showTabPage_Widget(iWidget *tabs, const iWidget *page) {
326 /* Select the corresponding button. */ {
327 iWidget *buttons = findChild_Widget(tabs, "tabs.buttons");
328 iForEach(ObjectList, i, buttons->children) {
329 iAssert(isInstance_Object(i.object, &Class_LabelWidget));
330 iAny *label = i.object;
331 const iBool isSel =
332 (pointerLabel_Command(cstr_String(command_LabelWidget(label)), "page") == page);
333 setFlags_Widget(label, selected_WidgetFlag, isSel);
334 }
335 }
336 /* Show/hide pages. */ {
337 iWidget *pages = findChild_Widget(tabs, "tabs.pages");
338 iForEach(ObjectList, i, pages->children) {
339 iWidget *child = as_Widget(i.object);
340 setFlags_Widget(child, hidden_WidgetFlag | disabled_WidgetFlag, child != page);
341 }
342 }
343 /* Notify. */
344 if (!isEmpty_String(id_Widget(page))) {
345 postCommandf_App("tabs.changed id:%s", cstr_String(id_Widget(page)));
346 }
347}
348
349const iWidget *currentTabPage_Widget(const iWidget *tabs) {
350 iWidget *pages = findChild_Widget(tabs, "tabs.pages");
351 iConstForEach(ObjectList, i, pages->children) {
352 if (isVisible_Widget(constAs_Widget(i.object))) {
353 return constAs_Widget(i.object);
354 }
355 }
356 return NULL;
357}
358
359size_t tabCount_Widget(const iWidget *tabs) {
360 return childCount_Widget(findChild_Widget(tabs, "tabs.buttons"));
361}
362
363/*-----------------------------------------------------------------------------------------------*/
364
365static void acceptFilePath_(iWidget *dlg) {
366 iInputWidget *input = findChild_Widget(dlg, "input");
367 iString *path = makeAbsolute_Path(text_InputWidget(input));
368 postCommandf_App("%s path:%s", cstr_String(id_Widget(dlg)), cstr_String(path));
369 destroy_Widget(dlg);
370 delete_String(path);
371}
372
373iBool filePathHandler_(iWidget *dlg, const char *cmd) {
374 iWidget *ptr = as_Widget(pointer_Command(cmd));
375 if (equal_Command(cmd, "input.ended")) {
376 if (hasParent_Widget(ptr, dlg)) {
377 if (arg_Command(cmd)) {
378 acceptFilePath_(dlg);
379 }
380 else {
381 destroy_Widget(dlg);
382 }
383 return iTrue;
384 }
385 return iFalse;
386 }
387 else if (ptr && !hasParent_Widget(ptr, dlg)) {
388 /* Command from outside the dialog, so dismiss the dialog. */
389 if (!equal_Command(cmd, "focus.lost")) {
390 destroy_Widget(dlg);
391 }
392 return iFalse;
393 }
394 else if (equal_Command(cmd, "filepath.cancel")) {
395 end_InputWidget(findChild_Widget(dlg, "input"), iFalse);
396 destroy_Widget(dlg);
397 return iTrue;
398 }
399 else if (equal_Command(cmd, "filepath.accept")) {
400 acceptFilePath_(dlg);
401 return iTrue;
402 }
403 return iFalse;
404}
405
406iWidget *makeSheet_Widget(const char *id) {
407 iWidget *sheet = new_Widget();
408 setId_Widget(sheet, id);
409 setBackgroundColor_Widget(sheet, gray25_ColorId);
410 setFlags_Widget(sheet,
411 keepOnTop_WidgetFlag | arrangeVertical_WidgetFlag |
412 arrangeHeight_WidgetFlag,
413 iTrue);
414 const iInt2 rootSize = rootSize_Window(get_Window());
415 setSize_Widget(sheet, init_I2(rootSize.x / 2, 0));
416 setFlags_Widget(sheet, fixedHeight_WidgetFlag, iFalse);
417 return sheet;
418}
419
420void centerSheet_Widget(iWidget *sheet) {
421 arrange_Widget(sheet);
422 const iInt2 rootSize = rootSize_Window(get_Window());
423 sheet->rect.pos.x = rootSize.x / 2 - sheet->rect.size.x / 2;
424}
425
426void makeFilePath_Widget(iWidget * parent,
427 const iString *initialPath,
428 const char * title,
429 const char * acceptLabel,
430 const char * command) {
431 setFocus_Widget(NULL);
432 processEvents_App();
433 iWidget *dlg = makeSheet_Widget(command);
434 setCommandHandler_Widget(dlg, filePathHandler_);
435 addChild_Widget(parent, iClob(dlg));
436 addChildFlags_Widget(dlg, iClob(new_LabelWidget(title, 0, 0, NULL)), frameless_WidgetFlag);
437 iInputWidget *input = addChild_Widget(dlg, iClob(new_InputWidget(0)));
438 if (initialPath) {
439 setText_InputWidget(input, collect_String(makeRelative_Path(initialPath)));
440 }
441 setId_Widget(as_Widget(input), "input");
442 as_Widget(input)->rect.size.x = dlg->rect.size.x;
443 addChild_Widget(dlg, iClob(makePadding_Widget(gap_UI)));
444 iWidget *div = new_Widget(); {
445 setFlags_Widget(div, arrangeHorizontal_WidgetFlag | arrangeSize_WidgetFlag, iTrue);
446 addChild_Widget(div, iClob(new_LabelWidget("Cancel", SDLK_ESCAPE, 0, "filepath.cancel")));
447 addChild_Widget(div, iClob(new_LabelWidget(acceptLabel, SDLK_RETURN, 0, "filepath.accept")));
448 }
449 addChild_Widget(dlg, iClob(div));
450 centerSheet_Widget(dlg);
451 setFocus_Widget(as_Widget(input));
452}
453
454static void acceptValueInput_(iWidget *dlg) {
455 const iInputWidget *input = findChild_Widget(dlg, "input");
456 const iString *val = text_InputWidget(input);
457 postCommandf_App("%s arg:%d value:%s",
458 cstr_String(id_Widget(dlg)),
459 toInt_String(val),
460 cstr_String(val));
461}
462
463iBool valueInputHandler_(iWidget *dlg, const char *cmd) {
464 iWidget *ptr = as_Widget(pointer_Command(cmd));
465 if (equal_Command(cmd, "input.ended")) {
466 if (hasParent_Widget(ptr, dlg)) {
467 if (arg_Command(cmd)) {
468 acceptValueInput_(dlg);
469 }
470 destroy_Widget(dlg);
471 return iTrue;
472 }
473 return iFalse;
474 }
475 else if (equal_Command(cmd, "cancel")) {
476 destroy_Widget(dlg);
477 return iTrue;
478 }
479 else if (equal_Command(cmd, "valueinput.accept")) {
480 acceptValueInput_(dlg);
481 destroy_Widget(dlg);
482 return iTrue;
483 }
484 return iFalse;
485}
486
487iWidget *makeValueInput_Widget(iWidget *parent, const iString *initialValue, const char *title,
488 const char *prompt, const char *command) {
489 setFocus_Widget(NULL);
490 processEvents_App();
491 iWidget *dlg = makeSheet_Widget(command);
492 setCommandHandler_Widget(dlg, valueInputHandler_);
493 addChild_Widget(parent, iClob(dlg));
494 addChild_Widget(dlg, iClob(new_LabelWidget(title, 0, 0, NULL)));
495 addChild_Widget(dlg, iClob(new_LabelWidget(prompt, 0, 0, NULL)));
496 iInputWidget *input = addChild_Widget(dlg, iClob(new_InputWidget(0)));
497 if (initialValue) {
498 setText_InputWidget(input, initialValue);
499 }
500 setId_Widget(as_Widget(input), "input");
501 as_Widget(input)->rect.size.x = dlg->rect.size.x;
502 addChild_Widget(dlg, iClob(makePadding_Widget(gap_UI)));
503 iWidget *div = new_Widget(); {
504 setFlags_Widget(div, arrangeHorizontal_WidgetFlag | arrangeSize_WidgetFlag, iTrue);
505 addChild_Widget(div, iClob(new_LabelWidget("Cancel", SDLK_ESCAPE, 0, "cancel")));
506 addChild_Widget(div, iClob(new_LabelWidget(cyan_ColorEscape "OK", SDLK_RETURN, 0, "valueinput.accept")));
507 }
508 addChild_Widget(dlg, iClob(div));
509 centerSheet_Widget(dlg);
510 setFocus_Widget(as_Widget(input));
511 return dlg;
512}
513
514static iBool messageHandler_(iWidget *msg, const char *cmd) {
515 /* Any command dismisses the sheet. */
516 iUnused(cmd);
517 destroy_Widget(msg);
518 return iFalse;
519}
520
521void makeMessage_Widget(const char *title, const char *msg) {
522 iWidget *dlg = makeQuestion_Widget(
523 title, msg, (const char *[]){ "Continue" }, (const char *[]){ "message.ok" }, 1);
524 addAction_Widget(dlg, SDLK_ESCAPE, 0, "message.ok");
525 addAction_Widget(dlg, SDLK_SPACE, 0, "message.ok");
526}
527
528iWidget *makeQuestion_Widget(const char *title,
529 const char *msg,
530 const char *labels[],
531 const char *commands[],
532 size_t count) {
533 processEvents_App();
534 iWidget *dlg = makeSheet_Widget("");
535 setCommandHandler_Widget(dlg, messageHandler_);
536 addChild_Widget(dlg, iClob(new_LabelWidget(title, 0, 0, NULL)));
537 addChild_Widget(dlg, iClob(new_LabelWidget(msg, 0, 0, NULL)));
538 addChild_Widget(dlg, iClob(makePadding_Widget(gap_UI)));
539 iWidget *div = new_Widget(); {
540 setFlags_Widget(div, arrangeHorizontal_WidgetFlag | arrangeSize_WidgetFlag, iTrue);
541 for (size_t i = 0; i < count; ++i) {
542 /* The last one is the default option. */
543 const int key = (i == count - 1 ? SDLK_RETURN : 0);
544 addChild_Widget(div, iClob(new_LabelWidget(labels[i], key, 0, commands[i])));
545 }
546 }
547 addChild_Widget(dlg, iClob(div));
548 addChild_Widget(get_Window()->root, iClob(dlg));
549 centerSheet_Widget(dlg);
550 return dlg;
551}
552
553void setToggle_Widget(iWidget *d, iBool active) {
554 setFlags_Widget(d, selected_WidgetFlag, active);
555 updateText_LabelWidget(
556 (iLabelWidget *) d,
557 collectNewFormat_String(
558 "%s", isSelected_Widget(d) ? "YES" : "NO"));
559}
560
561static iBool toggleHandler_(iWidget *d, const char *cmd) {
562 if (equal_Command(cmd, "toggle") && pointer_Command(cmd) == d) {
563 setToggle_Widget(d, (flags_Widget(d) & selected_WidgetFlag) == 0);
564 postCommand_Widget(d,
565 cstrFormat_String("%s.changed arg:%d",
566 cstr_String(id_Widget(d)),
567 isSelected_Widget(d) ? 1 : 0));
568 return iTrue;
569 }
570 return iFalse;
571}
572
573iWidget *makeToggle_Widget(const char *id) {
574 iWidget *toggle = as_Widget(new_LabelWidget("YES", 0, 0, "toggle"));
575 setId_Widget(toggle, id);
576 setCommandHandler_Widget(toggle, toggleHandler_);
577 return toggle;
578}
579
580iWidget *makePreferences_Widget(void) {
581 iWidget *dlg = makeSheet_Widget("prefs");
582 addChild_Widget(dlg, iClob(new_LabelWidget(cyan_ColorEscape "PREFERENCES", 0, 0, NULL)));
583 iWidget *page = new_Widget();
584 addChild_Widget(dlg, iClob(page));
585 setFlags_Widget(page, arrangeHorizontal_WidgetFlag | arrangeSize_WidgetFlag, iTrue);
586 iWidget *headings = addChildFlags_Widget(
587 page, iClob(new_Widget()), arrangeVertical_WidgetFlag | arrangeSize_WidgetFlag);
588 iWidget *values = addChildFlags_Widget(
589 page, iClob(new_Widget()), arrangeVertical_WidgetFlag | arrangeSize_WidgetFlag);
590 addChild_Widget(headings, iClob(makeHeading_Widget("Retain window size:")));
591 addChild_Widget(values, iClob(makeToggle_Widget("prefs.retainwindow")));
592 addChild_Widget(headings, iClob(makeHeading_Widget("UI scale factor:")));
593 setId_Widget(addChild_Widget(values, iClob(new_InputWidget(8))), "prefs.uiscale");
594 arrange_Widget(dlg);
595// as_Widget(songDir)->rect.size.x = dlg->rect.size.x - headings->rect.size.x;
596 iWidget *div = new_Widget(); {
597 setFlags_Widget(div, arrangeHorizontal_WidgetFlag | arrangeSize_WidgetFlag, iTrue);
598 addChild_Widget(div, iClob(new_LabelWidget("Dismiss", SDLK_ESCAPE, 0, "prefs.dismiss")));
599 }
600 addChild_Widget(dlg, iClob(div));
601 addChild_Widget(get_Window()->root, iClob(dlg));
602 centerSheet_Widget(dlg);
603 return dlg;
604}
diff --git a/src/ui/util.h b/src/ui/util.h
new file mode 100644
index 00000000..04683a2f
--- /dev/null
+++ b/src/ui/util.h
@@ -0,0 +1,110 @@
1#pragma once
2
3#include <the_Foundation/rect.h>
4#include <the_Foundation/vec2.h>
5#include <SDL_events.h>
6#include <ctype.h>
7
8iDeclareType(Click)
9iDeclareType(Widget)
10iDeclareType(LabelWidget)
11
12iBool isCommand_UserEvent (const SDL_Event *, const char *cmd);
13const char * command_UserEvent (const SDL_Event *);
14
15iLocalDef iBool isResize_UserEvent(const SDL_Event *d) {
16 return isCommand_UserEvent(d, "window.resized");
17}
18
19#if defined (iPlatformApple)
20# define KMOD_PRIMARY KMOD_GUI
21# define KMOD_SECONDARY KMOD_CTRL
22#else
23# define KMOD_PRIMARY KMOD_CTRL
24# define KMOD_SECONDARY KMOD_GUI
25#endif
26
27int keyMods_Sym (int kmods); /* shift, alt, control, or gui */
28
29/*-----------------------------------------------------------------------------------------------*/
30
31enum iClickResult {
32 none_ClickResult,
33 started_ClickResult,
34 drag_ClickResult,
35 finished_ClickResult,
36 aborted_ClickResult,
37 double_ClickResult,
38};
39
40struct Impl_Click {
41 iBool isActive;
42 int button;
43 iWidget *bounds;
44 iInt2 startPos;
45 iInt2 pos;
46};
47
48void init_Click (iClick *, iAnyObject *widget, int button);
49enum iClickResult processEvent_Click (iClick *, const SDL_Event *event);
50void cancel_Click (iClick *);
51
52iBool isMoved_Click (const iClick *);
53iInt2 pos_Click (const iClick *);
54iRect rect_Click (const iClick *);
55iInt2 delta_Click (const iClick *);
56
57/*-----------------------------------------------------------------------------------------------*/
58
59iWidget * makePadding_Widget (int size);
60iLabelWidget * makeHeading_Widget (const char *text);
61iWidget * makeHDiv_Widget (void);
62iWidget * makeVDiv_Widget (void);
63iWidget * addAction_Widget (iWidget *parent, int key, int kmods, const char *command);
64
65/*-----------------------------------------------------------------------------------------------*/
66
67iWidget * makeToggle_Widget (const char *id);
68void setToggle_Widget (iWidget *toggle, iBool active);
69
70/*-----------------------------------------------------------------------------------------------*/
71
72iDeclareType(MenuItem)
73
74struct Impl_MenuItem {
75 const char *label;
76 int key;
77 int kmods;
78 const char *command;
79};
80
81iWidget * makeMenu_Widget (iWidget *parent, const iMenuItem *items, size_t n); /* returns no ref */
82void openMenu_Widget (iWidget *, iInt2 coord);
83void closeMenu_Widget (iWidget *);
84
85iLabelWidget * makeMenuButton_LabelWidget (const char *label, const iMenuItem *items, size_t n);
86
87/*-----------------------------------------------------------------------------------------------*/
88
89iWidget * makeTabs_Widget (iWidget *parent);
90void appendTabPage_Widget (iWidget *tabs, iWidget *page, const char *label, int key, int kmods);
91void prependTabPage_Widget (iWidget *tabs, iWidget *page, const char *label, int key, int kmods);
92iWidget * tabPage_Widget (iWidget *tabs, size_t index);
93iWidget * removeTabPage_Widget (iWidget *tabs, size_t index); /* returns the page */
94void showTabPage_Widget (iWidget *tabs, const iWidget *page);
95const iWidget *currentTabPage_Widget(const iWidget *tabs);
96size_t tabCount_Widget (const iWidget *tabs);
97
98/*-----------------------------------------------------------------------------------------------*/
99
100iWidget * makeSheet_Widget (const char *id);
101void centerSheet_Widget (iWidget *sheet);
102
103void makeFilePath_Widget (iWidget *parent, const iString *initialPath, const char *title,
104 const char *acceptLabel, const char *command);
105iWidget * makeValueInput_Widget (iWidget *parent, const iString *initialValue, const char *title,
106 const char *prompt, const char *command);
107void makeMessage_Widget (const char *title, const char *msg);
108iWidget * makeQuestion_Widget (const char *title, const char *msg,
109 const char *labels[], const char *commands[], size_t count);
110iWidget * makePreferences_Widget (void);
diff --git a/src/ui/widget.c b/src/ui/widget.c
new file mode 100644
index 00000000..49fcd7c0
--- /dev/null
+++ b/src/ui/widget.c
@@ -0,0 +1,618 @@
1#include "widget.h"
2
3#include "app.h"
4#include "command.h"
5#include "paint.h"
6#include "util.h"
7#include "window.h"
8
9#include <the_Foundation/ptrset.h>
10#include <SDL_mouse.h>
11#include <stdarg.h>
12
13iDeclareType(RootData)
14
15struct Impl_RootData {
16 iWidget *hover;
17 iWidget *mouseGrab;
18 iWidget *focus;
19 iPtrSet *onTop;
20 iPtrSet *pendingDestruction;
21};
22
23static iRootData rootData_;
24
25iPtrSet *onTop_RootData_(void) {
26 if (!rootData_.onTop) {
27 rootData_.onTop = new_PtrSet();
28 }
29 return rootData_.onTop;
30}
31
32void destroyPending_Widget(void) {
33 iForEach(PtrSet, i, rootData_.pendingDestruction) {
34 iWidget *widget = *i.value;
35 remove_PtrSet(onTop_RootData_(), widget);
36 iRelease(removeChild_Widget(widget->parent, widget));
37 remove_PtrSetIterator(&i);
38 }
39}
40
41iDefineObjectConstruction(Widget)
42
43void init_Widget(iWidget *d) {
44 init_String(&d->id);
45 d->flags = 0;
46 d->rect = zero_Rect();
47 d->bgColor = none_ColorId;
48 d->children = NULL;
49 d->parent = NULL;
50 d->commandHandler = NULL;
51}
52
53void deinit_Widget(iWidget *d) {
54 iReleasePtr(&d->children);
55 deinit_String(&d->id);
56}
57
58static void aboutToBeDestroyed_Widget_(iWidget *d) {
59 if (isFocused_Widget(d)) {
60 setFocus_Widget(NULL);
61 return;
62 }
63 if (isHover_Widget(d)) {
64 rootData_.hover = NULL;
65 }
66 iForEach(ObjectList, i, d->children) {
67 aboutToBeDestroyed_Widget_(as_Widget(i.object));
68 }
69}
70
71void destroy_Widget(iWidget *d) {
72 aboutToBeDestroyed_Widget_(d);
73 if (!rootData_.pendingDestruction) {
74 rootData_.pendingDestruction = new_PtrSet();
75 }
76 insert_PtrSet(rootData_.pendingDestruction, d);
77}
78
79void setId_Widget(iWidget *d, const char *id) {
80 setCStr_String(&d->id, id);
81}
82
83const iString *id_Widget(const iWidget *d) {
84 return &d->id;
85}
86
87int flags_Widget(const iWidget *d) {
88 return d->flags;
89}
90
91void setFlags_Widget(iWidget *d, int flags, iBool set) {
92 iChangeFlags(d->flags, flags, set);
93 if (flags & keepOnTop_WidgetFlag) {
94 if (set) {
95 insert_PtrSet(onTop_RootData_(), d);
96 }
97 else {
98 remove_PtrSet(onTop_RootData_(), d);
99 }
100 }
101}
102
103void setPos_Widget(iWidget *d, iInt2 pos) {
104 d->rect.pos = pos;
105}
106
107void setSize_Widget(iWidget *d, iInt2 size) {
108 d->rect.size = size;
109 setFlags_Widget(d, fixedSize_WidgetFlag, iTrue);
110}
111
112void setBackgroundColor_Widget(iWidget *d, int bgColor) {
113 d->bgColor = bgColor;
114}
115
116void setCommandHandler_Widget(iWidget *d, iBool (*handler)(iWidget *, const char *)) {
117 d->commandHandler = handler;
118}
119
120static int numExpandingChildren_Widget_(const iWidget *d) {
121 int count = 0;
122 iConstForEach(ObjectList, i, d->children) {
123 const iWidget *child = constAs_Widget(i.object);
124 if (flags_Widget(child) & expand_WidgetFlag) {
125 count++;
126 }
127 }
128 return count;
129}
130
131static int widestChild_Widget_(const iWidget *d) {
132 int width = 0;
133 iConstForEach(ObjectList, i, d->children) {
134 const iWidget *child = constAs_Widget(i.object);
135 width = iMax(width, child->rect.size.x);
136 }
137 return width;
138}
139
140static void setWidth_Widget_(iWidget *d, int width) {
141 if (~d->flags & fixedWidth_WidgetFlag) {
142 d->rect.size.x = width;
143 }
144}
145
146static void setHeight_Widget_(iWidget *d, int height) {
147 if (~d->flags & fixedHeight_WidgetFlag) {
148 d->rect.size.y = height;
149 }
150}
151
152void arrange_Widget(iWidget *d) {
153 if (d->flags & resizeToParentWidth_WidgetFlag) {
154 setWidth_Widget_(d, d->parent->rect.size.x);
155 }
156 if (d->flags & resizeToParentHeight_WidgetFlag) {
157 setHeight_Widget_(d, d->parent->rect.size.y);
158 }
159 /* The rest of the arrangement depends on child widgets. */
160 if (!d->children) {
161 return;
162 }
163 /* Resize children to fill the parent widget. */
164 const size_t childCount = size_ObjectList(d->children);
165 if (d->flags & resizeChildren_WidgetFlag) {
166 const int expCount = numExpandingChildren_Widget_(d);
167 /* Only resize the expanding children, not touching the others. */
168 if (expCount > 0) {
169 iInt2 avail = d->rect.size;
170 iConstForEach(ObjectList, i, d->children) {
171 const iWidget *child = constAs_Widget(i.object);
172 if (~child->flags & expand_WidgetFlag) {
173 subv_I2(&avail, child->rect.size);
174 }
175 }
176 avail = divi_I2(avail, expCount);
177 iForEach(ObjectList, j, d->children) {
178 iWidget *child = as_Widget(j.object);
179 if (child->flags & expand_WidgetFlag) {
180 if (d->flags & arrangeHorizontal_WidgetFlag) {
181 setWidth_Widget_(child, avail.x);
182 setHeight_Widget_(child, d->rect.size.y);
183 }
184 else if (d->flags & arrangeVertical_WidgetFlag) {
185 setWidth_Widget_(child, d->rect.size.x);
186 setHeight_Widget_(child, avail.y);
187 }
188 }
189 else {
190 /* Fill the off axis, though. */
191 if (d->flags & arrangeHorizontal_WidgetFlag) {
192 setHeight_Widget_(child, d->rect.size.y);
193 }
194 else if (d->flags & arrangeVertical_WidgetFlag) {
195 setWidth_Widget_(child, d->rect.size.x);
196 }
197 }
198 }
199 }
200 else {
201 /* Evenly size all children. */
202 iInt2 childSize = d->rect.size;
203 if (d->flags & arrangeHorizontal_WidgetFlag) {
204 childSize.x /= childCount;
205 }
206 else if (d->flags & arrangeVertical_WidgetFlag) {
207 childSize.y /= childCount;
208 }
209 iForEach(ObjectList, i, d->children) {
210 iWidget *child = as_Widget(i.object);
211 setWidth_Widget_(child, childSize.x);
212 setHeight_Widget_(child, childSize.y);
213 }
214 }
215 }
216 if (d->flags & resizeChildrenToWidestChild_WidgetFlag) {
217 const int widest = widestChild_Widget_(d);
218 iForEach(ObjectList, i, d->children) {
219 setWidth_Widget_(as_Widget(i.object), widest);
220 }
221 }
222 iInt2 pos = zero_I2();
223 iForEach(ObjectList, i, d->children) {
224 iWidget *child = as_Widget(i.object);
225 arrange_Widget(child);
226 if (d->flags & (arrangeHorizontal_WidgetFlag | arrangeVertical_WidgetFlag)) {
227 child->rect.pos = pos;
228 if (d->flags & arrangeHorizontal_WidgetFlag) {
229 pos.x += child->rect.size.x;
230 }
231 else {
232 pos.y += child->rect.size.y;
233 }
234 }
235 }
236 /* Update the size of the widget according to the arrangement. */
237 if (d->flags & arrangeSize_WidgetFlag) {
238 iRect bounds = zero_Rect();
239 iConstForEach(ObjectList, i, d->children) {
240 const iWidget *child = constAs_Widget(i.object);
241 if (isEmpty_Rect(bounds)) {
242 bounds = child->rect;
243 }
244 else {
245 bounds = union_Rect(bounds, child->rect);
246 }
247 }
248 if (d->flags & arrangeWidth_WidgetFlag) {
249 setWidth_Widget_(d, bounds.size.x);
250 /* Parent size changed, must update the children.*/
251 iForEach(ObjectList, j, d->children) {
252 iWidget *child = as_Widget(j.object);
253 if (child->flags & resizeToParentWidth_WidgetFlag) {
254 arrange_Widget(child);
255 }
256 }
257 }
258 if (d->flags & arrangeHeight_WidgetFlag) {
259 setHeight_Widget_(d, bounds.size.y);
260 /* Parent size changed, must update the children.*/
261 iForEach(ObjectList, j, d->children) {
262 iWidget *child = as_Widget(j.object);
263 if (child->flags & resizeToParentHeight_WidgetFlag) {
264 arrange_Widget(child);
265 }
266 }
267 }
268 }
269}
270
271iRect bounds_Widget(const iWidget *d) {
272 iRect bounds = d->rect;
273 for (const iWidget *w = d->parent; w; w = w->parent) {
274 addv_I2(&bounds.pos, w->rect.pos);
275 }
276 return bounds;
277}
278
279iInt2 localCoord_Widget(const iWidget *d, iInt2 coord) {
280 for (const iWidget *w = d; w; w = w->parent) {
281 subv_I2(&coord, w->rect.pos);
282 }
283 return coord;
284}
285
286iBool contains_Widget(const iWidget *d, iInt2 coord) {
287 const iRect bounds = { zero_I2(), d->rect.size };
288 return contains_Rect(bounds, localCoord_Widget(d, coord));
289}
290
291iLocalDef iBool isKeyboardEvent_(const SDL_Event *ev) {
292 return (ev->type == SDL_KEYUP || ev->type == SDL_KEYDOWN || ev->type == SDL_TEXTINPUT);
293}
294
295iLocalDef iBool isMouseEvent_(const SDL_Event *ev) {
296 return (ev->type == SDL_MOUSEWHEEL || ev->type == SDL_MOUSEMOTION ||
297 ev->type == SDL_MOUSEBUTTONUP || ev->type == SDL_MOUSEBUTTONDOWN);
298}
299
300static iBool filterEvent_Widget_(const iWidget *d, const SDL_Event *ev) {
301 const iBool isKey = isKeyboardEvent_(ev);
302 const iBool isMouse = isMouseEvent_(ev);
303 if (d->flags & disabled_WidgetFlag) {
304 if (isKey || isMouse) return iFalse;
305 }
306 if (d->flags & hidden_WidgetFlag) {
307 if (isMouse) return iFalse;
308 }
309 return iTrue;
310}
311
312void unhover_Widget(void) {
313 rootData_.hover = NULL;
314}
315
316iBool dispatchEvent_Widget(iWidget *d, const SDL_Event *ev) {
317 if (!d->parent) {
318 if (ev->type == SDL_MOUSEMOTION) {
319 /* Hover widget may change. */
320 rootData_.hover = NULL;
321 }
322 if (rootData_.focus && isKeyboardEvent_(ev)) {
323 /* Root dispatches keyboard events directly to the focused widget. */
324 if (dispatchEvent_Widget(rootData_.focus, ev)) {
325 return iTrue;
326 }
327 }
328 /* Root offers events first to widgets on top. */
329 iForEach(PtrSet, i, rootData_.onTop) {
330 iWidget *widget = *i.value;
331 if (isVisible_Widget(widget) && dispatchEvent_Widget(widget, ev)) {
332 return iTrue;
333 }
334 }
335 }
336 else if (ev->type == SDL_MOUSEMOTION && !rootData_.hover &&
337 flags_Widget(d) & hover_WidgetFlag && ~flags_Widget(d) & hidden_WidgetFlag &&
338 ~flags_Widget(d) & disabled_WidgetFlag) {
339 if (contains_Widget(d, init_I2(ev->motion.x, ev->motion.y))) {
340 rootData_.hover = d;
341 }
342 }
343 if (filterEvent_Widget_(d, ev)) {
344 /* Children may handle it first. Done in reverse so children drawn on top get to
345 handle the events first. */
346 iReverseForEach(ObjectList, i, d->children) {
347 iWidget *child = as_Widget(i.object);
348 if (child == rootData_.focus && isKeyboardEvent_(ev)) {
349 continue; /* Already dispatched. */
350 }
351 if (isVisible_Widget(child) && child->flags & keepOnTop_WidgetFlag) {
352 /* Already dispatched. */
353 continue;
354 }
355 if (dispatchEvent_Widget(child, ev)) {
356 return iTrue;
357 }
358 }
359 if (class_Widget(d)->processEvent(d, ev)) {
360 return iTrue;
361 }
362 }
363 return iFalse;
364}
365
366iBool processEvent_Widget(iWidget *d, const SDL_Event *ev) {
367 if (ev->type == SDL_KEYDOWN) {
368 if (ev->key.keysym.sym == SDLK_TAB) {
369 setFocus_Widget(findFocusable_Widget(focus_Widget(),
370 ev->key.keysym.mod & KMOD_SHIFT
371 ? backward_WidgetFocusDir
372 : forward_WidgetFocusDir));
373 return iTrue;
374 }
375 }
376 switch (ev->type) {
377 case SDL_USEREVENT: {
378 if (ev->user.code == command_UserEventCode && d->commandHandler &&
379 d->commandHandler(d, ev->user.data1)) {
380 return iTrue;
381 }
382 break;
383 }
384 }
385 return iFalse;
386}
387
388void draw_Widget(const iWidget *d) {
389 if (d->flags & hidden_WidgetFlag) return;
390 if (d->bgColor >= 0) {
391 iPaint p;
392 init_Paint(&p);
393 fillRect_Paint(&p, bounds_Widget(d), d->bgColor);
394 }
395 iConstForEach(ObjectList, i, d->children) {
396 const iWidget *child = constAs_Widget(i.object);
397 if (~child->flags & keepOnTop_WidgetFlag && ~child->flags & hidden_WidgetFlag) {
398 class_Widget(child)->draw(child);
399 }
400 }
401 /* Root draws the on-top widgets on top of everything else. */
402 if (!d->parent) {
403 iConstForEach(PtrSet, i, onTop_RootData_()) {
404 draw_Widget(*i.value);
405 }
406 }
407}
408
409iAny *addChild_Widget(iWidget *d, iAnyObject *child) {
410 return addChildPos_Widget(d, child, back_WidgetAddPos);
411}
412
413iAny *addChildPos_Widget(iWidget *d, iAnyObject *child, enum iWidgetAddPos addPos) {
414 iAssert(child);
415 iAssert(d != child);
416 iWidget *widget = as_Widget(child);
417 iAssert(!widget->parent);
418 if (!d->children) {
419 d->children = new_ObjectList();
420 }
421 if (addPos == back_WidgetAddPos) {
422 pushBack_ObjectList(d->children, widget); /* ref */
423 }
424 else {
425 pushFront_ObjectList(d->children, widget); /* ref */
426 }
427 widget->parent = d;
428 return child;
429}
430
431iAny *addChildFlags_Widget(iWidget *d, iAnyObject *child, int childFlags) {
432 setFlags_Widget(child, childFlags, iTrue);
433 return addChild_Widget(d, child);
434}
435
436iAny *removeChild_Widget(iWidget *d, iAnyObject *child) {
437 ref_Object(child);
438 iBool found = iFalse;
439 iForEach(ObjectList, i, d->children) {
440 if (i.object == child) {
441 remove_ObjectListIterator(&i);
442 found = iTrue;
443 break;
444 }
445 }
446 iAssert(found);
447 ((iWidget *) child)->parent = NULL;
448 return child;
449}
450
451iAny *child_Widget(iWidget *d, size_t index) {
452 iForEach(ObjectList, i, d->children) {
453 if (index-- == 0) {
454 return i.object;
455 }
456 }
457 return NULL;
458}
459
460iAny *findChild_Widget(const iWidget *d, const char *id) {
461 if (cmp_String(id_Widget(d), id) == 0) {
462 return iConstCast(iAny *, d);
463 }
464 iConstForEach(ObjectList, i, d->children) {
465 iAny *found = findChild_Widget(constAs_Widget(i.object), id);
466 if (found) return found;
467 }
468 return NULL;
469}
470
471size_t childCount_Widget(const iWidget *d) {
472 if (!d->children) return 0;
473 return size_ObjectList(d->children);
474}
475
476iBool isVisible_Widget(const iWidget *d) {
477 for (const iWidget *w = d; w; w = w->parent) {
478 if (w->flags & hidden_WidgetFlag) {
479 return iFalse;
480 }
481 }
482 return iTrue;
483}
484
485iBool isDisabled_Widget(const iWidget *d) {
486 for (const iWidget *w = d; w; w = w->parent) {
487 if (w->flags & disabled_WidgetFlag) {
488 return iTrue;
489 }
490 }
491 return iFalse;
492}
493
494iBool isFocused_Widget(const iWidget *d) {
495 return rootData_.focus == d;
496}
497
498iBool isHover_Widget(const iWidget *d) {
499 return rootData_.hover == d;
500}
501
502iBool isSelected_Widget(const iWidget *d) {
503 return (d->flags & selected_WidgetFlag) != 0;
504}
505
506iBool isCommand_Widget(const iWidget *d, const SDL_Event *ev, const char *cmd) {
507 if (isCommand_UserEvent(ev, cmd)) {
508 const iWidget *src = pointer_Command(command_UserEvent(ev));
509 iAssert(!src || strstr(ev->user.data1, " ptr:"));
510 return src == d || hasParent_Widget(src, d);
511 }
512 return iFalse;
513}
514
515iBool hasParent_Widget(const iWidget *d, const iWidget *someParent) {
516 if (d) {
517 for (const iWidget *w = d->parent; w; w = w->parent) {
518 if (w == someParent) return iTrue;
519 }
520 }
521 return iFalse;
522}
523
524void setFocus_Widget(iWidget *d) {
525 if (rootData_.focus != d) {
526 if (rootData_.focus) {
527 iAssert(!contains_PtrSet(rootData_.pendingDestruction, rootData_.focus));
528 postCommand_Widget(rootData_.focus, "focus.lost");
529 }
530 rootData_.focus = d;
531 if (d) {
532 iAssert(flags_Widget(d) & focusable_WidgetFlag);
533 postCommand_Widget(d, "focus.gained");
534 }
535 }
536}
537
538iWidget *focus_Widget(void) {
539 return rootData_.focus;
540}
541
542iWidget *hover_Widget(void) {
543 return rootData_.hover;
544}
545
546static const iWidget *findFocusable_Widget_(const iWidget *d, const iWidget *startFrom,
547 iBool *getNext, enum iWidgetFocusDir focusDir) {
548 if (startFrom == d) {
549 *getNext = iTrue;
550 return NULL;
551 }
552 if ((d->flags & focusable_WidgetFlag) && isVisible_Widget(d) && !isDisabled_Widget(d) &&
553 *getNext) {
554 return d;
555 }
556 if (focusDir == forward_WidgetFocusDir) {
557 iConstForEach(ObjectList, i, d->children) {
558 const iWidget *found =
559 findFocusable_Widget_(constAs_Widget(i.object), startFrom, getNext, focusDir);
560 if (found) return found;
561 }
562 }
563 else {
564 iReverseConstForEach(ObjectList, i, d->children) {
565 const iWidget *found =
566 findFocusable_Widget_(constAs_Widget(i.object), startFrom, getNext, focusDir);
567 if (found) return found;
568 }
569 }
570 return NULL;
571}
572
573iAny *findFocusable_Widget(const iWidget *startFrom, enum iWidgetFocusDir focusDir) {
574 iWidget *root = get_Window()->root;
575 iBool getNext = (startFrom ? iFalse : iTrue);
576 const iWidget *found = findFocusable_Widget_(root, startFrom, &getNext, focusDir);
577 if (!found && startFrom) {
578 getNext = iTrue;
579 found = findFocusable_Widget_(root, NULL, &getNext, focusDir);
580 }
581 return iConstCast(iWidget *, found);
582}
583
584void setMouseGrab_Widget(iWidget *d) {
585 if (rootData_.mouseGrab != d) {
586 rootData_.mouseGrab = d;
587 SDL_CaptureMouse(d != NULL);
588 }
589}
590
591iWidget *mouseGrab_Widget(void) {
592 return rootData_.mouseGrab;
593}
594
595void postCommand_Widget(const iWidget *d, const char *cmd, ...) {
596 iString str;
597 init_String(&str); {
598 va_list args;
599 va_start(args, cmd);
600 vprintf_Block(&str.chars, cmd, args);
601 va_end(args);
602 }
603 iBool isGlobal = iFalse;
604 if (*cstr_String(&str) == '!') {
605 isGlobal = iTrue;
606 remove_Block(&str.chars, 0, 1);
607 }
608 if (!isGlobal) {
609 appendFormat_String(&str, " ptr:%p", d);
610 }
611 postCommandString_App(&str);
612 deinit_String(&str);
613}
614
615iBeginDefineClass(Widget)
616 .processEvent = processEvent_Widget,
617 .draw = draw_Widget,
618iEndDefineClass(Widget)
diff --git a/src/ui/widget.h b/src/ui/widget.h
new file mode 100644
index 00000000..bf5c22f1
--- /dev/null
+++ b/src/ui/widget.h
@@ -0,0 +1,131 @@
1#pragma once
2
3/* Base class for UI widgets. */
4
5#include "metrics.h"
6
7#include <the_Foundation/object.h>
8#include <the_Foundation/objectlist.h>
9#include <the_Foundation/rect.h>
10#include <the_Foundation/string.h>
11#include <SDL_events.h>
12
13#define iDeclareWidgetClass(className) \
14 iDeclareType(className); \
15 typedef iWidgetClass i##className##Class; \
16 extern i##className##Class Class_##className;
17
18iDeclareType(Widget)
19iBeginDeclareClass(Widget)
20 iBool (*processEvent) (iWidget *, const SDL_Event *);
21 void (*draw) (const iWidget *);
22iEndDeclareClass(Widget)
23
24enum iWidgetFlag {
25 hidden_WidgetFlag = iBit(1),
26 disabled_WidgetFlag = iBit(2),
27 hover_WidgetFlag = iBit(3), /* eligible for mouse hover */
28 selected_WidgetFlag = iBit(4),
29 pressed_WidgetFlag = iBit(5),
30 alignLeft_WidgetFlag = iBit(6),
31 alignRight_WidgetFlag = iBit(7),
32 frameless_WidgetFlag = iBit(8),
33 drawKey_WidgetFlag = iBit(10),
34 focusable_WidgetFlag = iBit(11),
35 keepOnTop_WidgetFlag = iBit(12), /* gets events first; drawn last */
36 arrangeHorizontal_WidgetFlag = iBit(17), /* arrange children horizontally */
37 arrangeVertical_WidgetFlag = iBit(18), /* arrange children vertically */
38 arrangeWidth_WidgetFlag = iBit(19), /* area of children becomes parent size */
39 arrangeHeight_WidgetFlag = iBit(20), /* area of children becomes parent size */
40 arrangeSize_WidgetFlag = arrangeWidth_WidgetFlag | arrangeHeight_WidgetFlag,
41 resizeChildren_WidgetFlag = iBit(21), /* resize children to fill parent size */
42 expand_WidgetFlag = iBit(22),
43 fixedWidth_WidgetFlag = iBit(23),
44 fixedHeight_WidgetFlag = iBit(24),
45 fixedSize_WidgetFlag = fixedWidth_WidgetFlag | fixedHeight_WidgetFlag,
46 resizeChildrenToWidestChild_WidgetFlag = iBit(25),
47 resizeToParentWidth_WidgetFlag = iBit(26),
48 resizeToParentHeight_WidgetFlag = iBit(27),
49};
50
51enum iWidgetAddPos {
52 back_WidgetAddPos,
53 front_WidgetAddPos,
54};
55
56enum iWidgetFocusDir {
57 forward_WidgetFocusDir,
58 backward_WidgetFocusDir,
59};
60
61struct Impl_Widget {
62 iObject object;
63 iString id;
64 int flags;
65 iRect rect;
66 int bgColor;
67 iObjectList *children;
68 iWidget * parent;
69 iBool (*commandHandler)(iWidget *, const char *);
70};
71
72iDeclareObjectConstruction(Widget)
73
74iLocalDef iWidget *as_Widget(iAnyObject *d) {
75 if (d) {
76 iAssertIsObject(d);
77 iAssert(isInstance_Object(d, &Class_Widget));
78 }
79 return (iWidget *) d;
80}
81
82iLocalDef const iWidget *constAs_Widget(const iAnyObject *d) {
83 if (d) {
84 iAssertIsObject(d);
85 iAssert(isInstance_Object(d, &Class_Widget));
86 }
87 return (const iWidget *) d;
88}
89
90void destroy_Widget (iWidget *); /* widget removed and deleted later */
91void destroyPending_Widget(void);
92
93const iString *id_Widget (const iWidget *);
94int flags_Widget (const iWidget *);
95iRect bounds_Widget (const iWidget *);
96iInt2 localCoord_Widget (const iWidget *, iInt2 coord);
97iBool contains_Widget (const iWidget *, iInt2 coord);
98iAny * findChild_Widget (const iWidget *, const char *id);
99iAny * findFocusable_Widget(const iWidget *startFrom, enum iWidgetFocusDir focusDir);
100size_t childCount_Widget (const iWidget *);
101void draw_Widget (const iWidget *);
102
103iBool isVisible_Widget (const iWidget *);
104iBool isDisabled_Widget (const iWidget *);
105iBool isFocused_Widget (const iWidget *);
106iBool isHover_Widget (const iWidget *);
107iBool isSelected_Widget (const iWidget *);
108iBool isCommand_Widget (const iWidget *d, const SDL_Event *ev, const char *cmd);
109iBool hasParent_Widget (const iWidget *d, const iWidget *someParent);
110void setId_Widget (iWidget *, const char *id);
111void setFlags_Widget (iWidget *, int flags, iBool set);
112void setPos_Widget (iWidget *, iInt2 pos);
113void setSize_Widget (iWidget *, iInt2 size);
114void setBackgroundColor_Widget (iWidget *, int bgColor);
115void setCommandHandler_Widget (iWidget *, iBool (*handler)(iWidget *, const char *));
116iAny * addChild_Widget (iWidget *, iAnyObject *child); /* holds a ref */
117iAny * addChildPos_Widget (iWidget *, iAnyObject *child, enum iWidgetAddPos addPos);
118iAny * addChildFlags_Widget(iWidget *, iAnyObject *child, int childFlags); /* holds a ref */
119iAny * removeChild_Widget (iWidget *, iAnyObject *child); /* returns a ref */
120iAny * child_Widget (iWidget *, size_t index); /* O(n) */
121void arrange_Widget (iWidget *);
122iBool dispatchEvent_Widget(iWidget *, const SDL_Event *);
123iBool processEvent_Widget (iWidget *, const SDL_Event *);
124void postCommand_Widget (const iWidget *, const char *cmd, ...);
125
126void setFocus_Widget (iWidget *);
127iWidget *focus_Widget (void);
128iWidget *hover_Widget (void);
129void unhover_Widget (void);
130void setMouseGrab_Widget (iWidget *);
131iWidget *mouseGrab_Widget (void);
diff --git a/src/ui/window.c b/src/ui/window.c
new file mode 100644
index 00000000..8b4226ef
--- /dev/null
+++ b/src/ui/window.c
@@ -0,0 +1,441 @@
1#include "window.h"
2
3#include "app.h"
4#include "command.h"
5#include "paint.h"
6#include "text.h"
7#include "util.h"
8#include "labelwidget.h"
9#include "inputwidget.h"
10#include "embedded.h"
11#if defined (iPlatformMsys)
12# include "../win32.h"
13#endif
14#if defined (iPlatformApple) && !defined (iPlatformIOS)
15# include "macos.h"
16#endif
17
18#include <the_Foundation/file.h>
19#include <the_Foundation/path.h>
20#include <SDL_hints.h>
21#include <SDL_timer.h>
22#include <SDL_syswm.h>
23
24#define STB_IMAGE_IMPLEMENTATION
25#include "stb_image.h"
26
27static iWindow *theWindow_ = NULL;
28
29#if defined (iPlatformApple)
30static float initialUiScale_ = 1.0f;
31#else
32static float initialUiScale_ = 1.1f;
33#endif
34
35iDefineTypeConstruction(Window)
36
37static iBool handleRootCommands_(iWidget *root, const char *cmd) {
38 iUnused(root);
39 if (equal_Command(cmd, "menu.open")) {
40 iWidget *button = pointer_Command(cmd);
41 iWidget *menu = findChild_Widget(button, "menu");
42 iAssert(menu);
43 if (!isVisible_Widget(menu)) {
44 openMenu_Widget(menu, init_I2(0, button->rect.size.y));
45 }
46 else {
47 closeMenu_Widget(menu);
48 }
49 return iTrue;
50 }
51 else if (handleCommand_App(cmd)) {
52 return iTrue;
53 }
54 return iFalse;
55}
56
57static const iMenuItem fileMenuItems[] = {
58#if !defined (iPlatformApple)
59 { "Quit Lagrange", 'q', KMOD_PRIMARY, "quit" }
60#endif
61};
62
63static const iMenuItem editMenuItems[] = {
64#if !defined (iPlatformApple)
65 { "Preferences...", SDLK_COMMA, KMOD_PRIMARY, "preferences" }
66#endif
67};
68
69static const iMenuItem viewMenuItems[] = {
70};
71
72static void setupUserInterface_Window(iWindow *d) {
73 /* Children of root cover the entire window. */
74 setFlags_Widget(d->root, resizeChildren_WidgetFlag, iTrue);
75 setCommandHandler_Widget(d->root, handleRootCommands_);
76#if 0
77 iWidget *mainDiv = makeHDiv_Widget();
78 setId_Widget(mainDiv, "maindiv");
79 addChild_Widget(d->root, iClob(mainDiv));
80
81 iWidget *sidebar = makeVDiv_Widget();
82 setFlags_Widget(sidebar, arrangeWidth_WidgetFlag, iTrue);
83 setId_Widget(sidebar, "sidebar");
84 addChild_Widget(mainDiv, iClob(sidebar));
85
86 /* Menus. */ {
87#if defined (iPlatformApple) && !defined (iPlatformIOS)
88 /* Use the native menus. */
89 insertMenuItems_MacOS("File", fileMenuItems, iElemCount(fileMenuItems));
90 insertMenuItems_MacOS("Edit", editMenuItems, iElemCount(editMenuItems));
91 insertMenuItems_MacOS("View", viewMenuItems, iElemCount(viewMenuItems));
92#else
93 iWidget *menubar = new_Widget();
94 setBackgroundColor_Widget(menubar, gray25_ColorId);
95 setFlags_Widget(menubar, arrangeHorizontal_WidgetFlag | arrangeHeight_WidgetFlag, iTrue);
96 addChild_Widget(menubar, iClob(makeMenuButton_LabelWidget("File", fileMenuItems, iElemCount(fileMenuItems))));
97 addChild_Widget(menubar, iClob(makeMenuButton_LabelWidget("Edit", editMenuItems, iElemCount(editMenuItems))));
98 addChild_Widget(menubar, iClob(makeMenuButton_LabelWidget("View", viewMenuItems, iElemCount(viewMenuItems))));
99 addChild_Widget(sidebar, iClob(menubar));
100#endif
101 }
102 /* Tracker info. */ {
103 iWidget *trackerInfo = addChild_Widget(sidebar, iClob(new_Widget()));
104 setId_Widget(trackerInfo, "trackerinfo");
105 trackerInfo->rect.size.y = lineHeight_Text(default_FontId) + 2 * gap_UI;
106 setFlags_Widget(trackerInfo, arrangeHorizontal_WidgetFlag | resizeChildren_WidgetFlag, iTrue);
107 setId_Widget(
108 addChild_Widget(trackerInfo, iClob(new_LabelWidget("", 'p', KMOD_PRIMARY, "pattern.goto arg:-1"))),
109 "trackerinfo.current");
110 iLabelWidget *dims = new_LabelWidget("", 'r', KMOD_PRIMARY | KMOD_ALT, "pattern.resize");
111 setId_Widget(addChild_Widget(trackerInfo, iClob(dims)), "trackerinfo.dims");
112 }
113
114 iLibraryWidget *lib = new_LibraryWidget();
115 setId_Widget(as_Widget(lib), "library");
116 addChildFlags_Widget(sidebar, iClob(lib), expand_WidgetFlag);
117
118 iPlaybackWidget *play = new_PlaybackWidget();
119 setId_Widget(as_Widget(play), "playback");
120 addChild_Widget(sidebar, iClob(play));
121
122 iWidget *mainTabs = makeTabs_Widget(mainDiv);
123 setId_Widget(mainTabs, "maintabs");
124 setFlags_Widget(mainTabs, expand_WidgetFlag, iTrue);
125
126 /* Optional sidebar on the right. */
127 iWidget *sidebar2 = new_Widget();
128 setId_Widget(addChild_Widget(mainDiv, iClob(sidebar2)), "sidebar2");
129 setFlags_Widget(
130 sidebar2, fixedWidth_WidgetFlag | frameless_WidgetFlag | resizeChildren_WidgetFlag, iTrue);
131
132 /* Pattern sequence. */ {
133 iSequenceWidget *seq = new_SequenceWidget();
134 appendTabPage_Widget(mainTabs, iClob(seq), "SEQUENCE", 0, 0);
135 }
136 /* Tracker. */ {
137 iTrackerWidget *tracker = new_TrackerWidget();
138 appendTabPage_Widget(mainTabs, as_Widget(tracker), "PATTERN", 0, 0);
139 }
140 /* Voice editor. */ {
141 iWidget *voice = as_Widget(new_VoiceWidget());
142 setId_Widget(voice, "voicelayers");
143 appendTabPage_Widget(mainTabs, iClob(voice), "VOICE", '3', KMOD_PRIMARY);
144 }
145 /* Song information. */ {
146 iWidget *songPage = new_Widget();
147 setId_Widget(songPage, "songinfo");
148 setFlags_Widget(songPage, arrangeHorizontal_WidgetFlag, iTrue);
149 iWidget *headings =
150 addChildFlags_Widget(songPage,
151 iClob(new_Widget()),
152 resizeToParentHeight_WidgetFlag | resizeChildren_WidgetFlag |
153 arrangeVertical_WidgetFlag | arrangeWidth_WidgetFlag);
154 iWidget *values = addChildFlags_Widget(
155 songPage, iClob(new_Widget()), arrangeVertical_WidgetFlag | arrangeSize_WidgetFlag);
156
157 setId_Widget(addChild_Widget(headings, iClob(makePadding_Widget(2 * gap_UI))), "headings.padding");
158 setId_Widget(addChild_Widget(values, iClob(makePadding_Widget(2 * gap_UI))), "values.padding");
159
160 addChild_Widget(headings, iClob(makeHeading_Widget(cyan_ColorEscape "SONG PROPERTIES")));
161 addChild_Widget(values, iClob(makeHeading_Widget("")));
162
163 const int fieldWidth = advance_Text(monospace_FontId, "A").x * 40;
164 iWidget *field;
165
166 addChild_Widget(headings, iClob(makeHeading_Widget("Title:")));
167 setId_Widget(field = addChild_Widget(values, iClob(new_InputWidget(0))), "info.title");
168 field->rect.size.x = fieldWidth;
169
170 addChild_Widget(headings, iClob(makeHeading_Widget("Author:")));
171 setId_Widget(field = addChild_Widget(values, iClob(new_InputWidget(0))), "info.author");
172 field->rect.size.x = fieldWidth;
173
174 addChild_Widget(headings, iClob(makeHeading_Widget("Tempo:")));
175 setId_Widget(addChild_Widget(values, iClob(new_InputWidget(3))), "info.tempo");
176
177 addChild_Widget(headings, iClob(makeHeading_Widget("Events per Beat:")));
178 setId_Widget(addChild_Widget(values, iClob(new_InputWidget(2))), "info.eventsperbeat");
179
180 addChild_Widget(headings, iClob(makeHeading_Widget("Num of Tracks:")));
181 setId_Widget(addChild_Widget(values, iClob(new_InputWidget(2))), "info.numtracks");
182
183 addChild_Widget(headings, iClob(makePadding_Widget(2 * gap_UI)));
184 addChild_Widget(values, iClob(makePadding_Widget(2 * gap_UI)));
185
186 addChild_Widget(headings, iClob(makeHeading_Widget(cyan_ColorEscape "SONG METADATA")));
187 addChild_Widget(values, iClob(makeHeading_Widget("")));
188
189 addChild_Widget(headings, iClob(makeHeading_Widget("Duration:")));
190 setId_Widget(addChildFlags_Widget(values, iClob(newEmpty_LabelWidget()),
191 alignLeft_WidgetFlag | frameless_WidgetFlag),
192 "info.duration");
193 addChild_Widget(headings, iClob(makeHeading_Widget("Statistics:\n\n ")));
194 setId_Widget(addChildFlags_Widget(values,
195 iClob(newEmpty_LabelWidget()),
196 alignLeft_WidgetFlag | frameless_WidgetFlag),
197 "info.statistics");
198 addChild_Widget(headings, iClob(makeHeading_Widget("Created on:")));
199 setId_Widget(addChildFlags_Widget(values,
200 iClob(newEmpty_LabelWidget()),
201 alignLeft_WidgetFlag | frameless_WidgetFlag),
202 "info.created");
203
204 addChild_Widget(headings, iClob(makeHeading_Widget("Last Modified on:")));
205 setId_Widget(addChildFlags_Widget(values,
206 iClob(newEmpty_LabelWidget()),
207 alignLeft_WidgetFlag | frameless_WidgetFlag),
208 "info.lastmodified");
209 /* App info in the bottom. */ {
210 addChildFlags_Widget(headings, iClob(new_Widget()), expand_WidgetFlag);
211 addChildFlags_Widget(
212 headings,
213 iClob(new_LabelWidget(gray50_ColorEscape "Version " BWH_APP_VERSION, 0, 0, NULL)),
214 frameless_WidgetFlag | alignLeft_WidgetFlag);
215 }
216 appendTabPage_Widget(mainTabs, iClob(songPage), "INFO", '4', KMOD_PRIMARY);
217 }
218 /* Application status. */ {
219 iWidget *status = addChildFlags_Widget(d->root, iClob(newEmpty_LabelWidget()), 0);
220 setFont_LabelWidget((iLabelWidget *) status, monospace_FontId);
221 setFlags_Widget(status, frameless_WidgetFlag | alignRight_WidgetFlag, iTrue);
222 setId_Widget(status, "status");
223 }
224#endif
225 /* Glboal keyboard shortcuts. */ {
226 // addAction_Widget(d->root, SDLK_LEFTBRACKET, KMOD_SHIFT | KMOD_PRIMARY, "tabs.prev");
227 }
228}
229
230static void updateRootSize_Window_(iWindow *d) {
231 iInt2 *size = &d->root->rect.size;
232 SDL_GetRendererOutputSize(d->render, &size->x, &size->y);
233 arrange_Widget(d->root);
234 postCommandf_App("window.resized width:%d height:%d", size->x, size->y);
235}
236
237static float pixelRatio_Window_(const iWindow *d) {
238 int dx, x;
239 SDL_GetRendererOutputSize(d->render, &dx, NULL);
240 SDL_GetWindowSize(d->win, &x, NULL);
241 return (float) dx / (float) x;
242}
243
244void init_Window(iWindow *d) {
245 theWindow_ = d;
246 uint32_t flags = SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI;
247#if defined (iPlatformApple)
248 SDL_SetHint(SDL_HINT_RENDER_DRIVER, "metal");
249#else
250 flags |= SDL_WINDOW_OPENGL;
251#endif
252 SDL_SetHint(SDL_HINT_RENDER_VSYNC, "1");
253 if (SDL_CreateWindowAndRenderer(800, 500, flags, &d->win, &d->render)) {
254 fprintf(stderr, "Error when creating window: %s\n", SDL_GetError());
255 exit(-2);
256 }
257 SDL_SetWindowMinimumSize(d->win, 640, 480);
258 SDL_SetWindowTitle(d->win, "Lagrange");
259 SDL_ShowWindow(d->win);
260 /* Some info. */ {
261 SDL_RendererInfo info;
262 SDL_GetRendererInfo(d->render, &info);
263 printf("[window] renderer: %s\n", info.name);
264 }
265 d->uiScale = initialUiScale_;
266 d->pixelRatio = pixelRatio_Window_(d);
267 setPixelRatio_Metrics(d->pixelRatio * d->uiScale);
268#if defined (iPlatformMsys)
269 useExecutableIconResource_SDLWindow(d->win);
270#endif
271#if defined (iPlatformLinux)
272 /* Load the window icon. */ {
273 int w, h, num;
274 const iBlock *icon = &imageAppicon64_Embedded;
275 stbi_uc *pixels = stbi_load_from_memory(constData_Block(icon),
276 size_Block(icon),
277 &w,
278 &h,
279 &num,
280 STBI_rgb_alpha);
281 SDL_Surface *surf =
282 SDL_CreateRGBSurfaceWithFormatFrom(pixels, w, h, 32, 4 * w, SDL_PIXELFORMAT_RGBA32);
283 SDL_SetWindowIcon(d->win, surf);
284 SDL_FreeSurface(surf);
285 stbi_image_free(pixels);
286 }
287#endif
288 d->root = new_Widget();
289 d->presentTime = 0.0;
290 setId_Widget(d->root, "root");
291 init_Text(d->render);
292#if defined (iPlatformApple) && !defined (iPlatformIOS)
293 setupApplication_MacOS();
294#endif
295 setupUserInterface_Window(d);
296 updateRootSize_Window_(d);
297}
298
299void deinit_Window(iWindow *d) {
300 if (theWindow_ == d) {
301 theWindow_ = NULL;
302 }
303 iReleasePtr(&d->root);
304 deinit_Text();
305 SDL_DestroyRenderer(d->render);
306 SDL_DestroyWindow(d->win);
307}
308
309SDL_Renderer *renderer_Window(const iWindow *d) {
310 return d->render;
311}
312
313static iBool handleWindowEvent_Window_(iWindow *d, const SDL_WindowEvent *ev) {
314 switch (ev->event) {
315 case SDL_WINDOWEVENT_RESIZED:
316 updateRootSize_Window_(d);
317 return iTrue;
318 case SDL_WINDOWEVENT_LEAVE:
319 unhover_Widget();
320 return iTrue;
321 default:
322 break;
323 }
324 return iFalse;
325}
326
327iBool processEvent_Window(iWindow *d, const SDL_Event *ev) {
328 switch (ev->type) {
329 case SDL_WINDOWEVENT: {
330 return handleWindowEvent_Window_(d, &ev->window);
331 }
332 default: {
333 SDL_Event event = *ev;
334 /* Map mouse pointer coordinate to our coordinate system. */
335 if (event.type == SDL_MOUSEMOTION) {
336 const iInt2 pos = coord_Window(d, event.motion.x, event.motion.y);
337 event.motion.x = pos.x;
338 event.motion.y = pos.y;
339 }
340 else if (event.type == SDL_MOUSEBUTTONUP || event.type == SDL_MOUSEBUTTONDOWN) {
341 const iInt2 pos = coord_Window(d, event.button.x, event.button.y);
342 event.button.x = pos.x;
343 event.button.y = pos.y;
344 }
345 iWidget *widget = d->root;
346 if (event.type == SDL_MOUSEMOTION || event.type == SDL_MOUSEWHEEL ||
347 event.type == SDL_MOUSEBUTTONUP || event.type == SDL_MOUSEBUTTONDOWN) {
348 if (mouseGrab_Widget()) {
349 widget = mouseGrab_Widget();
350 }
351 }
352 return dispatchEvent_Widget(widget, &event);
353 }
354 }
355 return iFalse;
356}
357
358static void waitPresent_Window_(iWindow *d) {
359 const double ticksPerFrame = 1000.0 / 30.0;
360 uint32_t nowTime = SDL_GetTicks();
361 if (nowTime < d->presentTime) {
362 SDL_Delay((uint32_t) (d->presentTime - nowTime));
363 nowTime = SDL_GetTicks();
364 }
365 /* Now it is the presentation time. */
366 /* Figure out the next time in the future. */
367 if (d->presentTime <= nowTime) {
368 d->presentTime += ticksPerFrame * ((int) ((nowTime - d->presentTime) / ticksPerFrame) + 1);
369 }
370 else {
371 d->presentTime = nowTime;
372 }
373}
374
375void draw_Window(iWindow *d) {
376 /* Clear the window. */
377 SDL_SetRenderDrawColor(d->render, 0, 0, 0, 255);
378 SDL_RenderClear(d->render);
379 /* Draw widgets. */
380 d->frameTime = SDL_GetTicks();
381 draw_Widget(d->root);
382#if 0
383 /* Text cache debugging. */ {
384 SDL_Texture *cache = glyphCache_Text();
385 SDL_Rect rect = { 140, 60, 512, 512 };
386 SDL_SetRenderDrawColor(d->render, 0, 0, 0, 255);
387 SDL_RenderFillRect(d->render, &rect);
388 SDL_RenderCopy(d->render, glyphCache_Text(), NULL, &rect);
389 }
390#endif
391 waitPresent_Window_(d);
392 SDL_RenderPresent(d->render);
393}
394
395void resize_Window(iWindow *d, int w, int h) {
396 SDL_SetWindowSize(d->win, w, h);
397 updateRootSize_Window_(d);
398}
399
400void setUiScale_Window(iWindow *d, float uiScale) {
401 uiScale = iClamp(uiScale, 0.5f, 4.0f);
402 if (d) {
403 d->uiScale = uiScale;
404#if 0
405 deinit_Text();
406 setPixelRatio_Metrics(d->pixelRatio * d->uiScale);
407 init_Text(d->render);
408 postCommand_App("metrics.changed");
409 /* TODO: Dynamic UI metrics change. Widgets need to update themselves. */
410#endif
411 }
412 else {
413 initialUiScale_ = uiScale;
414 }
415}
416
417iInt2 rootSize_Window(const iWindow *d) {
418 return d->root->rect.size;
419}
420
421iInt2 coord_Window(const iWindow *d, int x, int y) {
422 return mulf_I2(init_I2(x, y), d->pixelRatio);
423}
424
425iInt2 mouseCoord_Window(const iWindow *d) {
426 int x, y;
427 SDL_GetMouseState(&x, &y);
428 return coord_Window(d, x, y);
429}
430
431float uiScale_Window(const iWindow *d) {
432 return d->uiScale;
433}
434
435uint32_t frameTime_Window(const iWindow *d) {
436 return d->frameTime;
437}
438
439iWindow *get_Window(void) {
440 return theWindow_;
441}
diff --git a/src/ui/window.h b/src/ui/window.h
new file mode 100644
index 00000000..d0413af4
--- /dev/null
+++ b/src/ui/window.h
@@ -0,0 +1,34 @@
1#pragma once
2
3#include "widget.h"
4
5#include <the_Foundation/defs.h>
6#include <SDL_events.h>
7#include <SDL_render.h>
8#include <SDL_video.h>
9
10iDeclareType(Window)
11iDeclareTypeConstruction(Window)
12
13struct Impl_Window {
14 SDL_Window * win;
15 SDL_Renderer *render;
16 iWidget * root;
17 float pixelRatio;
18 float uiScale;
19 uint32_t frameTime;
20 double presentTime;
21};
22
23iBool processEvent_Window (iWindow *, const SDL_Event *);
24void draw_Window (iWindow *);
25void resize_Window (iWindow *, int w, int h);
26void setUiScale_Window (iWindow *, float uiScale);
27
28iInt2 rootSize_Window (const iWindow *);
29float uiScale_Window (const iWindow *);
30iInt2 coord_Window (const iWindow *, int x, int y);
31iInt2 mouseCoord_Window (const iWindow *);
32uint32_t frameTime_Window (const iWindow *);
33
34iWindow * get_Window (void);