Add a text measurement API - #588
Conversation
Co-authored-by: Cedrick Cooke <development@cedrickc.net>
…tory) Co-authored-by: Cedrick Cooke <development@cedrickc.net>
… metrics) Co-authored-by: Cedrick Cooke <development@cedrickc.net>
Co-authored-by: Cedrick Cooke <development@cedrickc.net>
…a/android) Co-authored-by: Cedrick Cooke <development@cedrickc.net>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 0a03fc9. Configure here.
| return TextMetrics( | ||
| width = metrics.width.toFloat(), | ||
| ascent = ascent.toFloat(), | ||
| descent = descent.toFloat(), |
There was a problem hiding this comment.
HTML vertical metrics fallback wrong
Medium Severity
When fontBoundingBoxAscent / fontBoundingBoxDescent are unavailable, HtmlTextMeasurement falls back to actualBoundingBoxAscent / actualBoundingBoxDescent or zero. Those values are string-specific, not font metrics, so ascent/descent can vary by label text or become zero while width stays positive—contradicting TextMetrics and breaking layout that reserves height from measurement.
Reviewed by Cursor Bugbot for commit 0a03fc9. Configure here.


Summary
Krayon could draw text (
Kanvas.drawText) but couldn't measure it, so layout code had no way to reserve space for labels ahead of time. This adds a primitive, single-line text-measurement API. Line wrapping is an explicit non-goal.The design mirrors the existing
IsPointInPathoptional-capability paradigm:kanvascommon:TextMetrics(width, ascent, descent)with aheightconvenience.ascent/descentare baseline-relative (positive-up) font metrics;widthis the string's advance.fun interface MeasureText { fun measureText(text: CharSequence, paint: Paint.Text): TextMetrics }.MeasureText:AndroidKanvas,HtmlKanvas,CGContextKanvas, andComposeKanvas.SvgKanvasdeliberately does not (it has no font engine — same reason it doesn't implementIsPointInPath). The testCallRecordingKanvasis untouched since the capability is a separate interface.Kanvas(chart code runs layout inUpdateElement.update(...)with noKanvas, exactly like interaction uses standaloneIsPointInPaths):AndroidTextMeasurement(context),object CoreTextMeasurement(Core Text),HtmlTextMeasurement()(offscreen canvas), and Compose's publictextMeasurement()factory.Kanvasclasses delegate to these to avoid duplicated font resolution.Each implementation reuses the exact font resolution already used for drawing, so measurements match what gets rendered.
Usage
Testing
Per-platform tests cover every measurement implementation:
MeasureTextTestsAndroidTextMeasurementTests(Robolectric, native graphics)HtmlTextMeasurementTestsjsBrowserTest)CoreTextMeasurementTests(appleTest)SkiaMeasureTextTests(viatextMeasurement())AndroidMeasureTextTests(Robolectric, native graphics)Each asserts real behavior: empty string → 0 width, non-empty → positive width/ascent/descent, longer strings measure wider, and vertical metrics are independent of the text.
Also verified: compiles on all targets (jvm, android, js, wasmJs, Apple macos/ios);
./gradlew apiCheck(full project) andlintKotlinpass; API dumps regenerated forkanvasandcompose.Notes / out of scope
ContinuousAxisis intentionally left behavior-stable; this API makes label auto-sizing possible as a follow-up.