Haiku BFont: Text Measurement, Baselines, and Spacing Modes
Measure Haiku text with the active BFont, handle ascent and descent correctly, choose spacing modes, and avoid brittle character-width estimates.
Text layout in Haiku should be measured with the BFont that will actually draw the text. Character count, average glyph width, and a hard-coded line height are not reliable substitutes: font family, style, size, spacing, fallback glyphs, and device metrics all affect the result. The Interface Kit exposes StringWidth() for horizontal measurement and GetHeight() for vertical font metrics, making it possible to lay out labels and baselines using the same font state as rendering.
The most important rule is to measure and draw with consistent state. If a view measures using one BFont and later draws using another, layout drift is expected. Font changes, window scale or rendering context changes, localization, and user-selected font settings can all change available width or glyph metrics. Recompute measurements when the relevant state changes instead of caching one width indefinitely.
Measure with the font the view uses
BFont::StringWidth() returns the horizontal space needed for a string in the font’s current attributes. Those attributes include family, style, size, and spacing. In a custom BView, retrieve the view’s current font, measure the text, and use that measurement to compute a preferred width or truncation boundary.
float MeasureLabel(BView* view, const char* text)
{
if (view == NULL || text == NULL)
return 0.0f;
BFont font;
view->GetFont(&font);
return font.StringWidth(text);
}
The example uses a NUL-terminated string and the font configured on the target view. If the application is laying out a BTextView, use its own text and line APIs where those APIs express the actual content model. StringWidth() measures the string’s baseline extent; it does not automatically perform word wrapping, line breaking, clipping, or truncation for a neighboring control.
Do not approximate width as a fixed number multiplied by character count. Proportional fonts have glyphs with different advances, and multibyte encodings mean byte length is not the same as the number of visible characters. A string with many narrow glyphs and one with wide glyphs can have the same number of characters but different widths. If you need to measure a batch, use the corresponding batch API with correctly sized input and output arrays.
Position text from ascent, descent, and leading
GetHeight() fills a font_height structure containing ascent, descent, and leading. Ascent is the distance characters can rise above the baseline, descent is the distance they can extend below it, and leading is inter-line spacing. Their sum is a useful line-box height, but drawing still uses a baseline coordinate; centering a baseline by treating the top of the glyph as the baseline can clip accents or descenders.
font_height metrics;
font.GetHeight(&metrics);
const float lineBoxHeight = ceilf(
metrics.ascent + metrics.descent + metrics.leading);
const float baseline = bounds.top + metrics.ascent;
view->DrawString(label, BPoint(bounds.left, baseline));
Include the appropriate headers and make sure the drawing view, bounds, and font use the same coordinate system. Haiku’s documentation recommends rounding font-height components upward when integral screen spacing is needed to reduce overlap. Do not round each individual metric downward and add them later; that can compound rounding error. A line layout should reserve enough room for the tallest glyphs and the chosen leading, not just the nominal point size.
Point size is not a pixel-height promise. A font’s ascenders, descenders, and leading determine its vertical metrics, and glyph shapes can extend beyond what a simplistic “font size equals line height” formula expects. When a user changes font size or style, remeasure the line box and invalidate the relevant layout and drawing regions.
Select a spacing mode for the output medium
Haiku defines spacing modes that balance screen appearance and measurement consistency. B_CHAR_SPACING positions characters according to their individual advances and is suited to high-resolution output such as printing, but can look poorly separated at small screen sizes. B_STRING_SPACING optimizes positions within the total width of a string so that screen and print line lengths can align more closely; character positions can shift as the surrounding string changes. B_BITMAP_SPACING uses bitmap-oriented widths that improve screen legibility and avoid collisions, and is the spacing mode used by BTextView.
These modes affect more than cosmetic kerning. If an editor measures a line in one mode and draws it in another, caret placement, selection bounds, truncation, and hit testing can disagree. Preserve the BFont spacing settings used by the drawing path, and do not copy only its family and size while silently losing flags or spacing. If you intentionally change spacing, recompute any cached advances and test editing interactions at the new mode.
For controls that display a set of comparable labels, consistent font state is more important than choosing a theoretical “best” spacing mode. For printable content, use printing metrics where the API supports them and validate the final output path. Screen and printer metrics can differ, so a screen preview is not proof that pagination or exact printed alignment is correct.
Truncate visually, not by arbitrary character count
If a label must fit, use font-aware truncation or a layout policy that reserves space for an ellipsis. Cutting a UTF-8 string at a byte offset can split a multibyte sequence; cutting by a fixed number of characters can still produce an overlong label. BFont::TruncateString() and GetTruncatedStrings() are designed to fit text to a maximum width using a chosen truncation mode. The API documents that output buffers for GetTruncatedStrings() must be large enough for the possible UTF-8 ellipsis expansion and a terminator.
Choose truncation direction based on the information users need. A long path may be more useful when its unique file name remains visible at the end; a title may be better truncated at the end; a set of similar names may benefit from preserving their differing sections. B_TRUNCATE_SMART is documented as unimplemented in the current reference, so do not select it expecting smart path-aware behavior. Test the exact mode on the Haiku release you ship.
Text truncation is a presentation choice, not a data transformation. Keep the full value in the model and expose it through a tooltip, details view, copy action, or accessible text where appropriate. Never persist the shortened display string as if it were the original user data.
Account for font readiness and fallback
System BFont objects such as be_plain_font are initialized after a BApplication is created. Code that constructs a font before the application lifecycle is established can observe invalid system font state. Initialize GUI typography inside the supported application lifecycle and check dependencies before using system font objects.
Different fonts can lack glyphs for some scripts or symbols. A fallback font may render characters that the primary face does not support, and the fallback changes visible shape and width. Do not assume that a Latin sample validates all localized text. Test the actual scripts, combining marks, emoji or symbols relevant to the product, and user-supplied strings. If the product must detect glyph support, use the API’s glyph-query functions and still verify how fallback rendering behaves in the target UI.
Localization can also expand labels and change word boundaries. Recompute preferred sizes after translated strings are installed; do not reserve width based only on the English source. A robust layout combines measured text with minimum and maximum control sizes, wrapping or truncation rules, and the Interface Kit’s layout constraints.
Validate the full drawing path
Test empty text, long text, mixed narrow and wide glyphs, non-ASCII scripts, larger font sizes, alternate styles, and a font change while the window is open. Confirm the baseline remains inside the view, descenders are not clipped, and truncation preserves valid UTF-8. For printing, test the printer metric path separately from screen rendering.
Instrument layout measurements when a report says “text is cut off”: record the view bounds, selected font family/style/size, spacing, measured width, baseline, and actual draw point. That evidence separates a font metric mismatch from an incorrect coordinate conversion or a stale cached layout.
BFont provides the primitives to measure text accurately, but the application must still use them consistently. Measure with the active font, position from baseline metrics, choose a spacing mode for the destination, and refresh layout whenever the rendering inputs change.
Related:
- Haiku BTextView: UTF-8 Ranges, Styled Runs, and Reliable Editing
- How to Build Resizable Haiku Interfaces with BLayout Instead of Fixed Coordinates
Sources: