diff options
| author | blasty <blasty@local> | 2026-07-24 15:42:20 +0200 |
|---|---|---|
| committer | blasty <blasty@local> | 2026-07-24 15:42:20 +0200 |
| commit | 3835eb2aa35362401fc6489900394e022baa6981 (patch) | |
| tree | 150c70cc7dbb0b0f8d7ff415f96c9cdfe7a533fc | |
| parent | docs: refresh README for the worker-only architecture (diff) | |
| download | ida-tui-3835eb2aa35362401fc6489900394e022baa6981.tar.gz ida-tui-3835eb2aa35362401fc6489900394e022baa6981.tar.xz ida-tui-3835eb2aa35362401fc6489900394e022baa6981.zip | |
hex: click to place the byte cursor (hex + ascii panes)
HexView subclasses ScrollView directly and had no mouse handler, so clicking
never moved the byte cursor -- after a wheel-scroll the cursor was stuck off
screen with no way to reposition it by mouse. Add on_click: map the content
offset (scroll_offset + click, past the 1-col padding) to a row, and the x column
to a byte 0..15 across both the hex cells (3 cols each, +1 gap before byte 8) and
the ascii pane. Double-click jumps to code, mirroring Enter.
Pilot scenario extends `hex`: scroll the viewport (cursor goes off-screen), then
click in the hex pane and the ascii pane and assert the cursor lands on the exact
clicked row+byte. 9/9 hex checks pass.
| -rw-r--r-- | idatui/app.py | 33 | ||||
| -rw-r--r-- | tests/test_scenarios.py | 22 |
2 files changed, 55 insertions, 0 deletions
diff --git a/idatui/app.py b/idatui/app.py index 07325a1..5314f66 100644 --- a/idatui/app.py +++ b/idatui/app.py @@ -1712,6 +1712,39 @@ class HexView(ScrollView, can_focus=True): def action_leave(self) -> None: self.post_message(HexView.Leave()) + # -- mouse ------------------------------------------------------------- # + def _byte_at_x(self, x: int) -> int: + """Map a content column to a byte index 0..15, across the hex and ASCII + panes. Matches ``render_line``'s layout: addr(9) + file-offset(10) + 16 + hex cells of 3 cols (with a 1-col gap before byte 8), then ' |' + ASCII.""" + HEX, ASCII = 19, 70 + if x < HEX: # clicked the address/offset gutter -> row start + return 0 + if x < HEX + 49: # hex byte region + rel = x - HEX + if rel >= 24: # collapse the 1-col gap between the two halves + rel -= 1 + return min(rel // 3, 15) + if x < ASCII: # the ' |' separator -> last byte of the row + return 15 + return min(x - ASCII, 15) # ASCII pane (and anything past it) + + def on_click(self, event) -> None: # type: ignore[no-untyped-def] + if self.model is None or self.model.size == 0: + return + off = event.get_content_offset(self) + if off is None: + return + self.focus() + row = round(self.scroll_offset.y) + off.y + x = round(self.scroll_offset.x) + off.x + new = row * 16 + self._byte_at_x(x) + self.cursor = max(0, min(self.model.size - 1, new)) + self.refresh() # cursor is reactive(repaint=False); repaint the highlight + self.post_message(HexView.Moved(self.cursor_va())) + if event.chain >= 2: # double-click == place cursor + jump to code + self.post_message(HexView.ToCode(self.cursor_va())) + # -- rendering --------------------------------------------------------- # def _ensure_window(self, top: int) -> None: if self.model is None: diff --git a/tests/test_scenarios.py b/tests/test_scenarios.py index 62f474d..92c6315 100644 --- a/tests/test_scenarios.py +++ b/tests/test_scenarios.py @@ -506,6 +506,28 @@ 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 ---- + 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: viewport only + await c.pause(0.15) + top = round(hx.scroll_offset.y) + c.check("hex viewport scrolls without dragging the cursor along", + top >= 20 and (hx.cursor // 16) < top, + f"top={top} cursor_row={hx.cursor // 16}") + 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}") + 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}") await c.press("backslash") await c.wait(lambda: app._active != "hex", 10) c.check("backslash returns from hex to the code view", |
