Haiku BTextView: UTF-8 Ranges, Styled Runs, and Reliable Editing
Use Haiku BTextView correctly across UTF-8 edits, selection ranges, styled runs, clipboard operations, undo state, scrolling, and user input.
BTextView is Haiku’s multiline text-editing view. It combines a text buffer, caret and selection state, keyboard and input-method handling, clipboard operations, undo support, styled runs, and text-to-view geometry. A reliable editor treats those as connected parts of one editing model instead of a string widget that happens to draw several lines.
The first boundary to get right is offset units. The public API passes int32 offsets and lengths, while TextLength() reports the backing buffer length in bytes. The implementation inserts the byte length of a UTF-8 string and explicitly avoids cutting a multibyte sequence when SetMaxBytes() truncates. These offsets are not grapheme-cluster counts. A user-perceived character can contain multiple Unicode code points, and one code point can occupy multiple UTF-8 bytes.
Keep text offsets in the view’s coordinate system
Use TextLength(), GetSelection(), and OffsetAt() to obtain offsets that belong to this text view. Do not pass an index from a language-level Unicode string iterator directly to Select(), GetText(), Insert(), or Delete() unless it has been converted to the view’s byte-offset representation and remains on a valid UTF-8 boundary.
#include <TextView.h>
#include <string.h>
static status_t ReplaceSelection(BTextView* view, const char* replacement)
{
if (view == NULL || replacement == NULL)
return B_BAD_VALUE;
int32 start = 0;
int32 end = 0;
view->GetSelection(&start, &end);
const int32 textLength = view->TextLength();
if (start < 0 || end < start || end > textLength)
return B_BAD_VALUE;
if (end > start)
view->Delete(start, end);
view->Insert(start, replacement, (int32)strlen(replacement));
return B_OK;
}
The replacement string in this example must be valid UTF-8. strlen() is suitable for the byte length of a NUL-terminated UTF-8 buffer because UTF-8 does not use a NUL byte inside an encoded non-NUL character. It does not return a character count. If text comes from an external decoder, validate the encoding before passing it to the view and do not split a multibyte sequence while calculating a range.
The GetText(offset, length, buffer) API copies a byte range into caller-owned memory and the current implementation appends a NUL terminator. Allocate at least length + 1 bytes, especially when the requested range is the entire text. Text() returns a pointer owned by the view. Copy its contents before changing or destroying the view if a longer-lived snapshot is needed.
Selection ranges have two endpoints. A collapsed selection has equal start and end values; a nonempty selection can be replaced by deleting that range and inserting at its start. Make selection state explicit in code rather than assuming that the caret position is also the start of the selection. Test ranges at the beginning and end of the buffer, around newlines, and adjacent to multibyte UTF-8 text.
Separate insertion, user input, and document policy
Insert() edits at the current selection or at an explicit offset. Delete() removes the selection or an explicit range. These methods are convenient for programmatic edits such as replacing a token or inserting generated text, but document-level policy still belongs in the application. For example, if an editor tracks unsaved changes, update that state only after a successful edit path and keep file persistence separate from the view’s in-memory undo history.
The view handles ordinary keyboard input and input-method composition. Do not reimplement text entry by interpreting every raw key event as a character: composed input, keyboard layouts, dead keys, and input methods can produce text through a path more complex than a single KeyDown() byte. Use the view’s editing behavior for user input, and reserve explicit range edits for application actions that have well-defined text semantics.
If a subclass overrides the protected InsertText() or DeleteText() hooks, it must preserve the base class’s text-buffer, line-layout, style-run, caret, and refresh invariants. A subclass that only updates a separate std::string while ignoring the view’s buffer can make painting, selection, and undo disagree. Prefer wrapping public edit commands at the application layer unless a lower-level extension is necessary and thoroughly tested.
SetText() replaces the current content. Its overloads accept a NUL-terminated string, an explicit byte length, or a BFile range. The file overload lets the caller specify a source offset and length; it does not by itself make BTextView a streaming editor for arbitrarily large files. The view needs an in-memory text model and layout information for displayed content, so applications with very large documents should test memory and editing latency before choosing it as their sole document representation.
Treat styled runs as a parallel range model
Plain text and style are related but separate. A stylable BTextView stores font and color information in text_run_array ranges. SetFontAndColor(start, end, ...) styles a range; SetRunArray() applies a run array; RunArray() returns a dynamically allocated snapshot that the caller must release with FreeRunArray(). Keep run offsets aligned with the same text range model as edits, and avoid retaining a pointer returned by RunArray() after freeing it or after the associated text changes.
view->SetStylable(true);
BFont emphasis;
view->GetFontAndColor(&emphasis, NULL, NULL);
emphasis.SetFace(B_BOLD_FACE);
int32 start = 0;
int32 end = 0;
view->GetSelection(&start, &end);
if (end > start)
view->SetFontAndColor(start, end, &emphasis, B_FONT_FACE);
Styling a selected range changes presentation, not the underlying bytes. Preserve that distinction in save formats: plain-text files store text only, while a rich-text format must serialize the run data using a documented representation. If an application writes custom style metadata, version that format and define how a missing or unknown style is handled. Do not assume that the in-memory text_run_array is automatically persisted when calling Text() or GetText().
Before enabling stylable mode in a large document, test the cost of frequent style changes and edits. Syntax highlighting that reapplies the entire document on every keystroke can create avoidable work. Keep style ranges minimal, coalesce adjacent ranges with identical formatting when generating them, and invalidate only the affected presentation where the API allows it.
Coordinate caret, line, and viewport APIs carefully
The text API exposes both byte-oriented and line-oriented operations. LineAt(offset) maps a text offset to a line; OffsetAt(line) maps a line number to the offset at its start; PointAt(offset) and OffsetAt(point) bridge the text buffer and view geometry. These conversions are useful for search results, diagnostics, and navigation, but a visual line can wrap without a newline. Distinguish logical line numbers from on-screen wrapped rows.
Use ScrollToOffset() or ScrollToSelection() after an application command that moves the caret away from the visible viewport. Do not calculate scroll position from an assumed character width: fonts, tabs, wrapping, insets, and user zoom all affect layout. SetWordWrap(), SetTabWidth(), SetTextRect(), and SetInsets() change presentation geometry and should be covered by resize and font-size tests.
Keyboard navigation and text edits should occur on the window’s looper thread when the view is attached. A worker that performs search or parsing can return byte ranges to the UI thread, but should not mutate the view directly while it is drawing or processing input. If worker results were computed from an earlier document revision, associate them with a document generation or snapshot and reject stale offsets instead of applying them to changed text.
Use undo as an editing facility, not a save protocol
BTextView exposes SetDoesUndo(), UndoState(), and Undo(). Undo state answers whether an undo or redo action is available; it does not say whether the document matches the last saved file. The application should maintain its own saved revision or dirty-state policy. A user can undo back to the saved text, make a different edit, or save through a format conversion, so comparing only the number of undo steps is not a reliable modified flag.
When an application performs a compound operation, such as replacing multiple occurrences, decide whether the user should undo it as one logical action or as several. Test the actual grouping behavior of the chosen API path rather than assuming all programmatic edits are grouped automatically. Clipboard cut, copy, and paste also cross a system service boundary; preserve the view’s standard actions unless the application has a clear reason to override them.
Be deliberate with SetMaxBytes(). It limits the text buffer in bytes rather than user-visible characters. The implementation avoids ending at a partial multibyte UTF-8 character when reducing the limit, but a limit can still truncate a document and should not be used as a substitute for a user-facing validation message. If a format limit is expressed in Unicode characters, calculate and enforce that rule separately from the view’s byte capacity.
Acceptance tests for a text-editing surface
Build a small test matrix that includes ASCII, accented Latin text, a multibyte symbol, combining marks, right-to-left text if supported by the application, blank lines, tabs, and wrapped paragraphs. For each, test selection, copy/paste, replace, delete, undo/redo, save/reopen, and navigation to a search result. Verify that all byte ranges stay within 0..TextLength() and never split an encoded character.
Exercise the view at multiple window sizes and fonts. Check that a long selection scrolls into view, that wrapped visual rows are not mistaken for logical lines, that styled runs survive intended save formats, and that a plain-text save does not falsely promise formatting preservation. Simulate a worker finishing search after the document changes and confirm stale offsets are discarded.
BTextView is powerful because its APIs share one buffer and one coordinate system. Correct editors respect byte ranges, preserve the view’s input-method and undo paths, and keep document policy and persistence outside the widget. Once those boundaries are explicit, selection bugs and corrupted multibyte text become testable failures rather than mysterious rendering glitches.
Related:
- How to Configure and Test a Non-US Keymap on Haiku
- Haiku’s Locale Kit: Catalogs, Formatting, and Runtime Language Selection
Sources: