July 20, 2017

How Carets Move

When I show up at networking events and tell people “I’m spending a month reimplementing a <textarea> from scratch,” they usually give me a blank look and shuffle away slowly. At first glance, textareas are so ubiquitous that they seem like they’d be trivial to implement. They’re not. As an example, let’s imagine we have a caret in the very middle of a three-lined textarea:


screenshot of caret in middle of textbox


If I press the up arrow on my keyboard, what happens? This is still the easy part, so hopefully you said “it jumps up to the first line.”


screenshot of caret at top middle of textbox


But now, if I press up again, what happens? Many people get hung up here, and say it stays in place. In fact, it moves to the beginning of the line.


screenshot of caret at top left of textbox


If you’ve gotten it right up to this point, you probably think you understand it, and this caret movement thing isn’t too complicated. Just have a special case to jump to the beginning of the line when we go off the top, right?

Well, consider this — what happens when you now press the down arrow on the keyboard?


screenshot of caret at middle left of textbox


The beginning of the second line (shown above) is a pretty reasonable solution. After all, if we started at the top left corner and pressed down, it would obviously go to this point. However, it’s wrong. The caret jumps to the middle of the second line, back to the original position at the very beginning of this post.


original screenshot of caret in middle of textbox


Hopefully by now you’re starting to see that something pretty fishy is going on. This is similar to the case that you’re perhaps more familiar with, where the caret is at the end of a long line:


screenshot of caret at middle right of textbox


If you press up and then down in the case above, the caret will appear back at the end of the second line. We don’t want the caret to drift leftward as we pass over short lines.

We can handle both of these cases (and some other problems) with a simple solution: the caret must remember its x position, in pixels, and only updated this stored position when moving left/right or when typing. Then, whenever moving up/down, we jump to the closest character to that stored pixel x position. For non fixed-width typefaces, this being a pixel measurement and not a character measurement is crucial, since we want the caret to go up to the visually equivalent character, not the char-num equivalent character.