Repository navigation
feat(native): native scrollbars, portals, layout and scroll events, fonts, and a cheaper render path - #88
Conversation
…s and more alignment values
…ers stop re-rendering
Each render() call wrapped the tree in a Provider whose value was a new
{ renderer } object, so every call was a context change. In the chat example
that re-rendered ChatApp, whose inline onRetry closure then busted the memo on
Transcript: 305 SafeMdxContent renders and about 9 ms per find-bar keystroke.
The example also keeps onRetry stable now, so typing in the composer no longer
re-renders every row.
A padded box around a list is better served by padding on the list itself, with the thumb at the list's own edge. The list now takes padding on all four sides.
…ides The list ignored left and right padding, so rows spanned its full width. Rows now sit inside all four sides, and a scrollbarColor thumb stays at the list's own edge, past the padding. Moves the zed submodule to the commit that lays rows out inside horizontal padding.
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
gpuix/packages/native/src/retained_tree.rs
Lines 637 to 640 in 447ade9
For a pointerEvents: "none" wrapper whose child leaves pointerEvents unset, RetainedTree::collect_hits carries none into every descendant here, so getElementsAt drops the child even though GPUIX intentionally makes this property non-inherited and the child keeps its own hitbox. This makes the query disagree with actual clicks for translating or overlay wrappers; suppress only the element that declares none rather than passing that state to children.
AGENTS.md reference: AGENTS.md:L782-L784
gpuix/packages/native/src/retained_tree.rs
Lines 667 to 668 in 447ade9
When the point is over rendered text, this unconditional push includes each content-bearing primitive host text node before its JSX parent. React splits <text>Hello {name}!</text> into several such nodes, but their IDs have no PublicInstance/ref or event handler and DOM elementsFromPoint would return the parent element, so the documented [topmost] result becomes an unusable internal ID on ordinary labels; skip primitive text instances or coalesce them to their parent.
AGENTS.md reference: AGENTS.md:L230-L232
gpuix/packages/native/src/retained_tree.rs
Lines 626 to 628 in 447ade9
For a <virtual-list> with a partially visible row taller than its viewport (or an overdraw row), this branch never marks the point clipped: build_virtual_list uses gpui::list() without storing overflow: "scroll" in StyleDesc, and it does not record a box for the list ID. The row's full painted border box can therefore be returned for coordinates outside the list, even though the new getElementsAt contract says virtual-list and scroll content is clipped; record and use the list viewport as an implicit scroll clip.
AGENTS.md reference: AGENTS.md:L7-L11
gpuix/packages/native/src/renderer.rs
Line 8146 in 447ade9
When data is a TTC/OTC collection, check_font validates only face index 0 even though registerFont advertises loading every font in a collection and passes the entire buffer to add_fonts. A later face without the required names or m glyph therefore bypasses the guard, so it can be silently unavailable or reach the GPUI abort that this check is intended to prevent; enumerate and validate every face before registering the bytes.
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Summary
Features a large DOM-style React app needed from GPUIX, plus the render-path costs found while profiling it. Everything is opt-in or behaviour-preserving apart from the notes under "Behaviour changes".
New
createPortal(children, target)mounts children into an element that is already mounted, kept in key order. See "Portals" in the README.scrollbarColor/scrollbarWidthpaint a native thumb onoverflow: scrollcontainers and on<virtual-list>. The thumb is drawn in the frame the scroll happens: no JS in the loop, no extra frame, no pointer input.estimatedItemHeight.paddingon a<virtual-list>now insets its rows on all four sides (it ignored left and right before), and the thumb stays at the list's own edge, past the padding.onLayouton<div>and<text>reports the border box after first paint and on each size change.onScrollChangereports the offset of anoverflow: scrollelement after any scroll (wheel, scrollbar,scrollTo,scrollIntoView).registerFont(bytes)loads a TrueType/OpenType font thatfontFamilycan name.getElementsAt(x, y)lists the elements under a window point, topmost first, likeelementsFromPoint.collectGarbageWhenIdle()runs one full GC after the app has stopped committing. It watches the newappliedBatchCount()and CPU.aspectRatio,borderStyle: "dashed",boxShadowarrays andinset,alignItems: stretch | baseline,justifyContent: space-evenly.@gpuix/native/hostexportssameMutationValueandEVENT_TYPE_BY_PROPfor adapters that diff the same payloads.Fixed
hoverstyle, cursor and tooltips stayed until the pointer came back). The test renderer gainssimulateMouseExit.Performance
applyBatchrepaints only when the batch changed something a frame would draw.canvaschild to record bounds, so a frame over the same tree is about 15% cheaper.root.render()keeps one context value, souseGpuix()consumers stop re-rendering on every call. In the chat example a find-bar keystroke went from 305 row renders and about 9 ms to 0.2 ms.examples/.Behaviour changes to review
getElementBounds(), automation locators and text selection regions use the element's border box. They used the padding box, which was narrower by the border width.@gpuix/reactmoves toreact-reconciler@^0.33.0and peers onreact@^19.2.0(the old^18half of the range was never true).visibility: hiddenelements keep a bounds child, because gpui paints nothing for them.Zed fork
Depends on remorses/zed#10, five commits on
gpuix: forget failed font lookups when fonts are added, end hover at the window edge,ListState::averaged_scroll_extent, rows laid out inside left and right list padding, and a doc wording fix. That PR must merge first. The submodule pointer here references its head (c2e9f88) and has to be updated if it is squashed or rebased.Test plan
packages/react: 536/536 against an addon built from this HEADbun run build:nativeandbun run build:reactcargo testfor the native crate and gpui's list tests were not re-run on the final commits19 changesets: native patch/minor, react patch/minor.
Authored with Cursor (Claude Sonnet 5.5).