aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorblasty <blasty@local>2026-07-09 16:22:58 +0200
committerblasty <blasty@local>2026-07-09 16:22:58 +0200
commit6ba52ae97fbcf0238b379fef17733425f7cd8706 (patch)
treeb2ca718afaf556796b90ef726d870fae51b48746
parentsmoother scroll restore on jump-back; non-degenerate test (diff)
downloadida-tui-6ba52ae97fbcf0238b379fef17733425f7cd8706.tar.gz
ida-tui-6ba52ae97fbcf0238b379fef17733425f7cd8706.tar.xz
ida-tui-6ba52ae97fbcf0238b379fef17733425f7cd8706.zip
fix stale top frame on jump-back; test the actual repaint
The pane could keep showing the top (scroll 0) after jumping back even though scroll_offset was correct: the deferred call_after_refresh set the scroll AFTER the paint and didn't force a repaint. Now the deferred callback re-applies the scroll AND refreshes, so the pane is repainted at the restored offset. Crucially, the prior test only checked scroll_offset.y (which was correct even when the bug was visible). Added a render-level assertion: trace render_line and assert the last repaint happened at the restored scroll. (Also: my earlier scratch repros used a non-existent function name so 'back' was a no-op -- the real suite test uses a valid second function.) pilot suite 53/53.
-rw-r--r--idatui/app.py5
-rw-r--r--tests/test_tui.py13
2 files changed, 17 insertions, 1 deletions
diff --git a/idatui/app.py b/idatui/app.py
index a5be0f7..b76e6c5 100644
--- a/idatui/app.py
+++ b/idatui/app.py
@@ -627,8 +627,11 @@ class DisasmView(SearchMixin, NavMixin, ColumnCursor, ScrollView, can_focus=True
target = min(self._pending_scroll_y, max(total - 1, 0))
# Apply now (works when the region is already sized) and again after
# refresh (covers the case where max_scroll_y isn't computed yet).
+ # The deferred call also forces a repaint, so the pane is never left
+ # showing a stale frame at the top with scroll_offset already moved.
self.scroll_to(y=target, animate=False)
- self.call_after_refresh(lambda t=target: self.scroll_to(y=t, animate=False))
+ self.call_after_refresh(
+ lambda t=target: (self.scroll_to(y=t, animate=False), self.refresh()))
else:
self._scroll_cursor_into_view()
self._pending_scroll_y = None
diff --git a/tests/test_tui.py b/tests/test_tui.py
index 51c9f57..884b1c3 100644
--- a/tests/test_tui.py
+++ b/tests/test_tui.py
@@ -508,6 +508,15 @@ async def run(db):
want_rel = want_cur - want_sy
await goto_name(fb.name)
await wait_until(pilot, lambda: app._cur.ea == fb.addr, timeout=20)
+ # Record the scroll at each repaint so we catch the "scroll_offset moved
+ # but the pane wasn't repainted" bug (which scroll_offset checks miss).
+ renders: list[int] = []
+ _orig_rl = dis.render_line
+ def _traced(y, _o=_orig_rl):
+ if y == 0:
+ renders.append(round(dis.scroll_offset.y))
+ return _o(y)
+ dis.render_line = _traced
await pilot.press("escape")
await wait_until(pilot, lambda: app._cur.ea == fa.addr, timeout=20)
await wait_until(pilot, lambda: dis.total > 40, timeout=20)
@@ -516,6 +525,10 @@ async def run(db):
round(dis.scroll_offset.y) == want_sy and dis.cursor == want_cur and want_rel > 0,
f"scroll={round(dis.scroll_offset.y)} (want {want_sy}) "
f"cursor={dis.cursor} (want {want_cur}) rel_before={want_rel}")
+ check("pane is repainted at the restored scroll (no stale top frame)",
+ bool(renders) and renders[-1] == want_sy,
+ f"last repaint scroll={renders[-1] if renders else None} (want {want_sy})")
+ dis.render_line = _orig_rl
# PageUp/Down keep the cursor at the same viewport-relative row.
await goto_name(fa.name)