ObjectivelyMVC
Object oriented MVC framework for SDL3 and GNU C
Loading...
Searching...
No Matches
TextView.c File Reference
#include <assert.h>
#include <stdlib.h>
#include <string.h>
#include <Objectively/String.h>
#include "Colors.h"
#include "Text.h"
#include "TextView.h"
#include "Theme.h"

Go to the source code of this file.

Macros

#define _Class   _TextView
 

Functions

Class * _TextView (void)
 
static void applyStyle (View *self, const Style *style)
 
static bool autocomplete (TextView *self, int step, const ImageAtlas *icons)
 Auto-completes the text before the cursor.
 
static void awakeWithDictionary (View *self, const Dictionary *dictionary)
 
static void becomeKeyResponder (View *self)
 
static bool captureEvent (Control *self, const SDL_Event *event)
 
static void dealloc (Object *self)
 
static View * init (View *self)
 
static void initialize (Class *clazz)
 
static TextView * initWithFrame (TextView *self, const SDL_Rect *frame)
 
static void layoutSubviews (View *self)
 
static void render (View *self, Renderer *renderer)
 
static void replacePrefix (TextView *self, const char *chars)
 Replaces the text before the cursor with chars, leaving the cursor after them.
 
static void resignKeyResponder (View *self)
 
static void setAttributedText (TextView *self, const char *attributedText)
 
static void setDefaultText (TextView *self, const char *defaultText)
 
static void stateDidChange (Control *self)
 
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.
 
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.
 

Macro Definition Documentation

◆ _Class

#define _Class   _TextView

Definition at line 35 of file TextView.c.

Function Documentation

◆ _TextView()

Class * _TextView ( void  )

Definition at line 629 of file TextView.c.

629 {
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}
Class * _Control(void)
Definition Control.c:414
static void initialize(Class *clazz)
Definition TextView.c:605
A Control for presenting and capturing user-provied Text.
Definition TextView.h:93

◆ applyStyle()

static void applyStyle ( View *  self,
const Style *  style 
)
static

Definition at line 65 of file TextView.c.

65 {
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}
static void applyStyle(View *self, const Style *style)
Definition TextView.c:65
@ 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
Inlets enable inbound data binding of View attributes through JSON.
Definition View+JSON.h:149
Dictionary * attributes
Definition Style.h:59
Views are the fundamental building blocks of ObjectivelyMVC user interfaces.
Definition View.h:225

◆ autocomplete()

static bool autocomplete ( TextView *  self,
int  step,
const ImageAtlas *  icons 
)
static

Auto-completes the text before the cursor.

The first Tab completes to the longest prefix the delegate's completions share, and then Tab and Shift+Tab cycle through them. A lone completion is not cycled, so that the next Tab asks again, e.g. for the contents of a completed directory.

Parameters
step1 to cycle forward, -1 to cycle backward.
Returns
True if the text was completed, false if Tab should advance the key responder.

Definition at line 270 of file TextView.c.

270 {
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}
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 ImageAtlas * icons(Theme *self)
Definition Theme.c:144
Array *(* completionsForPrefix)(TextView *textView, const char *prefix)
Delegate callback for auto-completing the text before the cursor with Tab.
Definition TextView.h:85
String * attributedText
The user-provided text.
Definition TextView.h:109
TextViewDelegate delegate
The delegate.
Definition TextView.h:119

◆ awakeWithDictionary()

static void awakeWithDictionary ( View *  self,
const Dictionary *  dictionary 
)
static
See also
View::awakeWithDictionary(View *, const Dictionary *)

Definition at line 81 of file TextView.c.

81 {
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}
static void awakeWithDictionary(View *self, const Dictionary *dictionary)
Definition TextView.c:81
@ InletTypeCharacters
Definition View+JSON.h:52

◆ becomeKeyResponder()

static void becomeKeyResponder ( View *  self)
static
See also
View::becomeKeyResponder

Definition at line 97 of file TextView.c.

97 {
98
99 super(View, self, becomeKeyResponder);
100
101 SDL_StartTextInput(self->window);
102}
static void becomeKeyResponder(View *self)
Definition TextView.c:97
SDL_Window * window
The window.
Definition View.h:480

◆ captureEvent()

static bool captureEvent ( Control *  self,
const SDL_Event *  event 
)
static
See also
Control::captureEvent(Control *, const SDL_Event *)

Definition at line 353 of file TextView.c.

353 {
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}
static bool isFocused(const Control *self)
Definition Control.c:342
static bool autocomplete(TextView *self, int step, const ImageAtlas *icons)
Auto-completes the text before the cursor.
Definition TextView.c:270
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 bool captureEvent(Control *self, const SDL_Event *event)
Definition TextView.c:353
static void resignKeyResponder(View *self)
Definition TextView.c:189
static Theme * theme(SDL_Window *window)
Definition Theme.c:295
@ ViewEventChange
A Control's input value has changed.
Definition Types.h:110
static void setNeedsLayout(View *self)
Definition View.c:1865
static void emitViewEvent(View *self, ViewEvent code, ident data)
Definition View.c:794
Controls are Views which capture and respond to events.
Definition Control.h:83
Packs many Images into one sheet, backed by one Texture.
Definition ImageAtlas.h:49
The Theme type.
Definition Theme.h:54

◆ dealloc()

static void dealloc ( Object *  self)
static
See also
Object::dealloc(Object *)

Definition at line 42 of file TextView.c.

42 {
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}
static void dealloc(Object *self)
Definition TextView.c:42

◆ init()

static View * init ( View *  self)
static
See also
View::init(View *)

Definition at line 107 of file TextView.c.

107 {
108 return (View *) $((TextView *) self, initWithFrame, NULL);
109}
static TextView * initWithFrame(TextView *self, const SDL_Rect *frame)
Definition TextView.c:543

◆ initialize()

static void initialize ( Class *  clazz)
static
See also
Class::initialize(Class *)

Definition at line 605 of file TextView.c.

605 {
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}
static void setAttributedText(TextView *self, const char *attributedText)
Definition TextView.c:566
static void stateDidChange(Control *self)
Definition TextView.c:512
static View * init(View *self)
Definition TextView.c:107
static void setDefaultText(TextView *self, const char *defaultText)
Definition TextView.c:584
static void render(View *self, Renderer *renderer)
Definition TextView.c:152
static void layoutSubviews(View *self)
Definition TextView.c:114
void setAttributedText(TextView *self, const char *attributedText)
Definition TextView.c:566
layoutSubviews(View *self)
Arranges this View's immediate subviews within its (already final) frame.
Definition Box.c:74

◆ initWithFrame()

static TextView * initWithFrame ( TextView *  self,
const SDL_Rect *  frame 
)
static

Definition at line 543 of file TextView.c.

543 {
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}
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
Text rendered with TrueType fonts.
Definition Text.h:131
bool isEditable
True if this TextView supports editing, false otherwise.
Definition TextView.h:125
Text * text
The text.
Definition TextView.h:135

◆ layoutSubviews()

static void layoutSubviews ( View *  self)
static
See also
View::layoutSubviews(View *)

Definition at line 114 of file TextView.c.

114 {
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}
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
static void layoutIfNeeded(View *self)
Definition View.c:1174

◆ render()

static void render ( View *  self,
Renderer *  renderer 
)
static
See also
View::render(View *, Renderer *)

Definition at line 152 of file TextView.c.

152 {
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}
static SDL_Size size(const Image *self)
Definition Image.c:416
static void drawLine(const Renderer *self, const SDL_Point *points, const SDL_Color *color)
Definition Renderer.c:266
static SDL_Rect renderFrame(const View *self)
Definition View.c:1629
W3C Color constants.
Definition Colors.h:37
SDL_Color White
Definition Colors.h:185

◆ replacePrefix()

static void replacePrefix ( TextView *  self,
const char *  chars 
)
static

Replaces the text before the cursor with chars, leaving the cursor after them.

Definition at line 254 of file TextView.c.

254 {
255
256 const Range range = { .location = 0, .length = self->position };
257 $(self->attributedText, replaceCharactersInRange, range, chars);
258
259 self->position = strlen(chars);
260}
size_t position
Definition TextView.h:130

◆ resignKeyResponder()

static void resignKeyResponder ( View *  self)
static
See also
View::resignKeyResponder

Definition at line 189 of file TextView.c.

189 {
190
191 const bool wasKeyResponder = $(self, isKeyResponder);
192
193 super(View, self, resignKeyResponder);
194
195 if (wasKeyResponder) {
196 SDL_StopTextInput(self->window);
197 }
198}
static bool isKeyResponder(const View *self)
Definition View.c:1126

◆ setAttributedText()

static void setAttributedText ( TextView *  self,
const char *  attributedText 
)
static

Definition at line 566 of file TextView.c.

566 {
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}

◆ setDefaultText()

static void setDefaultText ( TextView *  self,
const char *  defaultText 
)
static

Definition at line 584 of file TextView.c.

584 {
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}
char * defaultText
The default text, displayed when no user-provided text is available.
Definition TextView.h:114

◆ stateDidChange()

static void stateDidChange ( Control *  self)
static

Definition at line 512 of file TextView.c.

512 {
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}
View view
The superclass.
Definition Control.h:88

◆ unitLengthAt()

static size_t unitLengthAt ( const char *  chars,
size_t  len,
size_t  position,
const ImageAtlas *  icons 
)
static

Returns the length in bytes of the cursor unit starting at position.

A unit is one UTF-8 encoded character, a color escape sequence (^0 through ^9, or ^^), or a registered icon escape (:name:), so that the cursor never lands inside one.

Definition at line 205 of file TextView.c.

205 {
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}
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

◆ unitLengthBefore()

static size_t unitLengthBefore ( const char *  chars,
size_t  len,
size_t  position,
const ImageAtlas *  icons 
)
static

Returns the length in bytes of the cursor unit ending at position.

Units are found by parsing forward from the start of the string, as they are rendered: scanning backward would read the 1 in ^^1 as the escape ^1.

See also
unitLengthAt

Definition at line 237 of file TextView.c.

237 {
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}