commit 40397a4342d58a3cdbafac60f5da7e0355e60637
parent c4d751181102f2d6ca8243a376d2add3821159ab
Author: Bharatvaj Hemanth <bharatvaj@yahoo.com>
Date: Tue, 13 Oct 2026 22:22:55 +0530
parent c4d751181102f2d6ca8243a376d2add3821159ab
Author: Bharatvaj Hemanth <bharatvaj@yahoo.com>
Date: Tue, 13 Oct 2026 22:22:55 +0530
Preserve list selection and viewport during stream updates
3 files changed, 47 insertions(+), 11 deletions(-)
diff --git a/HACKING b/HACKING @@ -111,8 +111,10 @@ Known simplifications not score-ranked). No silent row cap. LB_SETCOUNT is wrapped with redraw suppression: unchanged visible rows are not invalidated as the row count grows; only newly visible rows are, and those are painted immediately so - queued stream messages cannot starve WM_PAINT. The bottom bar shows search - progress after stdin completes. + queued stream messages cannot starve WM_PAINT. Restore the selection and + top row after LB_SETCOUNT (Windows can otherwise jump to the end), and + preserve pending selection paints when validating the viewport. The bottom + bar shows search progress after stdin completes. - lnav inspiration: its logfile index tracks lines separately from the view (https://github.com/tstack/lnav/blob/master/src/logfile.hh, lf_index and read_line); its text view requests counts and rows from a text sub-source
diff --git a/test_stream.c b/test_stream.c @@ -4,9 +4,14 @@ #include <assert.h> static int draw_calls; +static bool painted_selected[16]; static LRESULT CALLBACK TestWndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) { - if (msg == WM_DRAWITEM) ++draw_calls; + if (msg == WM_DRAWITEM) { + DRAWITEMSTRUCT *draw = (DRAWITEMSTRUCT *)lp; + ++draw_calls; + if (draw->itemID < 16) painted_selected[draw->itemID] = (draw->itemState & ODS_SELECTED) != 0; + } return WndProc(hwnd, msg, wp, lp); } @@ -55,6 +60,18 @@ int main(int argc, char **argv) } refill_list(""); assert(SendMessageW(list_hwnd, LB_GETCOUNT, 0, 0) == 60000); + SendMessageW(list_hwnd, LB_SETCURSEL, 0, 0); + UpdateWindow(list_hwnd); + list_move(5); + add_item("extra-row"); + refill_list(""); /* Count update before the selection's WM_PAINT. */ + UpdateWindow(list_hwnd); + assert(SendMessageW(list_hwnd, LB_GETCURSEL, 0, 0) == 5); + assert(SendMessageW(list_hwnd, LB_GETTOPINDEX, 0, 0) == 0); + assert(!painted_selected[0] && painted_selected[5]); + list_move(-5); + assert(SendMessageW(list_hwnd, LB_GETCURSEL, 0, 0) == 0); + assert(painted_selected[0] && !painted_selected[5]); SendMessageW(list_hwnd, LB_SETCURSEL, 59999, 0); char *last = current_text(); assert(last && !strncmp(last, "item-59999", 10)); @@ -92,14 +109,18 @@ int main(int argc, char **argv) DeleteDC(dc); ReleaseDC(NULL, screen); refill_list(""); - assert(SendMessageW(list_hwnd, LB_GETCOUNT, 0, 0) == 60000); + assert(SendMessageW(list_hwnd, LB_GETCOUNT, 0, 0) == 60001); ShowWindow(main_hwnd, SW_SHOWNOACTIVATE); ShowWindow(list_hwnd, SW_SHOWNOACTIVATE); UpdateWindow(list_hwnd); assert(!GetUpdateRect(list_hwnd, NULL, FALSE)); + SendMessageW(list_hwnd, LB_SETCURSEL, 59999, 0); + int kept_top = (int)SendMessageW(list_hwnd, LB_GETTOPINDEX, 0, 0); add_item("item-60000"); refill_list(""); - assert(SendMessageW(list_hwnd, LB_GETCOUNT, 0, 0) == 60001); + assert(SendMessageW(list_hwnd, LB_GETCOUNT, 0, 0) == 60002); + assert(SendMessageW(list_hwnd, LB_GETCURSEL, 0, 0) == 59999); + assert(SendMessageW(list_hwnd, LB_GETTOPINDEX, 0, 0) == kept_top); RECT dirty; assert(!GetUpdateRect(list_hwnd, &dirty, FALSE) || IsRectEmpty(&dirty)); /* Count/scrollbar may update, but the existing client rows stay untouched. */ @@ -108,7 +129,7 @@ int main(int argc, char **argv) input_done = false; add_item("tail-special"); while (search_pos < item_count) search_step(); - assert(match_count == 1 && matches[0] == 60001); + assert(match_count == 1 && matches[0] == 60002); input_done = true; update_progress(); assert(SendMessageW(list_hwnd, LB_GETCOUNT, 0, 0) == 1);
diff --git a/wmenu.c b/wmenu.c @@ -141,24 +141,29 @@ static void update_count(void) int selected = (int)SendMessageW(list_hwnd, LB_GETCURSEL, 0, 0); int top = (int)SendMessageW(list_hwnd, LB_GETTOPINDEX, 0, 0); int height = (int)SendMessageW(list_hwnd, LB_GETITEMHEIGHT, 0, 0); - RECT client; + RECT client, pending; + bool had_pending = GetUpdateRect(list_hwnd, &pending, FALSE) && !IsRectEmpty(&pending); GetClientRect(list_hwnd, &client); SendMessageW(list_hwnd, WM_SETREDRAW, FALSE, 0); SendMessageW(list_hwnd, LB_SETCOUNT, count, 0); - if (count && selected < 0) SendMessageW(list_hwnd, LB_SETCURSEL, 0, 0); + if (count) { + SendMessageW(list_hwnd, LB_SETCURSEL, selected >= 0 && selected < count ? selected : 0, 0); + if (top > 0 || SendMessageW(list_hwnd, LB_GETTOPINDEX, 0, 0) != 0) + SendMessageW(list_hwnd, LB_SETTOPINDEX, top < count ? top : count - 1, 0); + } SendMessageW(list_hwnd, WM_SETREDRAW, TRUE, 0); /* LB_SETCOUNT may invalidate everything. Only newly visible rows need paint. */ RedrawWindow(list_hwnd, NULL, NULL, RDW_VALIDATE | RDW_NOCHILDREN); int new_top = (int)SendMessageW(list_hwnd, LB_GETTOPINDEX, 0, 0); if (top != new_top || count < old_count || height <= 0) { InvalidateRect(list_hwnd, NULL, FALSE); - UpdateWindow(list_hwnd); } else if (old_count < new_top + (client.bottom - client.top + height - 1) / height) { client.top += (old_count - new_top) * height; if (client.top < 0) client.top = 0; InvalidateRect(list_hwnd, &client, FALSE); - UpdateWindow(list_hwnd); /* Posted stream messages must not starve WM_PAINT. */ } + if (had_pending) InvalidateRect(list_hwnd, &pending, FALSE); + UpdateWindow(list_hwnd); /* Do not starve new rows or selection paints. */ } static void update_progress(void) @@ -271,12 +276,20 @@ static void list_move(int delta) if (!has_items) return; LRESULT count = SendMessageW(list_hwnd, LB_GETCOUNT, 0, 0); LRESULT cur = SendMessageW(list_hwnd, LB_GETCURSEL, 0, 0); + LRESULT previous = cur; if (count <= 0) return; if (cur < 0) cur = delta > 0 ? -1 : count; cur += delta; if (cur < 0) cur = 0; if (cur >= count) cur = count - 1; + if (cur == previous) return; SendMessageW(list_hwnd, LB_SETCURSEL, cur, 0); + RECT rect; + if (previous >= 0 && SendMessageW(list_hwnd, LB_GETITEMRECT, previous, (LPARAM)&rect) != LB_ERR) + InvalidateRect(list_hwnd, &rect, FALSE); + if (SendMessageW(list_hwnd, LB_GETITEMRECT, cur, (LPARAM)&rect) != LB_ERR) + InvalidateRect(list_hwnd, &rect, FALSE); + UpdateWindow(list_hwnd); } static void edit_key(WORD key) { SendMessageW(input_hwnd, WM_KEYDOWN, key, 0); } @@ -455,7 +468,7 @@ static LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) case WM_DRAWITEM: { DRAWITEMSTRUCT *draw = (DRAWITEMSTRUCT *)lp; if (draw->CtlID != ID_COMBO || draw->itemID == (UINT)-1) break; - bool selected_row = (draw->itemState & ODS_SELECTED) != 0; + bool selected_row = (int)draw->itemID == (int)SendMessageW(list_hwnd, LB_GETCURSEL, 0, 0); FillRect(draw->hDC, &draw->rcItem, GetSysColorBrush(selected_row ? COLOR_HIGHLIGHT : COLOR_WINDOW)); EnterCriticalSection(&items_lock);
: nply/wmenu