diff options
| -rw-r--r-- | idatui/app.py | 35 | ||||
| -rw-r--r-- | tests/test_scenarios.py | 29 |
2 files changed, 38 insertions, 26 deletions
diff --git a/idatui/app.py b/idatui/app.py index 4b073af..446c364 100644 --- a/idatui/app.py +++ b/idatui/app.py @@ -1617,6 +1617,7 @@ class HexView(ScrollView, can_focus=True): super().__init__(id="hex") self.model = None self.total = 0 + self._internal_top: int | None = None # scroll target we set ourselves # -- public API -------------------------------------------------------- # def load(self, model, va: int | None = None) -> None: # type: ignore[no-untyped-def] @@ -1658,12 +1659,16 @@ class HexView(ScrollView, can_focus=True): def _apply_scroll(self, y: int) -> None: y = max(0, y) - self.scroll_to(y=y, animate=False) + if round(self.scroll_offset.y) != y: + self._internal_top = y # cursor-driven scroll incoming; don't follow it def _fix(yy: int = y) -> None: + if round(self.scroll_offset.y) != yy: + self._internal_top = yy self.scroll_to(y=yy, animate=False) self.refresh(layout=True) + self.scroll_to(y=y, animate=False) self.call_after_refresh(_fix) def _scroll_to_cursor(self, center: bool = False) -> None: @@ -1682,21 +1687,23 @@ class HexView(ScrollView, can_focus=True): def watch_scroll_y(self, old_value: float, new_value: float) -> None: super().watch_scroll_y(old_value, new_value) - # Follow the scroll: a wheel-scroll / scrollbar drag drags the byte cursor - # along so it stays visible (pinned to the nearest edge row, same column). - if round(old_value) != round(new_value): - self._follow_scroll(round(new_value)) + if round(old_value) == round(new_value): + return + nt = round(new_value) + if self._internal_top is not None and nt == self._internal_top: + self._internal_top = None # our cursor-driven scroll: cursor already placed + return + # A user scroll (wheel / scrollbar): drag the cursor by the same row delta + # so its screen position stays frozen (it points at a new byte in place). + self._internal_top = None + self._shift_cursor(nt - round(old_value)) - def _follow_scroll(self, top: int) -> None: - if self.model is None or self.model.size == 0: + def _shift_cursor(self, rows: int) -> None: + if not rows or self.model is None or self.model.size == 0: return - height = self._visible_height() - row, col = self.cursor // 16, self.cursor % 16 - new_row = min(max(row, top), top + max(height - 1, 0)) - new_row = max(0, min(new_row, max(self.total - 1, 0))) - new_cursor = max(0, min(new_row * 16 + col, self.model.size - 1)) - if new_cursor != self.cursor: - self.cursor = new_cursor + new = max(0, min(self.cursor + rows * 16, self.model.size - 1)) + if new != self.cursor: + self.cursor = new self.refresh() # cursor is reactive(repaint=False) self.post_message(HexView.Moved(self.cursor_va())) diff --git a/tests/test_scenarios.py b/tests/test_scenarios.py index 4731a16..1a07fb4 100644 --- a/tests/test_scenarios.py +++ b/tests/test_scenarios.py @@ -506,29 +506,34 @@ async def s_hex(c: Ctx): await c.wait(lambda: hx.cursor_va() == target_va, 15) c.check("'g' in the hex view jumps the cursor to an address", hx.cursor_va() == target_va, f"va={hx.cursor_va():#x} want={target_va:#x}") - # -- scroll the viewport, then click to move the byte cursor into it ---- + # -- a user scroll freezes the cursor's screen row (points at a new byte) -- await c.press("g") await c.type(hex(rng[0])) await c.press("enter") await c.wait(lambda: hx.cursor_va() == rng[0], 10) - hx.scroll_to(y=40, animate=False) # wheel-style scroll + for _ in range(8): # cursor to viewport row 8 (top still 0) + await c.press("j") + await c.pause(0.1) + top0 = round(hx.scroll_offset.y) + screen_row = hx.cursor // 16 - top0 + hx.scroll_to(y=top0 + 30, animate=False) # user scroll down 30 rows await c.pause(0.15) - top = round(hx.scroll_offset.y) - height = hx._visible_height() - c.check("hex cursor follows the scroll (stays visible)", - top >= 20 and top <= (hx.cursor // 16) < top + height, - f"top={top} cursor_row={hx.cursor // 16} height={height}") + top1 = round(hx.scroll_offset.y) + c.check("hex viewport scrolled", top1 >= top0 + 20, f"top0={top0} top1={top1}") + c.check("hex cursor's screen row stays frozen on scroll", + hx.cursor // 16 - top1 == screen_row, + f"screen_row={screen_row} now={hx.cursor // 16 - top1} top1={top1}") PAD = 1 # HexView { padding: 0 1 } -> content is inset one col await c.pilot.click(HexView, offset=(PAD + 19 + 3 * 3, 5)) # hex byte 3, row 5 await c.pause(0.1) - c.check("clicking the hex pane moves the cursor into the scrolled region", - hx.cursor == (top + 5) * 16 + 3, - f"cursor={hx.cursor} want={(top + 5) * 16 + 3} top={top}") + c.check("clicking the hex pane moves the cursor to the clicked byte", + hx.cursor == (top1 + 5) * 16 + 3, + f"cursor={hx.cursor} want={(top1 + 5) * 16 + 3} top1={top1}") await c.pilot.click(HexView, offset=(PAD + 70 + 10, 7)) # ascii byte 10, row 7 await c.pause(0.1) c.check("clicking the ascii pane maps to the right byte", - hx.cursor == (top + 7) * 16 + 10, - f"cursor={hx.cursor} want={(top + 7) * 16 + 10}") + hx.cursor == (top1 + 7) * 16 + 10, + f"cursor={hx.cursor} want={(top1 + 7) * 16 + 10}") await c.press("backslash") await c.wait(lambda: app._active != "hex", 10) c.check("backslash returns from hex to the code view", |
