: nply/wmenu

commit de4d213a66b669e4e4e3a58f407af6d33096fc1b
parent f0c7d6152842e3a30e95de2d8ee91888b3d4f277
Author: Bharatvaj Hemanth <bharatvaj@yahoo.com>
Date: Tue, 13 Oct 2026 22:09:59 +0530

Avoid repainting unchanged list rows as stream grows
3 files changed, 32 insertions(+), 3 deletions(-)
M
HACKING
|
4
+++-
M
test_stream.c
|
12
+++++++++++-
M
wmenu.c
|
19
++++++++++++++++++-
diff --git a/HACKING b/HACKING
@@ -107,7 +107,9 @@ Known simplifications
   thread, yields after each SEARCH_BATCH, and resumes through WM_SEARCH_STEP.
   It never yields while holding items_lock. The reader remains a separate
   thread. Matching rows are indexed in source order (fzy subsequence matching,
-  not score-ranked). No silent row cap. The bottom bar shows search progress
+  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. 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
diff --git a/test_stream.c b/test_stream.c
@@ -78,12 +78,22 @@ int main(int argc, char **argv)
     ReleaseDC(NULL, screen);
     refill_list("");
     assert(SendMessageW(list_hwnd, LB_GETCOUNT, 0, 0) == 60000);
+    ShowWindow(main_hwnd, SW_SHOWNOACTIVATE);
+    ShowWindow(list_hwnd, SW_SHOWNOACTIVATE);
+    UpdateWindow(list_hwnd);
+    assert(!GetUpdateRect(list_hwnd, NULL, FALSE));
+    add_item("item-60000");
+    refill_list("");
+    assert(SendMessageW(list_hwnd, LB_GETCOUNT, 0, 0) == 60001);
+    RECT dirty;
+    assert(!GetUpdateRect(list_hwnd, &dirty, FALSE) || IsRectEmpty(&dirty));
+    /* Count/scrollbar may update, but the existing client rows stay untouched. */
     refill_list("tail-special");
     search_step();
     input_done = false;
     add_item("tail-special");
     while (search_pos < item_count) search_step();
-    assert(match_count == 1 && matches[0] == 60000);
+    assert(match_count == 1 && matches[0] == 60001);
     input_done = true;
     update_progress();
     assert(SendMessageW(list_hwnd, LB_GETCOUNT, 0, 0) == 1);
diff --git a/wmenu.c b/wmenu.c
@@ -136,10 +136,27 @@ static int row_item(int row)
 static void update_count(void)
 {
     int count = search_query && *search_query ? match_count : item_count;
-    if (SendMessageW(list_hwnd, LB_GETCOUNT, 0, 0) == count) return;
+    int old_count = (int)SendMessageW(list_hwnd, LB_GETCOUNT, 0, 0);
+    if (old_count == count) return;
     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;
+    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);
+    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);
+    } 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);
+    }
 }
 
 static void update_progress(void)