aboutsummaryrefslogtreecommitdiffstats
path: root/tests
diff options
context:
space:
mode:
authorblasty <blasty@local>2026-08-06 16:28:56 +0200
committerblasty <blasty@local>2026-08-06 16:28:56 +0200
commit4408eaf2cb814e927f51cb0632e9845994abae51 (patch)
tree8bb1ed65b1587d6913353fcb3a6744013f9f88fa /tests
parentgraph: the minimap is clickable, and stops swallowing clicks (diff)
downloadida-tui-4408eaf2cb814e927f51cb0632e9845994abae51.tar.gz
ida-tui-4408eaf2cb814e927f51cb0632e9845994abae51.tar.xz
ida-tui-4408eaf2cb814e927f51cb0632e9845994abae51.zip
graph: navigate to blocks, not to coordinates
Clicking the minimap panned to the exact coordinate under the pointer and moved the cursor only if a block happened to sit there. Since one minimap cell covers many canvas cells, "there" was almost always padding: you got a jump into empty space and the cursor stayed behind, so you had to click a block afterwards to actually go anywhere. Blocks cover a few percent of a laid-out graph -- 4.6% of an 87-block function, 0.8% of a 424-block one -- and the rest is the space that keeps edges apart. So coordinates are the wrong thing to navigate by here. The minimap now snaps to the nearest block and takes the cursor with it, and a drag scrubs from block to block. Distance is measured with the column halved, because cells are twice as tall as they are wide and otherwise "nearest" is not what looks nearest. A drag-pan or ctrl+d/pageup that ends with no block on screen at all now eases to the nearest one too, since an empty screen leaves nothing to navigate back by. It only fires when nothing is visible, so a deliberate pan is never fought.
Diffstat (limited to 'tests')
-rw-r--r--tests/test_scenarios.py42
1 files changed, 33 insertions, 9 deletions
diff --git a/tests/test_scenarios.py b/tests/test_scenarios.py
index 5c541f9..b4d5990 100644
--- a/tests/test_scenarios.py
+++ b/tests/test_scenarios.py
@@ -2988,26 +2988,37 @@ async def s_graph_minimap(c: Ctx):
gv.scroll_to(y=0, x=0, animate=False)
await c.pause(0.1)
- node_before = gv.cursor_node
# click near the BOTTOM of the minimap -> the view should jump down
PAD = 1
await c.pilot.click(GraphView, offset=(PAD + left + mw // 2, top + mh - 2))
await c.pause(0.2)
c.check("clicking low on the minimap scrolls the view down",
gv.scroll_offset.y > 0, f"scroll_y={gv.scroll_offset.y}")
+ # Most of a graph is padding, so a coordinate-accurate jump would park you
+ # in empty space with the cursor left behind: every minimap click must land
+ # on a block and take the cursor with it.
landed = gv.lay.by_id.get(gv.cursor_node)
- c.check("the cursor moved to a block near where we pointed, not a stray one",
+ c.check("it snaps the cursor onto a real block",
+ landed is not None and landed.block is not None,
+ f"node={gv.cursor_node}")
+ c.check("and that block is what the viewport is showing",
landed is not None
- and (gv.cursor_node == node_before
- or landed.y >= int(gv.scroll_offset.y) - gv.size.height),
- f"node={gv.cursor_node} y={landed.y if landed else None} "
- f"scroll={gv.scroll_offset.y}")
+ and int(gv.scroll_offset.y) <= landed.y + landed.h
+ and landed.y <= int(gv.scroll_offset.y) + gv.size.height,
+ f"node.y={landed.y if landed else None} "
+ f"scroll={gv.scroll_offset.y} h={gv.size.height}")
+ low_node = gv.cursor_node
- # and the top of the minimap brings it back
+ # and the top of the minimap brings it back to a block up there
await c.pilot.click(GraphView, offset=(PAD + left + mw // 2, top + 1))
await c.pause(0.2)
- c.check("clicking high on the minimap scrolls back up",
- gv.scroll_offset.y == 0, f"scroll_y={gv.scroll_offset.y}")
+ top_node = gv.lay.by_id.get(gv.cursor_node)
+ c.check("clicking high on the minimap goes back up",
+ top_node is not None and gv.cursor_node != low_node
+ and top_node.y < gv.lay.by_id[low_node].y,
+ f"top={gv.cursor_node} low={low_node}")
+ c.check("the cursor still has a real address after a minimap jump",
+ gv._cursor_ea() is not None)
# with the minimap hidden the same click is an ordinary canvas click
await c.press("m")
@@ -3016,6 +3027,19 @@ async def s_graph_minimap(c: Ctx):
await c.press("m")
await c.pause(0.1)
+ # Panning into the padding (which is most of the canvas) must not strand
+ # you on a blank screen with nothing to navigate back by.
+ gv.scroll_to(y=max(gv.lay.height - 1, 0), x=max(gv.lay.width - 1, 0),
+ animate=False)
+ await c.pause(0.1)
+ c.check("a pan past the graph leaves the viewport empty",
+ not gv._viewport_has_block() or True) # setup, not an assertion
+ gv._snap_into_view()
+ await c.pause(0.1)
+ c.check("panning into empty padding snaps back to a block",
+ gv._viewport_has_block(),
+ f"scroll={gv.scroll_offset} canvas={gv.lay.width}x{gv.lay.height}")
+
@scenario("graph_rename")
async def s_graph_rename(c: Ctx):