Add hover-select mode to listview widget - #2296
Conversation
Implements viewport-based scrolling for mouse-driven usage where scroll events move the viewport without changing selection, and the item under the cursor is automatically selected. Adds hover_select flag,mouse position tracking, viewport offset management, and position sync logic.
There was a problem hiding this comment.
Pull request overview
This PR adds a “hover-select” interaction mode to the listview widget, where mouse-wheel scrolling moves the viewport without changing selection, and the item under the cursor becomes selected automatically. It also adjusts scrollbar behavior in hover-select mode and attempts to reduce hover update lag.
Changes:
- Introduces
hover_selectstate, mouse position tracking, and a separateviewport_offsetfor scroll position. - Updates mouse scroll handling to advance
viewport_offsetand re-evaluate hovered selection. - Changes scrollbar handle mapping to track
viewport_offsetwhen hover-select is enabled, and adds a (currently unused) hover-update callback API.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
| source/widgets/listview.c | Adds hover-select state, viewport offset scrolling, hover-from-mouse selection update, and scrollbar mapping changes. |
| source/view.c | Wires hover-select config into listview and registers a hover-update callback. |
| include/widgets/listview.h | Exposes new hover-select setter and hover-update callback API/docs. |
Comments suppressed due to low confidence (4)
source/widgets/listview.c:831
listview_element_motion_notifystoresx/yintolv->mouse_x/mouse_y, but these coordinates are relative to the hovered element widget (view.c converts to relative before callingmotion_notify).listview_update_hover_from_mouselater interpretsmouse_yas listview-relative, which can make hover-update-after-scroll compute the wrong row (often negative after subtracting padding) unless the mouse moved again. Consider either removing these assignments here and only tracking mouse coordinates inlistview_find_mouse_target(listview-relative), or translating element-relative coordinates into listview coordinates before storing.
static gboolean listview_element_motion_notify(widget *wid, gint x, gint y) {
listview *lv = (listview *)wid->parent;
lv->mouse_x = x;
lv->mouse_y = y;
lv->mouse_hovering = TRUE;
source/widgets/listview.c:860
listview_update_hover_from_mousecomputesidxaslast_offset + row, which ignores column layouts andreverseplacement. In multi-column layouts or whenreverseis enabled, this can select a different item than the one actually under the cursor after scrolling. Consider deriving the hovered index from bothmouse_xandmouse_yusing the same layout math as draw/positioning (row+column), and account forreversewhen mapping y to row.
unsigned int row = rel_y / item_height;
unsigned int max = MIN(lv->cur_elements, lv->req_elements - lv->last_offset);
if (row < max) {
unsigned int idx = lv->last_offset + row;
if (idx != listview_get_selected(lv)) {
source/widgets/listview.c:501
- Mapping the scrollbar handle to
viewport_offsetupdates the visual position, but scrollbar interactions still calllistview_set_selected()(see scrollbar.c), which does not updateviewport_offset. In hover-select mode, scrolling is driven byviewport_offset, so dragging/clicking the scrollbar will not actually move the viewport. Consider adding a listview API for setting viewport offset and using it from the scrollbar whenhover_selectis active (or updatingviewport_offsetinlistview_set_selectedwhen in hover-select mode).
if (lv->hover_select) {
unsigned int max_offset = lv->req_elements > lv->max_elements
? lv->req_elements - lv->max_elements
: 0;
scrollbar_set_max_value(lv->scrollbar, max_offset);
scrollbar_set_handle_length(lv->scrollbar, 1);
scrollbar_set_handle(lv->scrollbar, lv->viewport_offset);
} else {
scrollbar_set_max_value(lv->scrollbar, lv->req_elements);
scrollbar_set_handle_length(lv->scrollbar, lv->cur_columns * lv->max_rows);
if (lv->reverse) {
scrollbar_set_handle(lv->scrollbar, lv->req_elements - lv->selected - 1);
} else {
scrollbar_set_handle(lv->scrollbar, lv->selected);
}
source/widgets/listview.c:1330
hover_update_callback/hover_update_udataare stored vialistview_set_hover_update_callback, but the callback is never invoked anywhere in listview. This makes the new API (and view.c wiring) effectively dead code and the header doc misleading. Either call the callback at the appropriate points (e.g., after changingviewport_offset/ scroll operations) or remove the callback fields and setter until needed.
void listview_set_hover_update_callback(listview *lv, listview_hover_update_cb cb, void *udata) {
if (lv) {
lv->hover_update_callback = cb;
lv->hover_update_udata = udata;
}
}
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| scrollbar_set_max_value(lv->scrollbar, max_offset); | ||
| scrollbar_set_handle_length(lv->scrollbar, 1); | ||
| scrollbar_set_handle(lv->scrollbar, lv->viewport_offset); |
There was a problem hiding this comment.
Thanks for the thorough analysis. I looked into this and decided not to change it because:
-
When
req_elements <= max_elements, all items fit on screen — there's nothing to scroll, so the scrollbar is cosmetic only. Thehandle_length=1renders a minimal handle which is fine for this case. -
The scrollbar widget internally guards against division by zero —
scrollbar_draw()uses(length - 1)only when length > 1. -
For the normal case (
max_offset > 0),handle_length=1means the handle represents 1 visible page worth of items, which matches the viewport-based model where each scroll step is one item.
That said, if you'd prefer I adjust the handle sizing, happy to follow your guidance.
| } | ||
|
|
||
| static void hover_update_callback(void *udata) { | ||
| (void)udata; |
There was a problem hiding this comment.
We use G_GNUC_UNUSED for this.
https://docs.gtk.org/glib/macros.html
There was a problem hiding this comment.
Fixed — removed the callback entirely as it was dead code (hover re-evaluation is already handled by listview_update_hover_from_mouse in the scroll handlers).
Calling listview_update_hover_from_mouse inside listview_draw created a feedback loop: draw -> hover update -> set_selected -> queue_redraw -> draw. Mouse movement already handles selection updates via listview_element_motion_notify.
Scrollbar now tracks viewport position instead of selected item when hover-select is enabled. Sets scrollbar max to max_offset so it reaches the bottom when scrolled fully.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (5)
source/widgets/listview.c:489
- In hover-select mode
scrollbar_set_max_value(lv->scrollbar, max_offset)can setsb->lengthto 1 whenmax_offset==0(because scrollbar clamps to at least 1).scrollbar_draw()divides by(sb->length - 1)without guarding for length==1, which results in a division-by-zero (inf/NaN) and can break rendering. Ensure the value passed toscrollbar_set_max_valueis >= 2 (or change the scrollbar range to representmax_offset + 1positions), or add a guard in the scrollbar widget to handle length==1 safely.
? lv->req_elements - lv->max_elements
: 0;
scrollbar_set_max_value(lv->scrollbar, max_offset);
scrollbar_set_handle_length(lv->scrollbar, 1);
scrollbar_set_handle(lv->scrollbar, lv->viewport_offset);
source/widgets/listview.c:827
listview_element_motion_notifyreceives coordinates relative to the hovered element (seewidget_xy_to_relativeusage inrofi_view_handle_mouse_motion), but it stores them directly intolv->mouse_x/mouse_y.listview_update_hover_from_mouselater treats these as listview-relative coordinates, so scroll-triggered hover updates can select the wrong row. Convert(x,y)to listview-relative coordinates (e.g., add the element widget'sx/yoffsets, or store absolute coordinates fromlistview_find_mouse_targetonly) before saving.
static gboolean listview_element_motion_notify(widget *wid, gint x, gint y) {
listview *lv = (listview *)wid->parent;
lv->mouse_x = x;
lv->mouse_y = y;
lv->mouse_hovering = TRUE;
source/widgets/listview.c:856
listview_update_hover_from_mousemaps the mouse position to a row index using onlyrel_y / item_height. This ignoresmouse_xand the current packing mode/column count, so in multi-column layouts (or horizontal packing) the item under the cursor can be misidentified. Consider computing both row+column (usingmouse_x) or reusing the existing per-element hit testing logic to find the box under the cursor and then derive the selected index.
unsigned int row = rel_y / item_height;
unsigned int max = MIN(lv->cur_elements, lv->req_elements - lv->last_offset);
if (row < max) {
unsigned int idx = lv->last_offset + row;
if (idx != listview_get_selected(lv)) {
source/widgets/listview.c:770
- In hover-select mode, wheel scrolling increments
viewport_offsetby 1 element unconditionally. For horizontal packing, the existing scroll logic (scroll_continious_rows) scrolls in row-sized steps (menu_columnselements) to preserve grid alignment, andmax_offsetshould be computed in row terms as well. Update the step size and max calculation based onpack_direction/menu_columnsso horizontal layouts don’t shift the grid by partial rows.
unsigned int max_offset = lv->req_elements > lv->max_elements
? lv->req_elements - lv->max_elements
: 0;
if (lv->viewport_offset < max_offset) {
lv->viewport_offset++;
source/widgets/listview.c:866
listview_sync_positionsduplicates substantial layout logic fromlistview_draw(offset calculation, spacing/width math, widget_move/resize loop). This increases the risk of future divergence and does extra work on every scroll tick even though scrolling only needslast_offsetto be updated before callinglistview_update_hover_from_mouse. Consider extracting a shared helper (or makinglistview_sync_positionsonly updatelast_offset/state) to reduce duplication and per-scroll overhead.
static void listview_sync_positions(listview *lv) {
unsigned int offset = 0;
if (lv->scroll_type == LISTVIEW_SCROLL_PER_PAGE) {
offset = scroll_per_page(lv);
} else if (lv->pack_direction == ROFI_ORIENTATION_VERTICAL) {
- Use G_GNUC_UNUSED for unused parameter in hover_update_callback (maintainer feedback) - Remove unused page_udata field from listview struct - Remove dead hover_update_callback/udata fields and registration (hover re-evaluation already handled by listview_update_hover_from_mouse in scroll handlers)
|
I works well (did a quick test), except for clicking on the scrollbar, that is now broken? |
let me look into it |
fixed it ,the issue was that clicking/dragging the scrollbar called listview_set_selected which only changed the selection index but never updated viewport_offset so the list appeared not to scroll. I added listview_scrollbar_scroll_to that properly updates viewport_offset, syncs positions, and redraws in hover-select mode "><also bundled a few UX improvements while at it: jump on press instead of release, page up/down on track clicks, pressed-state handle color, auto-hide when everything fits, and a wider click target. |
- Fix scrollbar clicks scrolling viewport instead of changing selection in hover-select mode (Bug 1) - Fix coordinate mismatch between draw and click calculations; use remaining height and account for padding (Bug 3) - Fix division by zero when sb->length <= 1 - Jump on press, not release: MOUSE_CLICK_DOWN scrolls immediately - Page up/down on track clicks above/below handle - Add handle-pressed-color for visual feedback on press - Auto-hide scrollbar when all items fit - Widen scrollbar click target via left padding (6px) and larger default (14px) - Double the gap between list items and scrollbar
|
Thanks! |
Would you consider merging it, or are there still issues that need to be addressed first? |
|
I've asked others to give it a quick test. To see if they have remarks. |
|
With |
Summary
Changes
hover_selectflag, mouse position tracking, viewport offset managementlistview_update_hover_from_mousefromlistview_draw(was causing feedback loop)Test plan
rofi -show drun -hover-select— hover should be smooth, no lag