From 99516108f3f69f2c1a3e4f3d16820d800e86875a Mon Sep 17 00:00:00 2001 From: Amp Date: Fri, 13 Nov 2026 19:04:46 +0000 Subject: [PATCH] fix(zbugs-rune): keep a new issue's row in view at the list's top Creating an issue opened it in the issue pane, but its row in the list stayed out of view, so no row showed highlighted: Rune's virtual rows hold the reader's top row in place when a new row comes above it, even at offset 0, so the new issue landed just above the viewport. An issue created or reopened in another app did the same. A reorder (an issue edited elsewhere rising to the top) slides rows without anchoring, so it never hid. The list now records its scroll offset (`El::on_scroll_offset`) and, at its top, scrolls back to 0 when new rows arrive, as zbugs-gpui's `keep_scroll_anchor` and zbugs-swift's list do. Scrolled down, it still holds its rows in place. e2e.sh creates an issue from the New Issue sheet with the list at its top and checks its row is on screen and highlighted; it fails without the fix (the row sits at y=0, above the list's viewport at y=53). Amp-Thread-ID: https://ampcode.com/threads/T-01a1d634-9fac-7561-a14a-d552061a006c Co-authored-by: tmm --- apps/zbugs-rune/e2e.sh | 33 ++++++++++++++++++++++++++++++++- apps/zbugs-rune/src/main.rs | 9 +++++++++ apps/zbugs-rune/src/view.rs | 4 ++++ 3 files changed, 45 insertions(+), 1 deletion(-) diff --git a/apps/zbugs-rune/e2e.sh b/apps/zbugs-rune/e2e.sh index baba612621..197d36498a 100755 --- a/apps/zbugs-rune/e2e.sh +++ b/apps/zbugs-rune/e2e.sh @@ -2,7 +2,8 @@ # zbugs-rune against zbugs' backend, end to end: the app runs headless on # the wall clock (`--realtime`), opens at the list's first issue with no # selection saved, steps away from an open issue and back with -# the arrow keys, then opens it, comments on it, reacts +# the arrow keys, creates an issue and finds its row highlighted at the +# list's top, then opens the issue it stepped to, comments on it, reacts # to it, labels it, closes it, edits the comment, creates a label for it, # makes it internal, and unsubscribes from it, pressing its controls as a # person would. Opening it marks it viewed. Then Postgres must have each @@ -55,6 +56,34 @@ ZBUGS_RUNE_DIR="$out/arrows" cargo run --locked --quiet -- --realtime \ arrows=$(node -e 'console.log(JSON.parse(require("fs").readFileSync(process.argv[1])).selection)' \ "$out/arrows/settings.json") +# A new issue, from the New Issue sheet with the list at its top: it opens, +# and its row must show highlighted at the top of the list, not above it +# out of view (where Rune keeps a new row that comes above the top one). +# It draws (--snap), as a window does: Rune holds the row it drew on top. +new_title="e2e-new-$(date +%s)" +mkdir -p "$out/created" +cat > "$out/created.json" </dev/null +created=$(sql "select id from issue where title = '$new_title'") +created_row=$(node -e ' + const els = JSON.parse(require("fs").readFileSync(process.argv[1])).elements; + const list = els.find(e => e.key === "list-scroll")?.rect; + const row = els.find(e => e.key === process.argv[2]); + const shown = list && row && row.rect[1] >= list[1] && row.rect[1] + row.rect[3] <= list[1] + list[3]; + console.log(shown ? (row.fill === "#00000000" ? "shown" : "shown, highlighted") : "out of view"); +' "$out/created-tree.json" "$created") +created_selection=$(node -e 'console.log(JSON.parse(require("fs").readFileSync(process.argv[1])).selection)' \ + "$out/created/settings.json") +sql "delete from issue where id = '$created'" + # The label picker lists the issue's project's labels by name, bytewise. first_label=$(sql "select l.name from label l join issue i on i.\"projectID\" = l.\"projectID\" where i.id = '$issue' order by l.name collate \"C\" limit 1") @@ -107,6 +136,8 @@ check() { } check "the first issue, with none saved" "$first" "$first_issue" check "the arrow keys" "$arrows" "$issue" +check "the new issue, open" "$created_selection" "$created" +check "the new issue's row" "$created_row" "shown, highlighted" check "the comment, edited" \ "$(sql "select count(*) from comment where \"issueID\" = '$issue' and body = '$body edited'")" 1 check "the reaction" \ diff --git a/apps/zbugs-rune/src/main.rs b/apps/zbugs-rune/src/main.rs index 2a2195eefb..f336d3256c 100644 --- a/apps/zbugs-rune/src/main.rs +++ b/apps/zbugs-rune/src/main.rs @@ -152,6 +152,8 @@ pub struct App { pub checked: Vec, /// Scroll the list to the selected issue once it's there (a relaunch). reveal_selected: bool, + /// How far down the list is scrolled, which the list's viewport sets. + pub list_offset: rune::Live, /// Select the list's first issue once it has rows: a launch with no /// selection to restore. select_first: bool, @@ -279,6 +281,7 @@ impl App { select_mode: false, checked: Vec::new(), reveal_selected: false, + list_offset: rune::Live::new(0.0), select_first: true, detail: None, detail_result: ResultType::Unknown, @@ -1158,6 +1161,12 @@ fn apply_issues(s: &mut App, rows: Vec, result_type: ResultType, tx: &mut { s.reveal_selected = false; tx.scroll_into_view(id.clone()); + } else if s.list_offset.get() < 0.5 { + // At the top, the list stays at the top, as zbugs-swift's and + // zbugs-gpui's do: Rune holds the top row in place when a new row + // comes above it, which left an issue just created, here or in + // another app, above the view while it showed selected. + tx.scroll_to_y("list-scroll", 0.0); } } diff --git a/apps/zbugs-rune/src/view.rs b/apps/zbugs-rune/src/view.rs index e67931cbcd..964caff03c 100644 --- a/apps/zbugs-rune/src/view.rs +++ b/apps/zbugs-rune/src/view.rs @@ -557,6 +557,7 @@ fn issue_list(s: &App, k: &Kit, layer: &mut Vec) -> El { .child(text(message).font(k.ty.ui).color(k.c.text_faint)) } else { let kit = k.clone(); + let offset = s.list_offset.clone(); col() .key("list-scroll") .w_full() @@ -565,6 +566,9 @@ fn issue_list(s: &App, k: &Kit, layer: &mut Vec) -> El { .scroll(ScrollSpec { stick_bottom: false, }) + .on_scroll_offset(move |at| { + offset.set(at.offset[1]); + }) .child( col() .key("issues")