Skip to content

Add a text measurement API - #588

Draft
cedrickcooke wants to merge 5 commits into
mainfrom
cedrickc/cursor/text-measurement-api-9ab9
Draft

Add a text measurement API#588
cedrickcooke wants to merge 5 commits into
mainfrom
cedrickc/cursor/text-measurement-api-9ab9

Conversation

@cedrickcooke

Copy link
Copy Markdown
Collaborator

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 IsPointInPath optional-capability paradigm:

  • kanvas common:
    • TextMetrics(width, ascent, descent) with a height convenience. ascent/descent are baseline-relative (positive-up) font metrics; width is the string's advance.
    • fun interface MeasureText { fun measureText(text: CharSequence, paint: Paint.Text): TextMetrics }.
  • Concrete backends implement MeasureText: AndroidKanvas, HtmlKanvas, CGContextKanvas, and ComposeKanvas. SvgKanvas deliberately does not (it has no font engine — same reason it doesn't implement IsPointInPath). The test CallRecordingKanvas is untouched since the capability is a separate interface.
  • Standalone measurers for layout-without-a-Kanvas (chart code runs layout in UpdateElement.update(...) with no Kanvas, exactly like interaction uses standalone IsPointInPaths):
    • AndroidTextMeasurement(context), object CoreTextMeasurement (Core Text), HtmlTextMeasurement() (offscreen canvas), and Compose's public textMeasurement() factory.
    • The platform Kanvas classes 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

val paint = Paint.Text(black, size = 14f, Paint.Text.Alignment.Left, Font(sansSerif))

// From a measuring Kanvas:
val metrics = (kanvas as MeasureText).measureText("Hello", paint)

// Or, during chart layout, from a standalone measurer (no Kanvas needed):
val measurer: MeasureText = AndroidTextMeasurement(context) // or HtmlTextMeasurement() / CoreTextMeasurement / compose textMeasurement()
val width = measurer.measureText("Hello", paint).width

Testing

Per-platform tests cover every measurement implementation:

Platform Test Where it runs
kanvas common MeasureTextTests every target
kanvas Android AndroidTextMeasurementTests (Robolectric, native graphics) JVM/CI
kanvas JS HtmlTextMeasurementTests ChromeHeadless (jsBrowserTest)
kanvas Apple CoreTextMeasurementTests (appleTest) macOS CI
compose skia SkiaMeasureTextTests (via textMeasurement()) JVM/CI
compose Android AndroidMeasureTextTests (Robolectric, native graphics) JVM/CI

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) and lintKotlin pass; API dumps regenerated for kanvas and compose.

Notes / out of scope

  • No line wrapping (explicit non-goal).
  • ContinuousAxis is intentionally left behavior-stable; this API makes label auto-sizing possible as a follow-up.
Open in Web Open in Cursor 

cursoragent and others added 5 commits July 13, 2026 22:05
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>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ 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(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 0a03fc9. Configure here.

@github-actions

Copy link
Copy Markdown

Test Results

85 tests  +6   85 ✅ +6   2s ⏱️ ±0s
17 suites +2    0 💤 ±0 
17 files   +2    0 ❌ ±0 

Results for commit 0a03fc9. ± Comparison against base commit 1589cbc.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants