ObjectivelyMVC
Object oriented MVC framework for SDL3 and GNU C
Loading...
Searching...
No Matches
TextView.c
Go to the documentation of this file.
1/*
2 * ObjectivelyMVC: Object oriented MVC framework for SDL3 and C.
3 * Copyright (C) 2014 Jay Dolan <jay@jaydolan.com>
4 *
5 * This software is provided 'as-is', without any express or implied
6 * warranty. In no event will the authors be held liable for any damages
7 * arising from the use of this software.
8 *
9 * Permission is granted to anyone to use this software for any purpose,
10 * including commercial applications, and to alter it and redistribute it
11 * freely, subject to the following restrictions:
12 *
13 * 1. The origin of this software must not be misrepresented; you must not
14 * claim that you wrote the original software. If you use this software
15 * in a product, an acknowledgment in the product documentation would be
16 * appreciated but is not required.
17 *
18 * 2. Altered source versions must be plainly marked as such, and must not be
19 * misrepresented as being the original software.
20 *
21 * 3. This notice may not be removed or altered from any source distribution.
22 */
23
24#include <assert.h>
25#include <stdlib.h>
26#include <string.h>
27
28#include <Objectively/String.h>
29
30#include "Colors.h"
31#include "Text.h"
32#include "TextView.h"
33#include "Theme.h"
34
35#define _Class _TextView
36
37#pragma mark - Object
38
42static void dealloc(Object *self) {
43
44 TextView *this = (TextView *) self;
45
46 memset(&this->delegate, 0, sizeof(this->delegate));
47
48 free(this->defaultText);
49
50 release(this->text);
51
52 release(this->attributedText);
53
54 release(this->completions);
55
56 super(Object, self, dealloc);
57}
58
59#pragma mark - View
60
65static void applyStyle(View *self, const Style *style) {
66
67 super(View, self, applyStyle, style);
68
69 TextView *this = (TextView *) self;
70
71 const Inlet inlets[] = MakeInlets(
72 MakeInlet("editable", InletTypeBool, &this->isEditable, NULL)
73 );
74
75 $(self, bind, inlets, style->attributes);
76}
77
81static void awakeWithDictionary(View *self, const Dictionary *dictionary) {
82
83 super(View, self, awakeWithDictionary, dictionary);
84
85 TextView *this = (TextView *) self;
86
87 const Inlet inlets[] = MakeInlets(
88 MakeInlet("defaultText", InletTypeCharacters, &this->defaultText, NULL)
89 );
90
91 $(self, bind, inlets, dictionary);
92}
93
97static void becomeKeyResponder(View *self) {
98
99 super(View, self, becomeKeyResponder);
100
101 SDL_StartTextInput(self->window);
102}
103
107static View *init(View *self) {
108 return (View *) $((TextView *) self, initWithFrame, NULL);
109}
110
114static void layoutSubviews(View *self) {
115
116 TextView *this = (TextView *) self;
117
118 const char *text = this->attributedText->chars;
119
120 if (text == NULL || strlen(text) == 0) {
121 if ($((Control *) this, isFocused) == false) {
122 text = this->defaultText;
123 }
124 }
125
126 if (text == NULL) {
127 $(this->text, setText, NULL);
128 } else {
129 if (this->text->text) {
130 if (strcmp(text, this->text->text)) {
131 $(this->text, setText, text);
132 }
133 } else {
134 $(this->text, setText, text);
135 }
136 }
137
138 // An empty Text has no height, so alignment would place it, and the cursor drawn from its
139 // frame, a line too low; it is never shorter than a line here
140 if (this->text->font) {
141 this->text->view.minSize.h = $(this->text, sizeText, " ").h;
142 }
143
144 $((View *) this->text, layoutIfNeeded);
145
146 super(View, self, layoutSubviews);
147}
148
152static void render(View *self, Renderer *renderer) {
153
154 super(View, self, render, renderer);
155
156 TextView *this = (TextView *) self;
157
158 if ($((Control *) this, isFocused)) {
159 const char *text = this->text->text ?: "";
160
161 // Measured as the Text renders it, escapes and icons included
162 char *prefix = calloc(this->position + 1, sizeof(char));
163 strncpy(prefix, text, this->position);
164
165 SDL_Size size = $(this->text, sizeText, prefix);
166 free(prefix);
167
168 // An empty prefix has no height; borrow the line's
169 if (size.h == 0) {
170 size.h = $(this->text, sizeText, " ").h;
171 }
172
173 const int w = size.w, h = size.h;
174
175 SDL_Rect frame = $((View *) this->text, renderFrame);
176
177 const SDL_Point points[] = {
178 { frame.x + w, frame.y },
179 { frame.x + w, frame.y + h }
180 };
181
182 $(renderer, drawLine, points, &Colors.White);
183 }
184}
185
189static void resignKeyResponder(View *self) {
190
191 const bool wasKeyResponder = $(self, isKeyResponder);
192
193 super(View, self, resignKeyResponder);
194
195 if (wasKeyResponder) {
196 SDL_StopTextInput(self->window);
197 }
198}
199
205static size_t unitLengthAt(const char *chars, size_t len, size_t position, const ImageAtlas *icons) {
206
207 if (position >= len) {
208 return 0;
209 }
210
211 if (chars[position] == '^' && position + 1 < len &&
212 ((chars[position + 1] >= '0' && chars[position + 1] <= '9') || chars[position + 1] == '^')) {
213 return 2;
214 }
215
216 if (chars[position] == ':') {
217 const size_t n = MVC_IconEscapeLength(chars + position, icons, NULL);
218 if (n && position + n <= len) {
219 return n;
220 }
221 }
222
223 size_t n = 1;
224 while (position + n < len && (chars[position + n] & 0xC0) == 0x80) {
225 n++;
226 }
227
228 return n;
229}
230
237static size_t unitLengthBefore(const char *chars, size_t len, size_t position, const ImageAtlas *icons) {
238
239 size_t unit = 0;
240
241 for (size_t p = 0; p < position; p += unit) {
242 unit = unitLengthAt(chars, len, p, icons);
243 if (unit == 0) {
244 break;
245 }
246 }
247
248 return unit;
249}
250
254static void replacePrefix(TextView *self, const char *chars) {
255
256 const Range range = { .location = 0, .length = self->position };
257 $(self->attributedText, replaceCharactersInRange, range, chars);
258
259 self->position = strlen(chars);
260}
261
270static bool autocomplete(TextView *self, int step, const ImageAtlas *icons) {
271
272 if (self->completions) {
273 const size_t count = self->completions->count;
274
275 if (step > 0) {
276 self->completion = self->completion == count ? 0 : (self->completion + 1) % count;
277 } else {
278 self->completion = self->completion == 0 || self->completion == count ? count - 1 : self->completion - 1;
279 }
280
281 const String *completion = $(self->completions, objectAtIndex, self->completion);
282 replacePrefix(self, completion->chars ?: "");
283 return true;
284 }
285
286 if (self->delegate.completionsForPrefix == NULL) {
287 return false;
288 }
289
290 String *prefix = $(self->attributedText, substring, (Range) { .length = self->position });
291 Array *completions = self->delegate.completionsForPrefix(self, prefix->chars ?: "");
292
293 bool didComplete = false;
294
295 if (completions && completions->count) {
296 const size_t count = completions->count;
297 const String *first = $(completions, objectAtIndex, 0);
298
299 size_t len = first->length;
300 for (size_t i = 1; i < count; i++) {
301 const String *other = $(completions, objectAtIndex, i);
302 size_t j = 0;
303 while (j < len && j < other->length && first->chars[j] == other->chars[j]) {
304 j++;
305 }
306 len = j;
307 }
308
309 // Never stop inside an escape or a UTF-8 encoded character
310 size_t shared = 0;
311 while (shared < len) {
312 const size_t unit = unitLengthAt(first->chars, first->length, shared, icons);
313 if (shared + unit > len) {
314 break;
315 }
316 shared += unit;
317 }
318
319 if (shared > prefix->length) {
320 String *common = $(first, substring, (Range) { .length = shared });
321 replacePrefix(self, common->chars);
322 release(common);
323
324 self->completion = count;
325 didComplete = true;
326 } else if (count > 1) {
327 self->completion = step > 0 ? 0 : count - 1;
328
329 const String *completion = $(completions, objectAtIndex, self->completion);
330 replacePrefix(self, completion->chars ?: "");
331 didComplete = true;
332 } else if (strcmp(first->chars ?: "", prefix->chars ?: "")) {
333 replacePrefix(self, first->chars ?: "");
334 didComplete = true;
335 }
336
337 if (count > 1) {
338 self->completions = retain(completions);
339 }
340 }
341
342 release(completions);
343 release(prefix);
344
345 return didComplete;
346}
347
348#pragma mark - Control
349
353static bool captureEvent(Control *self, const SDL_Event *event) {
354
355 bool didEdit = false, didComplete = false, didCaptureEvent = false;
356
357 View *view = (View *) self;
358
359 TextView *this = (TextView *) self;
360
361 const size_t position = this->position;
362
363 if (this->isEditable) {
364 if (event->type == SDL_EVENT_TEXT_INPUT) {
365 if ($(self, isFocused)) {
366 if (this->position == this->attributedText->length) {
367 $(this->attributedText, appendCharacters, event->text.text);
368 } else {
369 $(this->attributedText, insertCharactersAtIndex, event->text.text, this->position);
370 }
371 this->position += strlen(event->text.text);
372 didEdit = true;
373 didCaptureEvent = true;
374 }
375 } else if (event->type == SDL_EVENT_KEY_DOWN) {
376 didCaptureEvent = true;
377
378 const char *chars = this->attributedText->chars;
379 const size_t len = this->attributedText->length;
380
381 Theme *theme = view->window ? $$(Theme, theme, view->window) : NULL;
382 const ImageAtlas *icons = theme ? $(theme, icons) : NULL;
383
384 switch (event->key.key) {
385
386 case SDLK_TAB:
387 case SDLK_KP_TAB:
388 if (autocomplete(this, (event->key.mod & SDL_KMOD_SHIFT) ? -1 : 1, icons)) {
389 didEdit = didComplete = true;
390 } else {
391 $(view, resignKeyResponder);
392 }
393 break;
394
395 case SDLK_ESCAPE:
396 case SDLK_KP_ENTER:
397 case SDLK_RETURN:
398 $(view, resignKeyResponder);
399 break;
400
401 case SDLK_BACKSPACE:
402 case SDLK_KP_BACKSPACE:
403 if (this->position > 0) {
404 const size_t n = unitLengthBefore(chars, len, this->position, icons);
405 const Range range = { .location = this->position - n, .length = n };
406 $(this->attributedText, deleteCharactersInRange, range);
407 this->position -= n;
408 didEdit = true;
409 }
410 break;
411
412 case SDLK_DELETE:
413 if (this->position < len) {
414 const Range range = { .location = this->position, .length = unitLengthAt(chars, len, this->position, icons) };
415 $(this->attributedText, deleteCharactersInRange, range);
416 didEdit = true;
417 }
418 break;
419
420 case SDLK_LEFT:
421 if (SDL_GetModState() & SDL_KMOD_CTRL) {
422 while (this->position > 0 && chars[this->position] == ' ') {
423 this->position -= unitLengthBefore(chars, len, this->position, icons);
424 }
425 while (this->position > 0 && chars[this->position] != ' ') {
426 this->position -= unitLengthBefore(chars, len, this->position, icons);
427 }
428 } else {
429 this->position -= unitLengthBefore(chars, len, this->position, icons);
430 }
431 break;
432
433 case SDLK_RIGHT:
434 if (SDL_GetModState() & SDL_KMOD_CTRL) {
435 while (this->position < len && chars[this->position] == ' ') {
436 this->position += unitLengthAt(chars, len, this->position, icons);
437 }
438 while (this->position < len && chars[this->position] != ' ') {
439 this->position += unitLengthAt(chars, len, this->position, icons);
440 }
441 if (this->position < len) {
442 this->position += unitLengthAt(chars, len, this->position, icons);
443 }
444 } else {
445 this->position += unitLengthAt(chars, len, this->position, icons);
446 }
447 break;
448
449 case SDLK_HOME:
450 this->position = 0;
451 break;
452
453 case SDLK_END:
454 this->position = len;
455 break;
456
457 case SDLK_A:
458 if (SDL_GetModState() & SDL_KMOD_CTRL) {
459 this->position = 0;
460 }
461 break;
462 case SDLK_E:
463 if (SDL_GetModState() & SDL_KMOD_CTRL) {
464 this->position = len;
465 }
466 break;
467
468 case SDLK_V:
469 if ((SDL_GetModState() & (SDL_KMOD_CTRL | SDL_KMOD_GUI)) && SDL_HasClipboardText()) {
470 char *text = SDL_GetClipboardText();
471 if (text != NULL) {
472 if (*text != '\0') {
473 if (this->position == len) {
474 $(this->attributedText, appendCharacters, text);
475 } else {
476 $(this->attributedText, insertCharactersAtIndex, text, this->position);
477 }
478 this->position += strlen(text);
479 didEdit = true;
480 }
481 SDL_free(text);
482 }
483 }
484 break;
485 }
486 }
487
488 if (!didComplete && (didEdit || this->position != position)) {
489 this->completions = release(this->completions);
490 }
491
492 if (didEdit) {
493 $((View *) self, setNeedsLayout);
494 if (this->delegate.didEdit) {
495 this->delegate.didEdit(this);
496 }
497
498 $((View *) self, emitViewEvent, ViewEventChange, NULL);
499 }
500 }
501
502 if (didCaptureEvent) {
503 return true;
504 }
505
506 return super(Control, self, captureEvent, event);
507}
508
512static void stateDidChange(Control *self) {
513
514 TextView *this = (TextView *) self;
515
516 if ($(self, isFocused)) {
517
518 SDL_StartTextInput(self->view.window);
519
520 if (this->delegate.didBeginEditing) {
521 this->delegate.didBeginEditing(this);
522 }
523 } else {
524
525 SDL_StopTextInput(self->view.window);
526
527 this->completions = release(this->completions);
528
529 if (this->delegate.didEndEditing) {
530 this->delegate.didEndEditing(this);
531 }
532 }
533
534 super(Control, self, stateDidChange);
535}
536
537#pragma mark - TextView
538
543static TextView *initWithFrame(TextView *self, const SDL_Rect *frame) {
544
545 self = (TextView *) super(Control, self, initWithFrame, frame);
546 if (self) {
547 self->attributedText = $$(String, string);
548 assert(self->attributedText);
549
550 self->isEditable = true;
551
552 self->text = $(alloc(Text), initWithText, NULL, NULL);
553 assert(self->text);
554
555 $((View *) self, addSubview, (View *) self->text);
556
557 }
558
559 return self;
560}
561
566static void setAttributedText(TextView *self, const char *attributedText) {
567
568 if (strcmp(self->attributedText->chars ?: "", attributedText ?: "")) {
569
570 $(self->attributedText, setCharacters, attributedText);
571
572 self->position = self->attributedText->length;
573
574 self->completions = release(self->completions);
575
576 $((View *) self, setNeedsLayout);
577 }
578}
579
584static void setDefaultText(TextView *self, const char *defaultText) {
585
586 if (strcmp(self->defaultText ?: "", defaultText ?: "")) {
587
588 free(self->defaultText);
589
590 if (defaultText) {
591 self->defaultText = strdup(defaultText);
592 } else {
593 self->defaultText = NULL;
594 }
595
596 $((View *) self, setNeedsLayout);
597 }
598}
599
600#pragma mark - Class lifecycle
601
605static void initialize(Class *clazz) {
606
607 ((ObjectInterface *) clazz->interface)->dealloc = dealloc;
608
609 ((ViewInterface *) clazz->interface)->applyStyle = applyStyle;
610 ((ViewInterface *) clazz->interface)->becomeKeyResponder = becomeKeyResponder;
611 ((ViewInterface *) clazz->interface)->awakeWithDictionary = awakeWithDictionary;
612 ((ViewInterface *) clazz->interface)->init = init;
613 ((ViewInterface *) clazz->interface)->layoutSubviews = layoutSubviews;
614 ((ViewInterface *) clazz->interface)->render = render;
615 ((ViewInterface *) clazz->interface)->resignKeyResponder = resignKeyResponder;
616
617 ((ControlInterface *) clazz->interface)->captureEvent = captureEvent;
618 ((ControlInterface *) clazz->interface)->stateDidChange = stateDidChange;
619
620 ((TextViewInterface *) clazz->interface)->initWithFrame = initWithFrame;
621 ((TextViewInterface *) clazz->interface)->setAttributedText = setAttributedText;
622 ((TextViewInterface *) clazz->interface)->setDefaultText = setDefaultText;
623}
624
629Class *_TextView(void) {
630 static Class *clazz;
631 static Once once;
632
633 do_once(&once, {
634 clazz = _initialize(&(const ClassDef) {
635 .name = "TextView",
636 .superclass = _Control(),
637 .instanceSize = sizeof(TextView),
638 .interfaceSize = sizeof(TextViewInterface),
639 .initialize = initialize,
640 });
641 });
642
643 return clazz;
644}
645
646#undef _Class
W3C Color constants.
Class * _Control(void)
Definition Control.c:414
static bool isFocused(const Control *self)
Definition Control.c:342
static SDL_Size size(const Image *self)
Definition Image.c:416
static Label * initWithText(Label *self, const char *text, Font *font)
Definition Label.c:97
static void addSubview(View *self, View *subview)
Definition PageView.c:49
static void drawLine(const Renderer *self, const SDL_Point *points, const SDL_Color *color)
Definition Renderer.c:266
static SDL_Size sizeText(const Text *self, const char *text)
Definition Text.c:853
static void setText(Text *self, const char *text)
Definition Text.c:899
size_t MVC_IconEscapeLength(const char *chars, const ImageAtlas *icons, AtlasImage **image)
Parses an icon escape at the start of chars: :name:, where name is one or more of [A-Za-z0-9_-] and u...
Definition Text.c:104
Text rendered with TrueType fonts.
static void setAttributedText(TextView *self, const char *attributedText)
Definition TextView.c:566
static bool autocomplete(TextView *self, int step, const ImageAtlas *icons)
Auto-completes the text before the cursor.
Definition TextView.c:270
static void becomeKeyResponder(View *self)
Definition TextView.c:97
static size_t unitLengthBefore(const char *chars, size_t len, size_t position, const ImageAtlas *icons)
Returns the length in bytes of the cursor unit ending at position.
Definition TextView.c:237
static void stateDidChange(Control *self)
Definition TextView.c:512
static View * init(View *self)
Definition TextView.c:107
static void awakeWithDictionary(View *self, const Dictionary *dictionary)
Definition TextView.c:81
static TextView * initWithFrame(TextView *self, const SDL_Rect *frame)
Definition TextView.c:543
static void applyStyle(View *self, const Style *style)
Definition TextView.c:65
static bool captureEvent(Control *self, const SDL_Event *event)
Definition TextView.c:353
static void setDefaultText(TextView *self, const char *defaultText)
Definition TextView.c:584
static size_t unitLengthAt(const char *chars, size_t len, size_t position, const ImageAtlas *icons)
Returns the length in bytes of the cursor unit starting at position.
Definition TextView.c:205
static void replacePrefix(TextView *self, const char *chars)
Replaces the text before the cursor with chars, leaving the cursor after them.
Definition TextView.c:254
static void render(View *self, Renderer *renderer)
Definition TextView.c:152
static void dealloc(Object *self)
Definition TextView.c:42
Class * _TextView(void)
Definition TextView.c:629
static void resignKeyResponder(View *self)
Definition TextView.c:189
static void initialize(Class *clazz)
Definition TextView.c:605
static void layoutSubviews(View *self)
Definition TextView.c:114
A Control for presenting and capturing user-provied Text.
static ImageAtlas * icons(Theme *self)
Definition Theme.c:144
static Theme * theme(SDL_Window *window)
Definition Theme.c:295
The Theme type.
@ ViewEventChange
A Control's input value has changed.
Definition Types.h:110
@ InletTypeCharacters
Definition View+JSON.h:52
@ InletTypeBool
Definition View+JSON.h:46
#define MakeInlets(...)
Creates a null-termianted array of Inlets.
Definition View+JSON.h:215
#define MakeInlet(name, type, dest, data)
Creates an Inlet with the specified parameters.
Definition View+JSON.h:210
static void layoutIfNeeded(View *self)
Definition View.c:1174
static void setNeedsLayout(View *self)
Definition View.c:1865
static SDL_Rect renderFrame(const View *self)
Definition View.c:1629
static bool isKeyResponder(const View *self)
Definition View.c:1126
static void emitViewEvent(View *self, ViewEvent code, ident data)
Definition View.c:794
W3C Color constants.
Definition Colors.h:37
SDL_Color White
Definition Colors.h:185
Controls are Views which capture and respond to events.
Definition Control.h:83
View view
The superclass.
Definition Control.h:88
Packs many Images into one sheet, backed by one Texture.
Definition ImageAtlas.h:49
Inlets enable inbound data binding of View attributes through JSON.
Definition View+JSON.h:149
Renderer extends Object with ObjectivelyMVC's UI rendering layer.
Definition Renderer.h:75
The Style type.
Definition Style.h:43
Dictionary * attributes
Definition Style.h:59
Text rendered with TrueType fonts.
Definition Text.h:131
Array *(* completionsForPrefix)(TextView *textView, const char *prefix)
Delegate callback for auto-completing the text before the cursor with Tab.
Definition TextView.h:85
A Control for presenting and capturing user-provied Text.
Definition TextView.h:93
bool isEditable
True if this TextView supports editing, false otherwise.
Definition TextView.h:125
size_t position
Definition TextView.h:130
char * defaultText
The default text, displayed when no user-provided text is available.
Definition TextView.h:114
String * attributedText
The user-provided text.
Definition TextView.h:109
Text * text
The text.
Definition TextView.h:135
TextViewDelegate delegate
The delegate.
Definition TextView.h:119
void setAttributedText(TextView *self, const char *attributedText)
Definition TextView.c:566
The Theme type.
Definition Theme.h:54
Views are the fundamental building blocks of ObjectivelyMVC user interfaces.
Definition View.h:225
SDL_Window * window
The window.
Definition View.h:480
layoutSubviews(View *self)
Arranges this View's immediate subviews within its (already final) frame.
Definition Box.c:74