Skip to content

fix(graph): size the graph column to the lanes on screen, with a cap - #93

Merged
noahbclarkson merged 3 commits into
mainfrom
fix/graph-column-width
Nov 4, 2026
Merged

noahbclarkson merged 3 commits into
mainfrom
fix/graph-column-width

Conversation

@noahbclarkson

@noahbclarkson noahbclarkson commented Nov 2, 2026 •

Copy link
Copy Markdown
Owner

Problem

On large repositories the graph column was sized to the busiest row anywhere in the loaded history (global_max_lane), with no upper limit. A wide stretch far down the history (dozens of long-lived upstream/* branch lines) made every row reserve that width. Linear sections showed hundreds of pixels of empty gutter, the message column got squeezed to a few words, and "Load more commits" could only make it wider.

What changes

  • The column fits what's on screen. The list measures the lanes drawn by the rows it actually shows: node lanes, active lanes, and lanes merging in. A linear stretch gets a narrow column even if the history below it is wide. Wider takes effect at once; narrower waits 400ms after the rows on screen stop changing, so the Author/Date columns don't shift back and forth mid-scroll.
  • A cap you can drag. The column never grows past graph_column_max_width (default 270px, about 12 lanes). Drag the right edge of the Graph header to change it; it's saved like the author and date widths.
  • The message column keeps 200px. In a narrow panel the graph gives way first. Before this change, a narrow panel could leave the Message column at zero width and hide every summary.
  • Clipped lanes read as clipped. Painting is masked to the column, lanes fade over the last 40px when the column is cut short, and a commit whose lane is off the edge is pinned there with a › marker and a dimmed avatar.
  • Worktree side lane is local. A pending-changes row that needs a side lane now takes the first lane past its own HEAD row, not past the whole graph, so it doesn't drag the column wide near the top of a big history.
  • Cleanup: removed the unused show_absolute_dates field (and its #[allow(dead_code)]) from WorkingTreeRowParams, and replaced the repeated lane-position arithmetic with lane_center_x.

New string: graph.resize_hint, the handle's tooltip (English and zh-CN).

Testing

  • cargo fmt --all -- --check, cargo clippy --workspace --all-targets -- -D warnings, cargo test --workspace: all pass.
  • New unit tests cover row lane extent, lanes in view (including worktree rows), cap and minimum width, the message-column budget, node pinning, fade alpha, and the local worktree side lane.
  • Checked by hand in an isolated debug instance against a synthetic repo: 40 linear commits on top of a 30-way octopus merge. The linear section shows an 80px column; scrolling into the octopus grows the column to the cap with clipped lanes and pinned nodes; scrolling back shrinks it; dragging the header widens it; with the diff panel open in a narrow window, the graph shrinks and messages stay readable.

Review fixes

From the Codex review of aa76ab3, fixed in 9b4feba:

  • Edges crossing the clip now fade. One opacity per path cut these edges off at full strength, so a line that reaches the fade paints with a horizontal gradient. Confirmed on screen on Windows with a 13-way octopus, where only the edge to lane 13 crosses the cap. Where about 30 edges overlap on one octopus row, their faint ends stack back up to near-opaque, so that row still ends sharply.
  • Merge edges and the worktree parent connector fade too. Merge edges were overwriting the fade with a fixed 0.85, and the connector had no fade at all. Neither was in the review; they are the same bug.
  • Worktree rows keep their 200px. The allowance now stays while a worktree row is in the list, even with the Message column hidden.

Pinned avatars and drawing cost

  • Node dots no longer show through dimmed avatars. A pinned avatar is drawn at 70% opacity, so the dot beneath it showed as a smaller, darker circle. The dot is now painted only when avatars are off.
  • Less drawing per row. Edges wholly past the column are dropped before they are built, and straight stretches of lane are painted as quads rather than paths.

Measured with scroll-storm on a --profile perf build, taking turns between builds. Medians over 3 runs per build from one session:

Repo Metric main 9b4feba This PR
60-way octopus draw p95 5.77 ms 6.48 ms 5.77 ms
60-way octopus frame p95 18.5 ms 20.0 ms 19.6 ms
60-way octopus dropped frames 35 78 40
medium corpus draw p95 5.49 ms 6.13 ms 5.43 ms
medium corpus frame p95 11.7 ms 11.7 ms 11.5 ms
medium corpus dropped frames 12 8 3

There are no regressions, and these differences are within run-to-run noise: on the wide repo, dropped frames for this PR ranged from 15 to 69 across runs. compare flagged peak working set on the medium corpus, but that is noise too, since main alone ranges from 133 to 162 MB and private bytes do not move. Frame p95 is higher on the wide repo than on the medium corpus on main as well, so this PR does not cause that gap.

🤖 Generated with Claude Code

https://claude.ai/code/session_01F1GjMp82mLDdAv3LEhyEB8

The graph column was as wide as the busiest row anywhere in the loaded
history. One wide stretch far down a big repository left every row with
hundreds of pixels of empty gutter and squeezed the message column, and
loading more commits only ever made it wider.

The column now fits the lanes the rows on screen draw. It grows as soon
as wider rows scroll into view and shrinks once scrolling settles, so the
columns to its right don't shift back and forth mid-scroll. It never grows
past a cap (twelve lanes by default) that can be dragged from the Graph
header and is saved in settings, and it gives way so the message column
keeps at least 200px. Lanes beyond the edge are clipped and fade out, and
a commit whose lane is cut off is pinned to the edge with a chevron.

A pending-changes worktree row that needs a side lane now takes the first
lane past its own row rather than past the whole graph, so it no longer
sits far off to the right in a wide history.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F1GjMp82mLDdAv3LEhyEB8
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 2, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-04T08:35:28.455092Z 0071bb8 Manual request
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: aa76ab395b

ℹ️ 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".

Comment thread crates/rgitui_graph/src/lib.rs Outdated
f32::from(self.container_bounds.size.width),
show_author_column.then_some(author_width),
show_date_column.then_some(date_col_width),
show_subject_column,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reserve message space for worktree rows

When the subject column is disabled and a dirty-worktree row is present, passing show_subject_column here removes the 200px message allowance even though render_working_tree_row still unconditionally renders its worktree name, status, and change badges in a flex_1 message column. On panels where the graph can approach its cap, the graph therefore consumes that space and collapses the pending-changes summary; either retain the allowance for these rows or hide their message consistently.

Useful? React with 👍 / 👎.

Comment thread crates/rgitui_graph/src/lib.rs Outdated
Comment on lines +2818 to +2820
color.a *= clipped_lane_alpha(
f32::from(from_x.min(to_x)),
graph_width,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Fade edges that cross the clipping boundary

For a merge or outgoing edge connecting a visible lane to an off-screen lane, taking min(from_x, to_x) evaluates the alpha at the visible endpoint, normally producing full opacity. Because the whole path then uses that single color, it reaches the content mask at full opacity and is abruptly cut off instead of fading over the final 40px, defeating the clipped-lane cue for these common diagonal edges.

Useful? React with 👍 / 👎.

noahbclarkson and others added 2 commits November 4, 2026 19:34
- An edge from a visible lane to one past the column's edge took its
  opacity from the visible end, so it reached the mask at full strength
  and was cut off. Taking it from the far end would dim the whole line
  instead. A path has one background, so a line that reaches the fade
  now paints with a horizontal gradient stopped at the fade zone. The
  shaders resolve path gradients over the path's clipped bounds on the
  DirectX, Metal and wgpu renderers, so the stops are computed over the
  part of the line inside the column. Lines short of the fade stay solid.
- Merge edges set their alpha to a fixed 0.85, which threw away the fade
  altogether. It now scales the lane colour instead.
- A worktree row's connector to its parent never faded, so a parent lane
  past the edge had the same hard cut. It uses the same background.
- Hiding the Message column gave its 200px to the graph, but worktree
  rows render their summary in a message column regardless, so the graph
  could collapse it. The allowance now stays while a worktree row is in
  the list.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F1GjMpzaaTdqoW5vq5c8Sy
- A pinned avatar is drawn at 70% opacity, so the node dot painted beneath
  it showed through as a smaller, darker circle. An avatar always covers
  the dot, so the dot is now painted only when avatars are off.
- Edges wholly past the column's right edge were cloned and tessellated
  every frame and then masked away. They are now dropped before any of
  that, which in a wide history is most of a row's edges.
- Straight stretches of lane are painted as quads, skipping tessellation
  and the multisampled path pass. This covers the commit rows and the
  worktree row's pass-through lanes.

Benchmarked with rgitui_perf's scroll-storm on a perf-profile build, on a
60-way octopus repo and the medium corpus, against main and
9b4feba. There are no regressions, but the gains are within
run-to-run noise: draw p95 on the wide repo matches main at 5.77ms, and
dropped frames range from 15 to 69 across runs of one build. The flagged
working-set increase on the medium corpus is noise as well: main itself
ranges from 133 to 162 MB, and private bytes do not move.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F1GjMpzaaTdqoW5vq5c8Sy
@noahbclarkson

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0071bb8869

ℹ️ 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".

Comment on lines +172 to +173
if x > last_x {
(last_x, true)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Normalize gradient stops to the full path span

When an edge runs from a visible lane to an off-screen lane—for example, x=30 to x=500 with the column ending at x=270—clamping right to 270 normalizes the fade over only 240px, but GPUI applies the content mask separately from the original path geometry. The resulting fade-start stop is about 0.83 along the full 470px path (x≈422), so the mask clips it at x=270 before the gradient begins. The fresh gradient implementation therefore still leaves cross-boundary edges fully opaque; normalize stops against the full path span or clip the path geometry first.

Useful? React with 👍 / 👎.

@noahbclarkson
noahbclarkson merged commit cd9c4b4 into main Nov 4, 2026
4 checks passed
noahbclarkson added a commit that referenced this pull request Nov 5, 2026
Changelog entry for everything since 0.5.0: #57, #89-#92 (already drafted
under [Unreleased]), plus new entries for #93, #95 and #98. The version is
bumped across the workspace, and the AppStream metainfo lists 0.5.1, which the
Linux metadata test requires.

The performance numbers are the ones measured in #95 and #98 with the
rgitui_perf scenarios on --profile perf builds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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