Skip to content

[codex] Fix narrow passband drag hit testing#3523

Open
rfoust wants to merge 1 commit into
aethersdr:mainfrom
rfoust:codex/narrow-passband-drag
Open

[codex] Fix narrow passband drag hit testing#3523
rfoust wants to merge 1 commit into
aethersdr:mainfrom
rfoust:codex/narrow-passband-drag

Conversation

@rfoust

@rfoust rfoust commented Jun 11, 2026

Copy link
Copy Markdown
Collaborator

Root cause

SpectrumWidget::mousePressEvent() used a fixed 8 px grab zone for each filter edge before checking whether the click was inside the passband body. When the panadapter was zoomed far enough out, a narrow slice passband could render at only a few pixels wide, so the edge grab zones overlapped most or all of the passband. A click intended to drag the whole slice was then classified as a filter-edge drag and changed the passband width instead.

A second cursor-specific issue hid the intended affordance after the hit testing was corrected: the hover cursor path could be bypassed when VFO child widgets covered the passband area, and the QRhi render-time cursor polling path updated tracking overlays without also updating the OS cursor shape. That left users seeing the inherited crosshair, or a pointing-hand cursor from an overlaid VFO child button, instead of the resize cursor at the true filter edges.

Review also caught one ordering mismatch: the cursor helper checked the active slice before inactive-slice activation targets, while press handling activates inactive slice targets first. In overlapping-slice cases, that could make the cursor preview a different action than the click would perform.

User impact

Users with narrow RX filters on a zoomed-out panadapter can move a slice by dragging inside the passband instead of accidentally latching onto an edge. Edge resizing remains available near the passband edges.

The hover cursor now previews the action before clicking: horizontal resize at filter edges, open hand for passband-body moves, closed hand once the move drag is active, and existing pointing-hand behavior for actual slice/button activation targets.

Change summary

  • Added shared pixel-based passband hit-test helpers that preserve a minimum passband-body hit area for narrow rendered filters.
  • Updated the active-slice mouse press path to use that classifier for filter-edge drags versus whole-passband VFO drags.
  • Centralized slice cursor classification so hover and child-widget cursor handling use the same edge/body decision as press handling.
  • Matched cursor-target ordering to press handling by checking inactive-slice activation targets before active-slice filter targets.
  • Let VFO child widgets defer cursor affordance to SpectrumWidget when they overlap slice passband/edge zones, without consuming their clicks.
  • Updated the QRhi cursor-polling recovery path so it also refreshes the visible cursor shape over slice passband/edge zones.
  • Rebased onto current upstream/main and kept the new adjustable freqScaleH() layout math in the cursor helper and conflict resolution.

Validation

  • git diff --check
  • cmake --build build -j8 after rebasing onto upstream/main at 6a0648b5
  • python3 tools/check_a11y.py src/gui/SpectrumWidget.cpp src/gui/SpectrumWidget.h
  • ctest --test-dir build --output-on-failure --parallel 8 -E theme_manager_test passed 32/32 tests
  • Manual reporter retest confirmed the cursor behavior is improved

Copilot AI review requested due to automatic review settings June 11, 2026 03:17
@rfoust rfoust requested a review from a team as a code owner June 11, 2026 03:17
@rfoust rfoust self-assigned this Jun 11, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes hit-testing for very narrow rendered RX filter passbands in SpectrumWidget so dragging inside the passband reliably moves the slice (VFO drag) instead of accidentally initiating a filter-edge resize when zoomed out.

Changes:

  • Added shared pixel-based passband hit-test helpers to classify edge vs body interactions for narrow passbands.
  • Updated active-slice mousePressEvent() logic to use the new edge/body classifier.
  • Updated hover cursor edge detection in mouseMoveEvent() to use the same classifier.

Comment on lines +81 to +88
static int filterInteriorGrabPx(int loX, int hiX, int grabPx)
{
const int widthPx = std::abs(hiX - loX);
if (widthPx <= kFilterPassbandMinBodyPx) {
return 0;
}
return std::min(grabPx, std::max(1, (widthPx - kFilterPassbandMinBodyPx) / 2));
}
Comment thread src/gui/SpectrumWidget.cpp Outdated
Comment on lines 4311 to 4316
@@ -4273,7 +4312,7 @@
const int hiX = mhzToX(ao->freqMhz + ao->filterHighHz / 1.0e6);
constexpr int GRAB = 5;
if (!foundCursor
&& (std::abs(mx - loX) <= GRAB || std::abs(mx - hiX) <= GRAB)) {
&& filterEdgeHitAtPixel(mx, loX, hiX, GRAB) != 0) {
setSpectrumCursor(Qt::SizeHorCursor);
@rfoust rfoust force-pushed the codex/narrow-passband-drag branch 2 times, most recently from 24ea85c to d0f9b48 Compare June 11, 2026 03:49

@aethersdr-agent aethersdr-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @rfoust — this is a well-structured fix. Centralizing the edge/body classification so press, hover, the VFO-child event filter, and the QRhi polling path all agree is the right shape, and I verified the supporting details: setSpectrumCursor() already guards redundant installs (so the per-frame call in renderGpuFrame is cheap), VfoWidget builds all its children in buildUI() at construction (so installVfoCursorEventFilter catches them all), and the event filter never consumes events, so child buttons keep their clicks as described.

One real inconsistency — hover/press ordering for overlapping slices

sliceCursorShapeAt() checks the active overlay first, then inactive slices. mousePressEvent() does the opposite: the inactive-slice loop (badge → centerline → passband body, all emitting sliceClicked) runs before the active overlay's edge/body checks. So when an inactive slice's badge or passband overlaps the active slice's edge zone or passband — exactly the zoomed-out, slices-close scenario this PR targets — the cursor shows SizeHor/OpenHand (active-slice action) but the click activates the inactive slice instead. Since the whole point of the cursor work is previewing the action, suggest reordering sliceCursorShapeAt() to check inactive slices first, mirroring press order.

On the Copilot findings

  • "filterInteriorGrabPx forces insideGrabPx >= 1, leaving a 7px passband only 5px of body"mostly a false positive. There is no >= 1 clamp; for widthPx = 7, (7 - 6) / 2 == 0, so no interior pixels are stolen and the body is the full 6px. However, there is a genuine off-by-one for even widths: the edge zones include their boundary pixel (mx <= loX + insideGrabPx), so the body is widthPx − 2·insideGrabPx − 1 pixels — 5px instead of 6 for widths 8, 10, …, 22. If the ≥6px guarantee is meant to be strict, (widthPx - kFilterPassbandMinBodyPx - 1) / 2 fixes it. Minor either way.
  • "Hover still uses a 5px grab radius vs 8px on press"false positive. This PR deletes the old GRAB = 5 hover path; hover now goes through sliceCursorShapeAt()filterEdgeHitAtPixel() with the same kFilterEdgeGrabPx = 8 as press.

Two small notes

  1. The childAt() guard bypass in mouseMoveEvent means the #2355 tooltip protection no longer applies where overlay-menu buttons overlap passband/edge zones — QToolTip::hideText() paths can kill their tooltips again there. Probably an acceptable tradeoff for the cursor affordance, but worth being aware it partially reverts #2355 in those zones.
  2. The validation section says "Manual reporter retest confirmed the cursor behavior was better before this push" — as written that says the reporter preferred the previous behavior. Assuming that's a typo for "better than before this push", but please confirm.

No scope, settings-convention, or resource concerns — the diff matches the stated scope and the new helpers are pure functions.


🤖 aethersdr-agent · cost: $10.5651 · model: claude-fable-5

@rfoust rfoust force-pushed the codex/narrow-passband-drag branch from d0f9b48 to 697b28b Compare June 11, 2026 15:25
@rfoust

rfoust commented Jun 11, 2026

Copy link
Copy Markdown
Collaborator Author

Updated after the latest review and upstream rebase.

Addressed review feedback:

  • Reordered sliceCursorShapeAt() so inactive-slice badge, centerline, and passband targets are checked before active-slice filter targets. That now matches mousePressEvent() ordering, so the hover cursor previews the click action in overlapping-slice cases.
  • Tightened filterInteriorGrabPx() to use (widthPx - kFilterPassbandMinBodyPx - 1) / 2, preserving the intended minimum passband-body hit area with inclusive edge-hit bounds.
  • Rebased onto current upstream/main (6a0648b5) and resolved the SpectrumWidget conflict by keeping upstream freqScaleH() layout math in the mouse/cursor paths.
  • Updated the PR body wording and validation section so it no longer has the confusing “before this push” phrasing.

Notes on prior comments:

  • The two Copilot inline comments are now outdated. Hover and press both flow through sliceCursorShapeAt() / filterEdgeHitAtPixel() with the same 8 px edge grab classifier.
  • The SpectrumWidget::mouseMoveEvent kills tooltips on MEM+ and DAX sidebar buttons #2355 tooltip guard is still preserved outside slice passband/edge cursor targets. Inside those targets, the spectrum cursor intentionally wins so the resize/move affordance reflects the click behavior.

Validation on the rebased head 697b28ba:

  • git diff --check
  • cmake --build build -j8
  • python3 tools/check_a11y.py src/gui/SpectrumWidget.cpp src/gui/SpectrumWidget.h
  • ctest --test-dir build --output-on-failure --parallel 8 -E theme_manager_test passed 32/32

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants