Skip to content

feat: setCursor, GDI face names (fontFamilyOf), font width, letter spacing, OpenType features and x-height for react-x11's Windows text engine - #14

Merged
sidorares merged 2 commits into
mainfrom
feat/cursor-and-text
Oct 4, 2026
Merged

sidorares merged 2 commits into
mainfrom
feat/cursor-and-text

Conversation

@sidorares

Copy link
Copy Markdown
Contributor

Found running @react-x11/components' browser example on Windows for the first time. react-x11 asked the bridge for things it did not have, and a page differed from Chrome's in ways a reader sees at once. Each item here is one of those.

Cursor

setCursor(windowId, name) maps a CSS cursor name to a system cursor: the link hand, the I-beam, the resize arrows, wait, not-allowed. none hides the cursor. The window answers WM_SETCURSOR over its client area with the cursor last set. Where the pointer is already over the window, the new cursor shows at once, since the move that changed what's under it was answered before the name arrived.

react-x11 already calls native.setCursor?.(id, name) for every window and for every <Frame> pane's host, but the class cursor was IDC_ARROW and nothing answered. No link ever showed the hand on Windows. I checked this with the real pointer: hand over a link, I-beam over text, crosshair where a page sets one, arrow elsewhere, both in a window and through a <Frame> pane.

Text

  • fontFamilyOf(name) looks up a name the system collection has no family for, the way GDI does (CreateFontFromLOGFONT). It answers { family, weight, stretch, italic }. DirectWrite groups faces by weight, width and slope, so Arial Black, Arial Narrow, Segoe UI Light and Franklin Gothic Medium are faces of other families, and fontExists says no to each. Chrome finds them all, and a page naming one fell back to the default face.
  • Font width (stretch, 1–9): on layoutCreate's options and spans, and as the fifth argument of fontMetrics and fontHandle. This is what reaches a narrow face. It's part of the handle cache key.
  • letterSpacing on a span, through IDWriteTextLayout1::SetCharacterSpacing. Without it, CSS letter-spacing came out tight, and react-x11's <Html>, which justifies through letter spacing when the engine can't justify itself, left every justified paragraph ragged.
  • features on a span, { smcp: 1, liga: 0 } by tag, through an IDWriteTypography.
  • xHeight and capHeight from fontMetrics: what CSS's ex and cap units are measured in.

Tests

test/text.js covers the GDI lookup (Segoe UI Semibold → Segoe UI 600, an unknown name → null), the width on handles and spans, letter spacing (five characters at 2 px each come out 10 px wider), features, and the new metrics. npm run test:unit and npm run test:window pass locally on Windows 11.

The react-x11 half is in the core PR that follows this one. It feature-detects fontFamilyOf and is harmless against 0.0.7.

Still open (not in this PR)

Under a lineHeight, layoutCreate uses proportional spacing with the baseline at 0.8 × multiple. At a multiple of 1 that puts the baseline at 80% of the face's. react-x11 now reads the ascent and descent the bridge measures, so <Html> compensates, but core's own <text> still draws high in its line box under a line height. That's for a follow-up.

🤖 Generated with Claude Code

sidorares and others added 2 commits October 4, 2026 20:06
…ient area, where the class cursor was the arrow everywhere and react-x11 asked for a cursor nothing answered

A name maps to the system's cursors — the link hand, the I-beam, the
resize arrows, wait, not-allowed — and `none` hides it. The window
answers WM_SETCURSOR over its client area with the cursor last set, and
shows it at once where the pointer is already there, since the move that
changed what is under it has been answered by then. react-x11 calls it
for every window and every <Frame> pane's host, and called it into
nothing: no link in a document ever showed the hand on Windows.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
… and a face take a width, a span its letter spacing and OpenType features, and a face's metrics carry its x-height and cap height — where Arial Black and Segoe UI Light fell back to the default face, `letter-spacing` was set tight and an `ex` had no face to be measured in

- `fontFamilyOf(name)`: a name the system collection has no family by,
  looked up as GDI looks a face name up (`CreateFontFromLOGFONT`), and
  answered as the family DirectWrite files the face under and the face's
  weight, width and slope. DirectWrite groups faces by those three, so the
  families a Windows user knows by name — Arial Black, Arial Narrow,
  Segoe UI Light, Franklin Gothic Medium — are faces of others here, and
  every other Windows program, Chrome among them, finds them.
- A width (`stretch`, 1–9) on `layoutCreate`'s options and spans, and as
  the fifth argument of `fontMetrics` and `fontHandle`: what reaches a
  narrow face.
- `letterSpacing` on a span, through `SetCharacterSpacing`, and
  `features` by tag, through an `IDWriteTypography`.
- `xHeight` and `capHeight` from `fontMetrics`, what CSS's `ex` and `cap`
  are measured in.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@sidorares
sidorares merged commit 59c7862 into main Oct 4, 2026
7 checks passed
sidorares added a commit to sidorares/react-x11 that referenced this pull request Oct 4, 2026
…ce names, font widths, letter spacing, OpenType features, x-heights and DirectWrite's justification (#879)

The range admitted 0.0.7 alone (a caret on 0.0.x is exact), so what #874
asks the bridge for was answered by none: no link showed the hand on
Windows, Arial Black and Segoe UI Light fell back to the default face, a
`letter-spacing` was set tight and an `ex` had no face to be measured in.
0.0.8 (windowkit/win32#14) and 0.0.9 (windowkit/win32#16, the
justification and the min-content width on request #878 switches on).

Co-authored-by: Claude Opus 5.5 <[email protected]>
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.

1 participant