Skip to content

fix: widgets a scroll view clips away get no outline - #30

Merged
Harrys76 merged 1 commit into
mainfrom
fix/clipped-highlights
Oct 27, 2026
Merged

Harrys76 merged 1 commit into
mainfrom
fix/clipped-highlights

Conversation

@Harrys76

Copy link
Copy Markdown
Owner

A widget that a scroll view clips away (a list row scrolled under the app bar) no longer gets an outline, a marker or a step, and an outline covers only the part of a widget that shows. Found while recording the 1.0 launch film on the iPhone 12, after #29 merged and before the v1.0.0 tag moved.

The bug

On the Chat App demo, once new messages had scrolled the list, the crosshair on Oversized Images outlined the avatar of message #42. That row sat above the list, under the tab bar, and the outline and its label were drawn over the screen's top edge. The bar said "Showing Image · 1 of 17", counting rows above and below the list that the screen did not show.

Cause

A list builds rows past each edge (its cache extent, 250 px by default) and paints only the ones inside its viewport. Detectors measure a built row's full rect with getGlobalRect, and the first-on-screen step (sleuth_controller.dart), the bar's "Showing" state (floating_issues_card.dart) and the outline painter (highlight_overlay.dart) test that rect against the screen, not against the list's clip. A row hidden behind the app bar still overlaps the screen, so it passed. A VM-service read of the controller's highlights on the device showed the selected image at y −26 to 30 and three more rows wholly above the list's top edge (near y 240): 17 highlights, of which the list showed 9.

Fix

  • Detectors keep reporting where a widget is laid out (getGlobalRect is unchanged). The controller now decides what shows when it publishes highlights: the new getVisibleGlobalRect walks to the root once, as localToGlobal does, and on the way reads each ancestor's describeApproximatePaintClip (the method semantics uses for visibility; a viewport answers with its edge, and returns null for Clip.none). A highlight is published cut to those clips, or left out when no part of it shows, so the outline, the markers, the step count and the bar only see what the screen shows.
  • The controller keeps the last scan's highlights whose widget stays built, hidden ones included and in scan order. Each rect refresh (on scroll) publishes them again, so a row that scrolls into view gets its outline while the list scrolls, whether it was hidden when the scan ran or scrolled out after it.
  • The visible rect comes from the box's transformed corners (MatrixUtils.transformRect) instead of its transformed origin plus its size. Under Transform.flip the origin maps to the far edge, so a rect built from it sat beside the widget, and the clip check would have dropped a visible widget. A highlight whose render object is not a box is published as given, as before.

Tests

  • getVisibleGlobalRect on a list scrolled by 75 px: null for the built rows above and below the list, the first visible row cut at the list's top edge, a full row unchanged, and the whole rect when the list does not clip. It measures a flipped box under a ClipRect where it shows, and returns null for a detached box and for a kept-alive page that is not showing.
  • Publishing the 11 built rows of a scrolled list keeps the 5 the list shows, the first cut at its edge, and the crosshair starts on the first of them with "1 of 5".
  • With the real overlay, a highlighted row scrolled into the cache extent loses its highlight and outline and gets both back when it scrolls back, and a row hidden when a scan runs gets its highlight when it scrolls into view, both with no scan tick.

The clip, flip, detached, publish and scroll tests each fail with their part of the fix taken out.

Checks

  • iPhone 12, Chat App, same flow: "Showing Image · 1 of 9", the outline on the top visible avatar cut at the list's edge, and all 9 highlights between y 241 and 746, inside the list. Jumping the list 150 px hid the outlined row under the header (no outline, 8 highlights), and jumping back put the outline on the same row again.
  • 5,109 tests on 3.47.6 and on the 3.32.8 floor, 44 benchmarks, 353 example tests, analyze, format and dartdoc clean, publish dry run 0 warnings. Codex review clean on the final commit.

After merge

v1.0.0 is tagged and has a GitHub Release, but 1.0.0 is not on pub.dev yet. The tag moves to this PR's merge commit and the release notes gain the Fixed line, so the published 1.0.0 includes this fix and #29.

🤖 Generated with Claude Code

A list builds rows past its edge (its cache extent). Their highlights
kept the full rect, which passed the on-screen check wherever the screen
covered them, so a list card's outline could start on a row scrolled
under the app bar and count rows the screen did not show.

The controller now publishes the part of each highlight that shows:
getVisibleGlobalRect cuts the box to each ancestor's paint clip in one
walk to the root, and a highlight with no part showing is left out.
Detectors still report where a widget is laid out. The controller keeps
the hidden highlights whose widget stays built, so a row that scrolls
into view gets its outline on the next rect refresh, not the next scan.
The visible rect comes from the box's transformed corners, so a flipped
or scaled box is not placed beside where it shows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Harrys76
Harrys76 merged commit f169bf0 into main Oct 27, 2026
6 checks passed
@Harrys76
Harrys76 deleted the fix/clipped-highlights branch October 27, 2026 03:40
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