diff options
Diffstat (limited to 'src/ui')
-rw-r--r-- | src/ui/color.c | 27 | ||||
-rw-r--r-- | src/ui/color.h | 48 | ||||
-rw-r--r-- | src/ui/command.c | 89 | ||||
-rw-r--r-- | src/ui/command.h | 16 | ||||
-rw-r--r-- | src/ui/inputwidget.c | 280 | ||||
-rw-r--r-- | src/ui/inputwidget.h | 20 | ||||
-rw-r--r-- | src/ui/labelwidget.c | 281 | ||||
-rw-r--r-- | src/ui/labelwidget.h | 22 | ||||
-rw-r--r-- | src/ui/macos.h | 9 | ||||
-rw-r--r-- | src/ui/macos.m | 434 | ||||
-rw-r--r-- | src/ui/metrics.c | 16 | ||||
-rw-r--r-- | src/ui/metrics.h | 9 | ||||
-rw-r--r-- | src/ui/paint.c | 58 | ||||
-rw-r--r-- | src/ui/paint.h | 33 | ||||
-rw-r--r-- | src/ui/text.c | 533 | ||||
-rw-r--r-- | src/ui/text.h | 52 | ||||
-rw-r--r-- | src/ui/util.c | 604 | ||||
-rw-r--r-- | src/ui/util.h | 110 | ||||
-rw-r--r-- | src/ui/widget.c | 618 | ||||
-rw-r--r-- | src/ui/widget.h | 131 | ||||
-rw-r--r-- | src/ui/window.c | 441 | ||||
-rw-r--r-- | src/ui/window.h | 34 |
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 | |||
3 | static const iColor transparent_; | ||
4 | |||
5 | iColor 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 | |||
5 | enum 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 | |||
42 | iDeclareType(Color) | ||
43 | |||
44 | struct Impl_Color { | ||
45 | uint8_t r, g, b, a; | ||
46 | }; | ||
47 | |||
48 | iColor 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 | |||
7 | iBool 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 | |||
14 | static const iString *tokenString_(const char *label) { | ||
15 | return collectNewFormat_String(" %s:", label); | ||
16 | } | ||
17 | |||
18 | int 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 | |||
27 | int arg_Command(const char *cmd) { | ||
28 | return argLabel_Command(cmd, "arg"); | ||
29 | } | ||
30 | |||
31 | float 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 | |||
39 | void *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 | |||
50 | void *pointer_Command(const char *cmd) { | ||
51 | return pointerLabel_Command(cmd, "ptr"); | ||
52 | } | ||
53 | |||
54 | const 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 | |||
63 | const 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 | |||
72 | iInt2 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 | |||
82 | iInt2 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 | |||
5 | iBool equal_Command (const char *commandWithArgs, const char *command); | ||
6 | |||
7 | int arg_Command (const char *); /* arg: */ | ||
8 | float argf_Command (const char *); /* arg: */ | ||
9 | int argLabel_Command (const char *, const char *label); | ||
10 | void * pointer_Command (const char *); /* ptr: */ | ||
11 | void * pointerLabel_Command (const char *, const char *label); | ||
12 | iInt2 coord_Command (const char *); | ||
13 | iInt2 dir_Command (const char *); | ||
14 | |||
15 | const iString * string_Command (const char *, const char *label); | ||
16 | const 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 | |||
8 | struct 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 | |||
19 | iDefineObjectConstructionArgs(InputWidget, (size_t maxLen), maxLen) | ||
20 | |||
21 | void 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 | |||
38 | void deinit_InputWidget(iInputWidget *d) { | ||
39 | deinit_Array(&d->oldText); | ||
40 | deinit_Array(&d->text); | ||
41 | } | ||
42 | |||
43 | void setMode_InputWidget(iInputWidget *d, enum iInputMode mode) { | ||
44 | d->mode = mode; | ||
45 | } | ||
46 | |||
47 | const iString *text_InputWidget(const iInputWidget *d) { | ||
48 | return collect_String(newUnicodeN_String(constData_Array(&d->text), size_Array(&d->text))); | ||
49 | } | ||
50 | |||
51 | void 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 | |||
66 | void 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 | |||
73 | void setCursor_InputWidget(iInputWidget *d, size_t pos) { | ||
74 | d->cursor = iMin(pos, size_Array(&d->text)); | ||
75 | } | ||
76 | |||
77 | void 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 | |||
95 | void 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 | |||
111 | static 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 | |||
227 | static 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 | |||
277 | iBeginDefineSubclass(InputWidget, Widget) | ||
278 | .processEvent = (iAny *) processEvent_InputWidget_, | ||
279 | .draw = (iAny *) draw_InputWidget_, | ||
280 | iEndDefineSubclass(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 | |||
5 | iDeclareWidgetClass(InputWidget) | ||
6 | iDeclareObjectConstructionArgs(InputWidget, size_t maxLen) | ||
7 | |||
8 | enum iInputMode { | ||
9 | insert_InputMode, | ||
10 | overwrite_InputMode, | ||
11 | }; | ||
12 | |||
13 | void setMode_InputWidget (iInputWidget *, enum iInputMode mode); | ||
14 | void setMaxLen_InputWidget (iInputWidget *, size_t maxLen); | ||
15 | void setText_InputWidget (iInputWidget *, const iString *text); | ||
16 | void setCursor_InputWidget (iInputWidget *, size_t pos); | ||
17 | void begin_InputWidget (iInputWidget *); | ||
18 | void end_InputWidget (iInputWidget *, iBool accept); | ||
19 | |||
20 | const 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 | |||
8 | iLocalDef iInt2 padding_(void) { return init_I2(3 * gap_UI, gap_UI); } | ||
9 | |||
10 | struct 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 | |||
20 | iDefineObjectConstructionArgs(LabelWidget, | ||
21 | (const char *label, int key, int kmods, const char *cmd), | ||
22 | label, key, kmods, cmd) | ||
23 | |||
24 | static iBool checkModifiers_(int have, int req) { | ||
25 | return keyMods_Sym(req) == keyMods_Sym(have); | ||
26 | } | ||
27 | |||
28 | static void trigger_LabelWidget_(const iLabelWidget *d) { | ||
29 | postCommand_Widget(&d->widget, "%s", cstr_String(&d->command)); | ||
30 | } | ||
31 | |||
32 | static 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 | |||
67 | static 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 | |||
118 | static 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 | |||
210 | void 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 | |||
229 | void 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 | |||
246 | void deinit_LabelWidget(iLabelWidget *d) { | ||
247 | deinit_String(&d->label); | ||
248 | deinit_String(&d->command); | ||
249 | } | ||
250 | |||
251 | void setFont_LabelWidget(iLabelWidget *d, int fontId) { | ||
252 | d->font = fontId; | ||
253 | updateSize_LabelWidget(d); | ||
254 | } | ||
255 | |||
256 | void setText_LabelWidget(iLabelWidget *d, const iString *text) { | ||
257 | updateText_LabelWidget(d, text); | ||
258 | updateSize_LabelWidget(d); | ||
259 | } | ||
260 | |||
261 | void updateText_LabelWidget(iLabelWidget *d, const iString *text) { | ||
262 | set_String(&d->label, text); | ||
263 | } | ||
264 | |||
265 | void updateTextCStr_LabelWidget(iLabelWidget *d, const char *text) { | ||
266 | setCStr_String(&d->label, text); | ||
267 | } | ||
268 | |||
269 | void setTextCStr_LabelWidget(iLabelWidget *d, const char *text) { | ||
270 | setCStr_String(&d->label, text); | ||
271 | updateSize_LabelWidget(d); | ||
272 | } | ||
273 | |||
274 | const iString *command_LabelWidget(const iLabelWidget *d) { | ||
275 | return &d->command; | ||
276 | } | ||
277 | |||
278 | iBeginDefineSubclass(LabelWidget, Widget) | ||
279 | .processEvent = (iAny *) processEvent_LabelWidget_, | ||
280 | .draw = (iAny *) draw_LabelWidget_, | ||
281 | iEndDefineSubclass(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 | |||
7 | iDeclareWidgetClass(LabelWidget) | ||
8 | iDeclareObjectConstructionArgs(LabelWidget, const char *label, int key, int kmods, const char *command) | ||
9 | |||
10 | iLocalDef iLabelWidget *newEmpty_LabelWidget(void) { | ||
11 | return new_LabelWidget("", 0, 0, NULL); | ||
12 | } | ||
13 | |||
14 | void setFont_LabelWidget (iLabelWidget *, int fontId); | ||
15 | void setText_LabelWidget (iLabelWidget *, const iString *text); /* resizes widget */ | ||
16 | void setTextCStr_LabelWidget (iLabelWidget *, const char *text); | ||
17 | |||
18 | void updateSize_LabelWidget (iLabelWidget *); | ||
19 | void updateText_LabelWidget (iLabelWidget *, const iString *text); /* not resized */ | ||
20 | void updateTextCStr_LabelWidget (iLabelWidget *, const char *text); /* not resized */ | ||
21 | |||
22 | const 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 | |||
7 | void setupApplication_MacOS (void); | ||
8 | void insertMenuItems_MacOS (const char *menuLabel, const iMenuItem *items, size_t count); | ||
9 | void 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 | ||
10 | static NSTouchBarItemIdentifier play_TouchId_ = @"fi.skyjake.BitwiseHarmony.play"; | ||
11 | static NSTouchBarItemIdentifier restart_TouchId_ = @"fi.skyjake.BitwiseHarmony.restart"; | ||
12 | |||
13 | static NSTouchBarItemIdentifier seqMoveUp_TouchId_ = @"fi.skyjake.BitwiseHarmony.sequence.move.up"; | ||
14 | static NSTouchBarItemIdentifier seqMoveDown_TouchId_ = @"fi.skyjake.BitwiseHarmony.sequence.move.down"; | ||
15 | |||
16 | static NSTouchBarItemIdentifier goto_TouchId_ = @"fi.skyjake.BitwiseHarmony.goto"; | ||
17 | static NSTouchBarItemIdentifier mute_TouchId_ = @"fi.skyjake.BitwiseHarmony.mute"; | ||
18 | static NSTouchBarItemIdentifier solo_TouchId_ = @"fi.skyjake.BitwiseHarmony.solo"; | ||
19 | static NSTouchBarItemIdentifier color_TouchId_ = @"fi.skyjake.BitwiseHarmony.color"; | ||
20 | static NSTouchBarItemIdentifier event_TouchId_ = @"fi.skyjake.BitwiseHarmony.event"; | ||
21 | |||
22 | static NSTouchBarItemIdentifier eventList_TouchId_ = @"fi.skyjake.BitwiseHarmony.eventlist"; | ||
23 | static NSTouchBarItemIdentifier masterGainEvent_TouchId_ = @"fi.skyjake.BitwiseHarmony.event.mastergain"; | ||
24 | static NSTouchBarItemIdentifier resetEvent_TouchId_ = @"fi.skyjake.BitwiseHarmony.event.reset"; | ||
25 | static NSTouchBarItemIdentifier voiceEvent_TouchId_ = @"fi.skyjake.BitwiseHarmony.event.voice"; | ||
26 | static NSTouchBarItemIdentifier panEvent_TouchId_ = @"fi.skyjake.BitwiseHarmony.event.pan"; | ||
27 | static NSTouchBarItemIdentifier gainEvent_TouchId_ = @"fi.skyjake.BitwiseHarmony.event.gain"; | ||
28 | static NSTouchBarItemIdentifier fadeEvent_TouchId_ = @"fi.skyjake.BitwiseHarmony.event.fade"; | ||
29 | static NSTouchBarItemIdentifier pitchSpeedEvent_TouchId_ = @"fi.skyjake.BitwiseHarmony.event.pitchspeed"; | ||
30 | static NSTouchBarItemIdentifier pitchBendUpEvent_TouchId_ = @"fi.skyjake.BitwiseHarmony.event.pitchbendup"; | ||
31 | static NSTouchBarItemIdentifier pitchBendDownEvent_TouchId_ = @"fi.skyjake.BitwiseHarmony.event.pitchbenddown"; | ||
32 | static NSTouchBarItemIdentifier tremoloEvent_TouchId_ = @"fi.skyjake.BitwiseHarmony.event.tremolo"; | ||
33 | #endif | ||
34 | |||
35 | enum 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 | |||
347 | void 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 | |||
358 | void 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 | |||
417 | void 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 | |||
8 | int gap_UI = defaultGap_Metrics; | ||
9 | iInt2 gap2_UI = { defaultGap_Metrics, defaultGap_Metrics }; | ||
10 | int fontSize_UI = defaultFontSize_Metrics; | ||
11 | |||
12 | void 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 | |||
5 | extern int gap_UI; | ||
6 | extern int fontSize_UI; | ||
7 | extern iInt2 gap2_UI; | ||
8 | |||
9 | void 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 | |||
3 | iLocalDef SDL_Renderer *renderer_Paint_(const iPaint *d) { | ||
4 | iAssert(d->dst); | ||
5 | return d->dst->render; | ||
6 | } | ||
7 | |||
8 | static 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 | |||
13 | void init_Paint(iPaint *d) { | ||
14 | d->dst = get_Window(); | ||
15 | } | ||
16 | |||
17 | void setClip_Paint(iPaint *d, iRect rect) { | ||
18 | SDL_RenderSetClipRect(renderer_Paint_(d), (const SDL_Rect *) &rect); | ||
19 | } | ||
20 | |||
21 | void 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 | |||
26 | void 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 | |||
42 | void 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 | |||
50 | void 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 | |||
55 | void 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 | |||
8 | iDeclareType(Paint) | ||
9 | |||
10 | struct Impl_Paint { | ||
11 | iWindow *dst; | ||
12 | }; | ||
13 | |||
14 | void init_Paint (iPaint *); | ||
15 | |||
16 | void setClip_Paint (iPaint *, iRect rect); | ||
17 | void clearClip_Paint (iPaint *); | ||
18 | |||
19 | void drawRect_Paint (const iPaint *, iRect rect, int color); | ||
20 | void drawRectThickness_Paint (const iPaint *, iRect rect, int thickness, int color); | ||
21 | void fillRect_Paint (const iPaint *, iRect rect, int color); | ||
22 | |||
23 | void drawLines_Paint (const iPaint *, const iInt2 *points, size_t count, int color); | ||
24 | |||
25 | iLocalDef void drawLine_Paint(const iPaint *d, iInt2 a, iInt2 b, int color) { | ||
26 | drawLines_Paint(d, (iInt2[]){ a, b }, 2, color); | ||
27 | } | ||
28 | iLocalDef void drawHLine_Paint(const iPaint *d, iInt2 pos, int len, int color) { | ||
29 | drawLine_Paint(d, pos, addX_I2(pos, len), color); | ||
30 | } | ||
31 | iLocalDef 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 | |||
20 | iDeclareType(Glyph) | ||
21 | iDeclareTypeConstructionArgs(Glyph, iChar ch) | ||
22 | |||
23 | struct Impl_Glyph { | ||
24 | iHashNode node; | ||
25 | iRect rect; | ||
26 | int advance; | ||
27 | int dx, dy; | ||
28 | }; | ||
29 | |||
30 | void init_Glyph(iGlyph *d, iChar ch) { | ||
31 | d->node.key = ch; | ||
32 | d->rect = zero_Rect(); | ||
33 | d->advance = 0; | ||
34 | } | ||
35 | |||
36 | void deinit_Glyph(iGlyph *d) { | ||
37 | iUnused(d); | ||
38 | } | ||
39 | |||
40 | iChar char_Glyph(const iGlyph *d) { | ||
41 | return d->node.key; | ||
42 | } | ||
43 | |||
44 | iDefineTypeConstructionArgs(Glyph, (iChar ch), ch) | ||
45 | |||
46 | /*-----------------------------------------------------------------------------------------------*/ | ||
47 | |||
48 | iDeclareType(Font) | ||
49 | |||
50 | struct Impl_Font { | ||
51 | iBlock * data; | ||
52 | stbtt_fontinfo font; | ||
53 | float scale; | ||
54 | int height; | ||
55 | int baseline; | ||
56 | iHash glyphs; | ||
57 | }; | ||
58 | |||
59 | static 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 | |||
71 | static 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 | |||
79 | iDeclareType(Text) | ||
80 | |||
81 | struct 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 | |||
91 | static iText text_; | ||
92 | |||
93 | void 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 | |||
134 | void 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 | |||
144 | static 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 | |||
154 | static iBool isSpecialChar_(iChar ch) { | ||
155 | return ch >= specialSymbol_Text && ch < 0x20; | ||
156 | } | ||
157 | |||
158 | static float symbolEmWidth_(int symbol) { | ||
159 | return 1.5f; | ||
160 | } | ||
161 | |||
162 | static float symbolAdvance_(int symbol) { | ||
163 | return 1.5f; | ||
164 | } | ||
165 | |||
166 | static int specialChar_(iChar ch) { | ||
167 | return ch - specialSymbol_Text; | ||
168 | } | ||
169 | |||
170 | iLocalDef SDL_Rect sdlRect_(const iRect rect) { | ||
171 | return (SDL_Rect){ rect.pos.x, rect.pos.y, rect.size.x, rect.size.y }; | ||
172 | } | ||
173 | |||
174 | static 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 | |||
205 | static 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 | |||
352 | static 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 | |||
363 | enum iRunMode { measure_RunMode, draw_RunMode, drawPermanentColor_RunMode }; | ||
364 | |||
365 | static 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 | |||
428 | int lineHeight_Text(int fontId) { | ||
429 | return text_.fonts[fontId].height; | ||
430 | } | ||
431 | |||
432 | iInt2 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 | |||
439 | iInt2 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 | |||
446 | iInt2 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 | |||
455 | static 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 | |||
467 | void 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 | |||
487 | void 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 | |||
500 | SDL_Texture *glyphCache_Text(void) { | ||
501 | return text_.cache; | ||
502 | } | ||
503 | |||
504 | /*-----------------------------------------------------------------------------------------------*/ | ||
505 | |||
506 | iDefineTypeConstructionArgs(TextBuf, (int font, const char *text), font, text) | ||
507 | |||
508 | void 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 | |||
522 | void deinit_TextBuf(iTextBuf *d) { | ||
523 | SDL_DestroyTexture(d->texture); | ||
524 | } | ||
525 | |||
526 | void 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 | |||
8 | enum 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 | |||
25 | enum iSpecialSymbol { | ||
26 | silence_SpecialSymbol, | ||
27 | }; | ||
28 | |||
29 | void init_Text (SDL_Renderer *); | ||
30 | void deinit_Text (void); | ||
31 | |||
32 | int lineHeight_Text (int font); | ||
33 | iInt2 measure_Text (int font, const char *text); | ||
34 | iInt2 advance_Text (int font, const char *text); | ||
35 | iInt2 advanceN_Text (int font, const char *text, size_t n); | ||
36 | |||
37 | void draw_Text (int font, iInt2 pos, int color, const char *text, ...); /* negative pos to switch alignment */ | ||
38 | void drawCentered_Text (int font, iRect rect, int color, const char *text, ...); | ||
39 | |||
40 | SDL_Texture * glyphCache_Text (void); | ||
41 | |||
42 | /*-----------------------------------------------------------------------------------------------*/ | ||
43 | |||
44 | iDeclareType(TextBuf) | ||
45 | iDeclareTypeConstructionArgs(TextBuf, int font, const char *text) | ||
46 | |||
47 | struct Impl_TextBuf { | ||
48 | SDL_Texture *texture; | ||
49 | iInt2 size; | ||
50 | }; | ||
51 | |||
52 | void 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 | |||
15 | iBool 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 | |||
20 | const 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 | |||
27 | int 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 | |||
39 | void 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 | |||
47 | enum 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 | |||
95 | void cancel_Click(iClick *d) { | ||
96 | if (d->isActive) { | ||
97 | d->isActive = iFalse; | ||
98 | setMouseGrab_Widget(NULL); | ||
99 | } | ||
100 | } | ||
101 | |||
102 | iBool isMoved_Click(const iClick *d) { | ||
103 | return dist_I2(d->startPos, d->pos) > 2; | ||
104 | } | ||
105 | |||
106 | iInt2 pos_Click(const iClick *d) { | ||
107 | return d->pos; | ||
108 | } | ||
109 | |||
110 | iRect rect_Click(const iClick *d) { | ||
111 | return initCorners_Rect(min_I2(d->startPos, d->pos), max_I2(d->startPos, d->pos)); | ||
112 | } | ||
113 | |||
114 | iInt2 delta_Click(const iClick *d) { | ||
115 | return sub_I2(d->pos, d->startPos); | ||
116 | } | ||
117 | |||
118 | /*-----------------------------------------------------------------------------------------------*/ | ||
119 | |||
120 | iWidget *makePadding_Widget(int size) { | ||
121 | iWidget *pad = new_Widget(); | ||
122 | setSize_Widget(pad, init1_I2(size)); | ||
123 | return pad; | ||
124 | } | ||
125 | |||
126 | iLabelWidget *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 | |||
132 | iWidget *makeVDiv_Widget(void) { | ||
133 | iWidget *div = new_Widget(); | ||
134 | setFlags_Widget(div, resizeChildren_WidgetFlag | arrangeVertical_WidgetFlag, iTrue); | ||
135 | return div; | ||
136 | } | ||
137 | |||
138 | iWidget *makeHDiv_Widget(void) { | ||
139 | iWidget *div = new_Widget(); | ||
140 | setFlags_Widget(div, resizeChildren_WidgetFlag | arrangeHorizontal_WidgetFlag, iTrue); | ||
141 | return div; | ||
142 | } | ||
143 | |||
144 | iWidget *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 | |||
153 | static 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 | |||
166 | iWidget *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 | |||
195 | void 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 | |||
218 | void closeMenu_Widget(iWidget *d) { | ||
219 | setFlags_Widget(d, hidden_WidgetFlag, iTrue); | ||
220 | } | ||
221 | |||
222 | iLabelWidget *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 | |||
231 | static iBool isTabPage_Widget_(const iWidget *tabs, const iWidget *page) { | ||
232 | return page->parent == findChild_Widget(tabs, "tabs.pages"); | ||
233 | } | ||
234 | |||
235 | static 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 | |||
274 | iWidget *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 | |||
287 | static 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 | |||
300 | void 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 | |||
304 | void 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 | |||
308 | iWidget *tabPage_Widget(iWidget *tabs, size_t index) { | ||
309 | iWidget *pages = findChild_Widget(tabs, "tabs.pages"); | ||
310 | return child_Widget(pages, index); | ||
311 | } | ||
312 | |||
313 | iWidget *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 | |||
325 | void 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 | |||
349 | const 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 | |||
359 | size_t tabCount_Widget(const iWidget *tabs) { | ||
360 | return childCount_Widget(findChild_Widget(tabs, "tabs.buttons")); | ||
361 | } | ||
362 | |||
363 | /*-----------------------------------------------------------------------------------------------*/ | ||
364 | |||
365 | static 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 | |||
373 | iBool 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 | |||
406 | iWidget *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 | |||
420 | void 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 | |||
426 | void 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 | |||
454 | static 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 | |||
463 | iBool 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 | |||
487 | iWidget *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 | |||
514 | static 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 | |||
521 | void 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 | |||
528 | iWidget *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 | |||
553 | void 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 | |||
561 | static 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 | |||
573 | iWidget *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 | |||
580 | iWidget *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 | |||
8 | iDeclareType(Click) | ||
9 | iDeclareType(Widget) | ||
10 | iDeclareType(LabelWidget) | ||
11 | |||
12 | iBool isCommand_UserEvent (const SDL_Event *, const char *cmd); | ||
13 | const char * command_UserEvent (const SDL_Event *); | ||
14 | |||
15 | iLocalDef 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 | |||
27 | int keyMods_Sym (int kmods); /* shift, alt, control, or gui */ | ||
28 | |||
29 | /*-----------------------------------------------------------------------------------------------*/ | ||
30 | |||
31 | enum iClickResult { | ||
32 | none_ClickResult, | ||
33 | started_ClickResult, | ||
34 | drag_ClickResult, | ||
35 | finished_ClickResult, | ||
36 | aborted_ClickResult, | ||
37 | double_ClickResult, | ||
38 | }; | ||
39 | |||
40 | struct Impl_Click { | ||
41 | iBool isActive; | ||
42 | int button; | ||
43 | iWidget *bounds; | ||
44 | iInt2 startPos; | ||
45 | iInt2 pos; | ||
46 | }; | ||
47 | |||
48 | void init_Click (iClick *, iAnyObject *widget, int button); | ||
49 | enum iClickResult processEvent_Click (iClick *, const SDL_Event *event); | ||
50 | void cancel_Click (iClick *); | ||
51 | |||
52 | iBool isMoved_Click (const iClick *); | ||
53 | iInt2 pos_Click (const iClick *); | ||
54 | iRect rect_Click (const iClick *); | ||
55 | iInt2 delta_Click (const iClick *); | ||
56 | |||
57 | /*-----------------------------------------------------------------------------------------------*/ | ||
58 | |||
59 | iWidget * makePadding_Widget (int size); | ||
60 | iLabelWidget * makeHeading_Widget (const char *text); | ||
61 | iWidget * makeHDiv_Widget (void); | ||
62 | iWidget * makeVDiv_Widget (void); | ||
63 | iWidget * addAction_Widget (iWidget *parent, int key, int kmods, const char *command); | ||
64 | |||
65 | /*-----------------------------------------------------------------------------------------------*/ | ||
66 | |||
67 | iWidget * makeToggle_Widget (const char *id); | ||
68 | void setToggle_Widget (iWidget *toggle, iBool active); | ||
69 | |||
70 | /*-----------------------------------------------------------------------------------------------*/ | ||
71 | |||
72 | iDeclareType(MenuItem) | ||
73 | |||
74 | struct Impl_MenuItem { | ||
75 | const char *label; | ||
76 | int key; | ||
77 | int kmods; | ||
78 | const char *command; | ||
79 | }; | ||
80 | |||
81 | iWidget * makeMenu_Widget (iWidget *parent, const iMenuItem *items, size_t n); /* returns no ref */ | ||
82 | void openMenu_Widget (iWidget *, iInt2 coord); | ||
83 | void closeMenu_Widget (iWidget *); | ||
84 | |||
85 | iLabelWidget * makeMenuButton_LabelWidget (const char *label, const iMenuItem *items, size_t n); | ||
86 | |||
87 | /*-----------------------------------------------------------------------------------------------*/ | ||
88 | |||
89 | iWidget * makeTabs_Widget (iWidget *parent); | ||
90 | void appendTabPage_Widget (iWidget *tabs, iWidget *page, const char *label, int key, int kmods); | ||
91 | void prependTabPage_Widget (iWidget *tabs, iWidget *page, const char *label, int key, int kmods); | ||
92 | iWidget * tabPage_Widget (iWidget *tabs, size_t index); | ||
93 | iWidget * removeTabPage_Widget (iWidget *tabs, size_t index); /* returns the page */ | ||
94 | void showTabPage_Widget (iWidget *tabs, const iWidget *page); | ||
95 | const iWidget *currentTabPage_Widget(const iWidget *tabs); | ||
96 | size_t tabCount_Widget (const iWidget *tabs); | ||
97 | |||
98 | /*-----------------------------------------------------------------------------------------------*/ | ||
99 | |||
100 | iWidget * makeSheet_Widget (const char *id); | ||
101 | void centerSheet_Widget (iWidget *sheet); | ||
102 | |||
103 | void makeFilePath_Widget (iWidget *parent, const iString *initialPath, const char *title, | ||
104 | const char *acceptLabel, const char *command); | ||
105 | iWidget * makeValueInput_Widget (iWidget *parent, const iString *initialValue, const char *title, | ||
106 | const char *prompt, const char *command); | ||
107 | void makeMessage_Widget (const char *title, const char *msg); | ||
108 | iWidget * makeQuestion_Widget (const char *title, const char *msg, | ||
109 | const char *labels[], const char *commands[], size_t count); | ||
110 | iWidget * 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 | |||
13 | iDeclareType(RootData) | ||
14 | |||
15 | struct Impl_RootData { | ||
16 | iWidget *hover; | ||
17 | iWidget *mouseGrab; | ||
18 | iWidget *focus; | ||
19 | iPtrSet *onTop; | ||
20 | iPtrSet *pendingDestruction; | ||
21 | }; | ||
22 | |||
23 | static iRootData rootData_; | ||
24 | |||
25 | iPtrSet *onTop_RootData_(void) { | ||
26 | if (!rootData_.onTop) { | ||
27 | rootData_.onTop = new_PtrSet(); | ||
28 | } | ||
29 | return rootData_.onTop; | ||
30 | } | ||
31 | |||
32 | void 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 | |||
41 | iDefineObjectConstruction(Widget) | ||
42 | |||
43 | void 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 | |||
53 | void deinit_Widget(iWidget *d) { | ||
54 | iReleasePtr(&d->children); | ||
55 | deinit_String(&d->id); | ||
56 | } | ||
57 | |||
58 | static 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 | |||
71 | void 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 | |||
79 | void setId_Widget(iWidget *d, const char *id) { | ||
80 | setCStr_String(&d->id, id); | ||
81 | } | ||
82 | |||
83 | const iString *id_Widget(const iWidget *d) { | ||
84 | return &d->id; | ||
85 | } | ||
86 | |||
87 | int flags_Widget(const iWidget *d) { | ||
88 | return d->flags; | ||
89 | } | ||
90 | |||
91 | void 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 | |||
103 | void setPos_Widget(iWidget *d, iInt2 pos) { | ||
104 | d->rect.pos = pos; | ||
105 | } | ||
106 | |||
107 | void setSize_Widget(iWidget *d, iInt2 size) { | ||
108 | d->rect.size = size; | ||
109 | setFlags_Widget(d, fixedSize_WidgetFlag, iTrue); | ||
110 | } | ||
111 | |||
112 | void setBackgroundColor_Widget(iWidget *d, int bgColor) { | ||
113 | d->bgColor = bgColor; | ||
114 | } | ||
115 | |||
116 | void setCommandHandler_Widget(iWidget *d, iBool (*handler)(iWidget *, const char *)) { | ||
117 | d->commandHandler = handler; | ||
118 | } | ||
119 | |||
120 | static 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 | |||
131 | static 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 | |||
140 | static void setWidth_Widget_(iWidget *d, int width) { | ||
141 | if (~d->flags & fixedWidth_WidgetFlag) { | ||
142 | d->rect.size.x = width; | ||
143 | } | ||
144 | } | ||
145 | |||
146 | static void setHeight_Widget_(iWidget *d, int height) { | ||
147 | if (~d->flags & fixedHeight_WidgetFlag) { | ||
148 | d->rect.size.y = height; | ||
149 | } | ||
150 | } | ||
151 | |||
152 | void 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 | |||
271 | iRect 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 | |||
279 | iInt2 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 | |||
286 | iBool 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 | |||
291 | iLocalDef iBool isKeyboardEvent_(const SDL_Event *ev) { | ||
292 | return (ev->type == SDL_KEYUP || ev->type == SDL_KEYDOWN || ev->type == SDL_TEXTINPUT); | ||
293 | } | ||
294 | |||
295 | iLocalDef 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 | |||
300 | static 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 | |||
312 | void unhover_Widget(void) { | ||
313 | rootData_.hover = NULL; | ||
314 | } | ||
315 | |||
316 | iBool 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 | |||
366 | iBool 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 | |||
388 | void 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 | |||
409 | iAny *addChild_Widget(iWidget *d, iAnyObject *child) { | ||
410 | return addChildPos_Widget(d, child, back_WidgetAddPos); | ||
411 | } | ||
412 | |||
413 | iAny *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 | |||
431 | iAny *addChildFlags_Widget(iWidget *d, iAnyObject *child, int childFlags) { | ||
432 | setFlags_Widget(child, childFlags, iTrue); | ||
433 | return addChild_Widget(d, child); | ||
434 | } | ||
435 | |||
436 | iAny *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 | |||
451 | iAny *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 | |||
460 | iAny *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 | |||
471 | size_t childCount_Widget(const iWidget *d) { | ||
472 | if (!d->children) return 0; | ||
473 | return size_ObjectList(d->children); | ||
474 | } | ||
475 | |||
476 | iBool 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 | |||
485 | iBool 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 | |||
494 | iBool isFocused_Widget(const iWidget *d) { | ||
495 | return rootData_.focus == d; | ||
496 | } | ||
497 | |||
498 | iBool isHover_Widget(const iWidget *d) { | ||
499 | return rootData_.hover == d; | ||
500 | } | ||
501 | |||
502 | iBool isSelected_Widget(const iWidget *d) { | ||
503 | return (d->flags & selected_WidgetFlag) != 0; | ||
504 | } | ||
505 | |||
506 | iBool 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 | |||
515 | iBool 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 | |||
524 | void 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 | |||
538 | iWidget *focus_Widget(void) { | ||
539 | return rootData_.focus; | ||
540 | } | ||
541 | |||
542 | iWidget *hover_Widget(void) { | ||
543 | return rootData_.hover; | ||
544 | } | ||
545 | |||
546 | static 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 | |||
573 | iAny *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 | |||
584 | void setMouseGrab_Widget(iWidget *d) { | ||
585 | if (rootData_.mouseGrab != d) { | ||
586 | rootData_.mouseGrab = d; | ||
587 | SDL_CaptureMouse(d != NULL); | ||
588 | } | ||
589 | } | ||
590 | |||
591 | iWidget *mouseGrab_Widget(void) { | ||
592 | return rootData_.mouseGrab; | ||
593 | } | ||
594 | |||
595 | void 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 | |||
615 | iBeginDefineClass(Widget) | ||
616 | .processEvent = processEvent_Widget, | ||
617 | .draw = draw_Widget, | ||
618 | iEndDefineClass(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 | |||
18 | iDeclareType(Widget) | ||
19 | iBeginDeclareClass(Widget) | ||
20 | iBool (*processEvent) (iWidget *, const SDL_Event *); | ||
21 | void (*draw) (const iWidget *); | ||
22 | iEndDeclareClass(Widget) | ||
23 | |||
24 | enum 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 | |||
51 | enum iWidgetAddPos { | ||
52 | back_WidgetAddPos, | ||
53 | front_WidgetAddPos, | ||
54 | }; | ||
55 | |||
56 | enum iWidgetFocusDir { | ||
57 | forward_WidgetFocusDir, | ||
58 | backward_WidgetFocusDir, | ||
59 | }; | ||
60 | |||
61 | struct 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 | |||
72 | iDeclareObjectConstruction(Widget) | ||
73 | |||
74 | iLocalDef 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 | |||
82 | iLocalDef 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 | |||
90 | void destroy_Widget (iWidget *); /* widget removed and deleted later */ | ||
91 | void destroyPending_Widget(void); | ||
92 | |||
93 | const iString *id_Widget (const iWidget *); | ||
94 | int flags_Widget (const iWidget *); | ||
95 | iRect bounds_Widget (const iWidget *); | ||
96 | iInt2 localCoord_Widget (const iWidget *, iInt2 coord); | ||
97 | iBool contains_Widget (const iWidget *, iInt2 coord); | ||
98 | iAny * findChild_Widget (const iWidget *, const char *id); | ||
99 | iAny * findFocusable_Widget(const iWidget *startFrom, enum iWidgetFocusDir focusDir); | ||
100 | size_t childCount_Widget (const iWidget *); | ||
101 | void draw_Widget (const iWidget *); | ||
102 | |||
103 | iBool isVisible_Widget (const iWidget *); | ||
104 | iBool isDisabled_Widget (const iWidget *); | ||
105 | iBool isFocused_Widget (const iWidget *); | ||
106 | iBool isHover_Widget (const iWidget *); | ||
107 | iBool isSelected_Widget (const iWidget *); | ||
108 | iBool isCommand_Widget (const iWidget *d, const SDL_Event *ev, const char *cmd); | ||
109 | iBool hasParent_Widget (const iWidget *d, const iWidget *someParent); | ||
110 | void setId_Widget (iWidget *, const char *id); | ||
111 | void setFlags_Widget (iWidget *, int flags, iBool set); | ||
112 | void setPos_Widget (iWidget *, iInt2 pos); | ||
113 | void setSize_Widget (iWidget *, iInt2 size); | ||
114 | void setBackgroundColor_Widget (iWidget *, int bgColor); | ||
115 | void setCommandHandler_Widget (iWidget *, iBool (*handler)(iWidget *, const char *)); | ||
116 | iAny * addChild_Widget (iWidget *, iAnyObject *child); /* holds a ref */ | ||
117 | iAny * addChildPos_Widget (iWidget *, iAnyObject *child, enum iWidgetAddPos addPos); | ||
118 | iAny * addChildFlags_Widget(iWidget *, iAnyObject *child, int childFlags); /* holds a ref */ | ||
119 | iAny * removeChild_Widget (iWidget *, iAnyObject *child); /* returns a ref */ | ||
120 | iAny * child_Widget (iWidget *, size_t index); /* O(n) */ | ||
121 | void arrange_Widget (iWidget *); | ||
122 | iBool dispatchEvent_Widget(iWidget *, const SDL_Event *); | ||
123 | iBool processEvent_Widget (iWidget *, const SDL_Event *); | ||
124 | void postCommand_Widget (const iWidget *, const char *cmd, ...); | ||
125 | |||
126 | void setFocus_Widget (iWidget *); | ||
127 | iWidget *focus_Widget (void); | ||
128 | iWidget *hover_Widget (void); | ||
129 | void unhover_Widget (void); | ||
130 | void setMouseGrab_Widget (iWidget *); | ||
131 | iWidget *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 | |||
27 | static iWindow *theWindow_ = NULL; | ||
28 | |||
29 | #if defined (iPlatformApple) | ||
30 | static float initialUiScale_ = 1.0f; | ||
31 | #else | ||
32 | static float initialUiScale_ = 1.1f; | ||
33 | #endif | ||
34 | |||
35 | iDefineTypeConstruction(Window) | ||
36 | |||
37 | static 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 | |||
57 | static const iMenuItem fileMenuItems[] = { | ||
58 | #if !defined (iPlatformApple) | ||
59 | { "Quit Lagrange", 'q', KMOD_PRIMARY, "quit" } | ||
60 | #endif | ||
61 | }; | ||
62 | |||
63 | static const iMenuItem editMenuItems[] = { | ||
64 | #if !defined (iPlatformApple) | ||
65 | { "Preferences...", SDLK_COMMA, KMOD_PRIMARY, "preferences" } | ||
66 | #endif | ||
67 | }; | ||
68 | |||
69 | static const iMenuItem viewMenuItems[] = { | ||
70 | }; | ||
71 | |||
72 | static 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 | |||
230 | static 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 | |||
237 | static 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 | |||
244 | void 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 | |||
299 | void 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 | |||
309 | SDL_Renderer *renderer_Window(const iWindow *d) { | ||
310 | return d->render; | ||
311 | } | ||
312 | |||
313 | static 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 | |||
327 | iBool 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 | |||
358 | static 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 | |||
375 | void 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 | |||
395 | void resize_Window(iWindow *d, int w, int h) { | ||
396 | SDL_SetWindowSize(d->win, w, h); | ||
397 | updateRootSize_Window_(d); | ||
398 | } | ||
399 | |||
400 | void 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 | |||
417 | iInt2 rootSize_Window(const iWindow *d) { | ||
418 | return d->root->rect.size; | ||
419 | } | ||
420 | |||
421 | iInt2 coord_Window(const iWindow *d, int x, int y) { | ||
422 | return mulf_I2(init_I2(x, y), d->pixelRatio); | ||
423 | } | ||
424 | |||
425 | iInt2 mouseCoord_Window(const iWindow *d) { | ||
426 | int x, y; | ||
427 | SDL_GetMouseState(&x, &y); | ||
428 | return coord_Window(d, x, y); | ||
429 | } | ||
430 | |||
431 | float uiScale_Window(const iWindow *d) { | ||
432 | return d->uiScale; | ||
433 | } | ||
434 | |||
435 | uint32_t frameTime_Window(const iWindow *d) { | ||
436 | return d->frameTime; | ||
437 | } | ||
438 | |||
439 | iWindow *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 | |||
10 | iDeclareType(Window) | ||
11 | iDeclareTypeConstruction(Window) | ||
12 | |||
13 | struct 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 | |||
23 | iBool processEvent_Window (iWindow *, const SDL_Event *); | ||
24 | void draw_Window (iWindow *); | ||
25 | void resize_Window (iWindow *, int w, int h); | ||
26 | void setUiScale_Window (iWindow *, float uiScale); | ||
27 | |||
28 | iInt2 rootSize_Window (const iWindow *); | ||
29 | float uiScale_Window (const iWindow *); | ||
30 | iInt2 coord_Window (const iWindow *, int x, int y); | ||
31 | iInt2 mouseCoord_Window (const iWindow *); | ||
32 | uint32_t frameTime_Window (const iWindow *); | ||
33 | |||
34 | iWindow * get_Window (void); | ||