Smooth Qt redraws during pane updates

ober

958ce03932b65622679be4b725e51b2142538f91

diff --git a/lib/jerboa-emacs/qt/buffer.sls b/lib/jerboa-emacs/qt/buffer.sls
index 8d338a3..d6b744e 100644
--- a/lib/jerboa-emacs/qt/buffer.sls
+++ b/lib/jerboa-emacs/qt/buffer.sls
@@ -41,23 +41,31 @@
   (def (qt-buffer-attach! editor buf (run-post-hook? #t))
        "Switch editor to display this buffer's document.\n   Re-applies the document's read-only state after swap because QScintilla\n   may have a widget-level readOnly flag that persists across document switches.\n   Runs post-buffer-attach-hook to handle image/text display toggling.\n   All visual changes are batched via setUpdatesEnabled to prevent flicker."
        (verbose-log! "qt-buffer-attach! buf=" (buffer-name buf))
-       (qt-widget-set-updates-enabled! editor #f)
-       (let ([doc (buffer-doc-pointer buf)])
-         (verbose-log! "qt-buffer-attach! SCI_SETDOCPOINTER begin")
-         (sci-send editor SCI_SETDOCPOINTER 0 doc)
-         (verbose-log! "qt-buffer-attach! SCI_SETDOCPOINTER done")
-         (doc-editor-register! doc editor)
-         (let ([ro (sci-send editor SCI_GETREADONLY)])
-           (sci-send editor SCI_SETREADONLY ro))
-         (let ([lang (buffer-lexer-lang buf)])
-           (qt-plain-text-edit-set-line-wrap!
-             editor
-             (not (or (eq? lang 'terminal) (eq? lang 'shell)))))
-         (when run-post-hook?
-           (verbose-log!
-             "qt-buffer-attach! post-buffer-attach-hook begin")
-           (run-hooks! 'post-buffer-attach-hook editor buf))
-         (verbose-log!
-           "qt-buffer-attach! done buf="
-           (buffer-name buf)))
-       (qt-widget-set-updates-enabled! editor #t)))
+       (let ([container (qt-widget-parent editor)])
+         (def (updates-enabled! enabled?)
+              (when container
+                (qt-widget-set-updates-enabled! container enabled?))
+              (qt-widget-set-updates-enabled! editor enabled?))
+         (updates-enabled! #f)
+         (with-catch
+           (lambda (e) (updates-enabled! #t) (raise e))
+           (lambda ()
+             (let ([doc (buffer-doc-pointer buf)])
+               (verbose-log! "qt-buffer-attach! SCI_SETDOCPOINTER begin")
+               (sci-send editor SCI_SETDOCPOINTER 0 doc)
+               (verbose-log! "qt-buffer-attach! SCI_SETDOCPOINTER done")
+               (doc-editor-register! doc editor)
+               (let ([ro (sci-send editor SCI_GETREADONLY)])
+                 (sci-send editor SCI_SETREADONLY ro))
+               (let ([lang (buffer-lexer-lang buf)])
+                 (qt-plain-text-edit-set-line-wrap!
+                   editor
+                   (not (or (eq? lang 'terminal) (eq? lang 'shell)))))
+               (when run-post-hook?
+                 (verbose-log!
+                   "qt-buffer-attach! post-buffer-attach-hook begin")
+                 (run-hooks! 'post-buffer-attach-hook editor buf))
+               (verbose-log!
+                 "qt-buffer-attach! done buf="
+                 (buffer-name buf)))
+             (updates-enabled! #t))))))
diff --git a/lib/jerboa-emacs/qt/sci-shim.sls b/lib/jerboa-emacs/qt/sci-shim.sls
index 61cb78a..217b5c5 100644
--- a/lib/jerboa-emacs/qt/sci-shim.sls
+++ b/lib/jerboa-emacs/qt/sci-shim.sls
@@ -115,29 +115,30 @@
    qt-timer-set-single-shot! qt-timer-start! qt-timer-stop!
    qt-toolbar-add-action! qt-toolbar-add-separator!
    qt-toolbar-create qt-toolbar-set-movable! qt-widget-close!
-   qt-widget-create qt-widget-destroy! qt-widget-font
-   qt-widget-height qt-widget-hide! qt-widget-resize!
-   qt-widget-set-focus! qt-widget-set-font-size!
-   qt-widget-set-maximum-height! qt-widget-set-minimum-height!
-   qt-widget-set-minimum-size! qt-widget-set-size-policy!
-   qt-widget-set-style-sheet! qt-widget-set-attribute!
-   qt-widget-set-updates-enabled! qt-widget-show!
-   qt-widget-show-fullscreen! qt-widget-show-maximized!
-   qt-widget-show-minimized! qt-widget-show-normal!
-   qt-widget-width qt-widget-window-state QT_MOD_SHIFT
-   QT_MOD_ALT QT_MOD_META QT_MOD_CTRL QT_KEY_ESCAPE
-   QT_KEY_BACKSPACE QT_KEY_RETURN QT_KEY_ENTER QT_KEY_DELETE
-   QT_KEY_TAB QT_KEY_INSERT QT_KEY_HOME QT_KEY_END QT_KEY_LEFT
-   QT_KEY_RIGHT QT_KEY_UP QT_KEY_DOWN QT_KEY_PAGE_UP
-   QT_KEY_PAGE_DOWN QT_KEY_SPACE QT_KEY_A QT_KEY_G QT_KEY_N
-   QT_KEY_P QT_KEY_R QT_KEY_S QT_KEY_W QT_KEY_Z QT_KEY_F1
-   QT_KEY_F2 QT_KEY_F3 QT_KEY_F4 QT_KEY_F5 QT_KEY_F6 QT_KEY_F7
-   QT_KEY_F8 QT_KEY_F9 QT_KEY_F10 QT_KEY_F11 QT_KEY_F12
-   QT_CURSOR_UP QT_CURSOR_DOWN QT_CURSOR_START QT_CURSOR_END
-   QT_CURSOR_START_OF_BLOCK QT_CURSOR_END_OF_BLOCK
-   QT_CURSOR_NEXT_CHAR QT_CURSOR_NEXT_WORD
-   QT_CURSOR_PREVIOUS_CHAR QT_CURSOR_PREVIOUS_WORD
-   qt-terminal-create qt-terminal-destroy! qt-terminal-spawn!
+   qt-widget-create qt-widget-parent qt-widget-destroy!
+   qt-widget-font qt-widget-height qt-widget-hide!
+   qt-widget-resize! qt-widget-set-focus!
+   qt-widget-set-font-size! qt-widget-set-maximum-height!
+   qt-widget-set-minimum-height! qt-widget-set-minimum-size!
+   qt-widget-set-size-policy! qt-widget-set-style-sheet!
+   qt-widget-set-attribute! qt-widget-set-updates-enabled!
+   qt-widget-show! qt-widget-show-fullscreen!
+   qt-widget-show-maximized! qt-widget-show-minimized!
+   qt-widget-show-normal! qt-widget-width
+   qt-widget-window-state QT_MOD_SHIFT QT_MOD_ALT QT_MOD_META
+   QT_MOD_CTRL QT_KEY_ESCAPE QT_KEY_BACKSPACE QT_KEY_RETURN
+   QT_KEY_ENTER QT_KEY_DELETE QT_KEY_TAB QT_KEY_INSERT
+   QT_KEY_HOME QT_KEY_END QT_KEY_LEFT QT_KEY_RIGHT QT_KEY_UP
+   QT_KEY_DOWN QT_KEY_PAGE_UP QT_KEY_PAGE_DOWN QT_KEY_SPACE
+   QT_KEY_A QT_KEY_G QT_KEY_N QT_KEY_P QT_KEY_R QT_KEY_S
+   QT_KEY_W QT_KEY_Z QT_KEY_F1 QT_KEY_F2 QT_KEY_F3 QT_KEY_F4
+   QT_KEY_F5 QT_KEY_F6 QT_KEY_F7 QT_KEY_F8 QT_KEY_F9 QT_KEY_F10
+   QT_KEY_F11 QT_KEY_F12 QT_CURSOR_UP QT_CURSOR_DOWN
+   QT_CURSOR_START QT_CURSOR_END QT_CURSOR_START_OF_BLOCK
+   QT_CURSOR_END_OF_BLOCK QT_CURSOR_NEXT_CHAR
+   QT_CURSOR_NEXT_WORD QT_CURSOR_PREVIOUS_CHAR
+   QT_CURSOR_PREVIOUS_WORD qt-terminal-create
+   qt-terminal-destroy! qt-terminal-spawn!
    qt-terminal-connect-fd! qt-terminal-send-key-event!
    qt-terminal-send-input! qt-terminal-is-running?
    qt-terminal-get-screen-text qt-terminal-interrupt!
@@ -266,6 +267,10 @@
   (def ffi-qt-last-key-widget
        (foreign-procedure "qt_last_key_widget" () void*))
   (def (qt-last-key-widget) (ffi-qt-last-key-widget))
+  (def ffi-qt-widget-parent
+       (foreign-procedure "qt_widget_parent" (void*) void*))
+  (def (qt-widget-parent widget)
+       (ffi-qt-widget-parent widget))
   (def (sci-check-handle! caller sci)
        (unless sci
          (error 'sci-check-handle!
diff --git a/lib/jerboa-emacs/qt/window.sls b/lib/jerboa-emacs/qt/window.sls
index de010f7..8f180d0 100644
--- a/lib/jerboa-emacs/qt/window.sls
+++ b/lib/jerboa-emacs/qt/window.sls
@@ -169,6 +169,7 @@
   (def (qt-safe-widget-destroy! context widget)
        (qt-window-check-handle! context "widget" widget)
        (qt-widget-destroy! widget))
+  (def *qt-window-visual-state* (make-hash-table-eq))
   (def (qt-safe-widget-set-focus! context widget)
        (qt-window-check-handle! context "widget" widget)
        (qt-widget-set-focus! widget))
@@ -180,6 +181,7 @@
          (qt-safe-widget-hide! context trash)
          (qt-safe-widget-destroy! context trash)))
   (def (qt-dispose-window-container! context container)
+       (hash-remove! *qt-window-visual-state* container)
        (qt-window-pre-container-destroy! container)
        (qt-dispose-widget! context container))
   (def (qt-focus-target-for-edit-window win)
@@ -844,6 +846,7 @@
                               (not (eq? parent-spl
                                         (qt-frame-splitter fr))))
                      (when container
+                       (hash-remove! *qt-window-visual-state* container)
                        (qt-window-pre-container-destroy! container))
                      (set! container #f)
                      (qt-dispose-widget!
@@ -907,7 +910,7 @@
            fr
            "after qt-frame-delete-other-windows!")))
   (def (qt-frame-update-visual-indicators! fr)
-       "Update container borders to show which window is active.\n   Active window: blue border; inactive windows: no border."
+       "Update container borders to show which window is active.\n   Active window: blue border; inactive windows: subtle constant-width border."
        (let ([cur-idx (qt-frame-current-idx fr)]
              [windows (qt-frame-windows fr)])
          (let loop ([wins windows] [i 0])
@@ -915,13 +918,17 @@
              (let* ([win (car wins)]
                     [container (qt-edit-window-container win)]
                     [is-current (= i cur-idx)]
+                    [state (if is-current 'active 'inactive)]
                     [border-style (if is-current
-                                      "border: 2px solid #51afef;"
-                                      "border: 1px solid #3a3a3a;")])
-               (qt-safe-widget-set-style-sheet!
-                 "qt-frame-update-visual-indicators!"
-                 container
-                 border-style))
+                                      "border: 2px solid #51afef; margin: 0px; padding: 0px;"
+                                      "border: 2px solid #303030; margin: 0px; padding: 0px;")])
+               (unless (eq? (hash-get *qt-window-visual-state* container)
+                            state)
+                 (qt-safe-widget-set-style-sheet!
+                   "qt-frame-update-visual-indicators!"
+                   container
+                   border-style)
+                 (hash-put! *qt-window-visual-state* container state)))
              (loop (cdr wins) (+ i 1))))))
   (def (qt-frame-other-window! fr)
        "Switch to the next window (wraps around)."
diff --git a/src/jerboa-emacs/qt/buffer.ss b/src/jerboa-emacs/qt/buffer.ss
index 57c9139..fe8191a 100644
--- a/src/jerboa-emacs/qt/buffer.ss
+++ b/src/jerboa-emacs/qt/buffer.ss
@@ -55,28 +55,38 @@
    Runs post-buffer-attach-hook to handle image/text display toggling.
    All visual changes are batched via setUpdatesEnabled to prevent flicker."
   (verbose-log! "qt-buffer-attach! buf=" (buffer-name buf))
-  ;; Suppress intermediate repaints: doc swap + highlight + font loop
-  ;; are 3 visual changes that would flash in sequence without batching.
-  (qt-widget-set-updates-enabled! editor #f)
-  (let ((doc (buffer-doc-pointer buf)))
-    (verbose-log! "qt-buffer-attach! SCI_SETDOCPOINTER begin")
-    (sci-send editor SCI_SETDOCPOINTER 0 doc)
-    (verbose-log! "qt-buffer-attach! SCI_SETDOCPOINTER done")
-    (doc-editor-register! doc editor)
-    ;; Force QScintilla widget to sync with the new document's read-only state.
-    ;; Without this, viewing a read-only buffer (e.g. *Buffer List*) makes all
-    ;; subsequent buffers uneditable.
-    (let ((ro (sci-send editor SCI_GETREADONLY)))
-      (sci-send editor SCI_SETREADONLY ro))
-    ;; Terminal buffers: disable line wrap (each vtscreen row = one visual line).
-    ;; Non-terminal buffers: enable word wrap for readability.
-    ;; Must be set per buffer-switch since wrap is a widget property, not per-document.
-    (let ((lang (buffer-lexer-lang buf)))
-      (qt-plain-text-edit-set-line-wrap! editor
-        (not (or (eq? lang 'terminal) (eq? lang 'shell)))))
-    (when run-post-hook?
-      (verbose-log! "qt-buffer-attach! post-buffer-attach-hook begin")
-      ;; Toggle image/editor display via hook (set up in qt/app.ss)
-      (run-hooks! 'post-buffer-attach-hook editor buf))
-    (verbose-log! "qt-buffer-attach! done buf=" (buffer-name buf)))
-  (qt-widget-set-updates-enabled! editor #t))
+  ;; Suppress intermediate repaints: doc swap + stacked page switch + highlight
+  ;; setup are separate visual operations and otherwise flash in sequence.
+  (let ((container (qt-widget-parent editor)))
+    (def (updates-enabled! enabled?)
+      (when container
+        (qt-widget-set-updates-enabled! container enabled?))
+      (qt-widget-set-updates-enabled! editor enabled?))
+    (updates-enabled! #f)
+    (with-catch
+      (lambda (e)
+        (updates-enabled! #t)
+        (raise e))
+      (lambda ()
+        (let ((doc (buffer-doc-pointer buf)))
+          (verbose-log! "qt-buffer-attach! SCI_SETDOCPOINTER begin")
+          (sci-send editor SCI_SETDOCPOINTER 0 doc)
+          (verbose-log! "qt-buffer-attach! SCI_SETDOCPOINTER done")
+          (doc-editor-register! doc editor)
+          ;; Force QScintilla widget to sync with the new document's read-only state.
+          ;; Without this, viewing a read-only buffer (e.g. *Buffer List*) makes all
+          ;; subsequent buffers uneditable.
+          (let ((ro (sci-send editor SCI_GETREADONLY)))
+            (sci-send editor SCI_SETREADONLY ro))
+          ;; Terminal buffers: disable line wrap (each vtscreen row = one visual line).
+          ;; Non-terminal buffers: enable word wrap for readability.
+          ;; Must be set per buffer-switch since wrap is a widget property, not per-document.
+          (let ((lang (buffer-lexer-lang buf)))
+            (qt-plain-text-edit-set-line-wrap! editor
+              (not (or (eq? lang 'terminal) (eq? lang 'shell)))))
+          (when run-post-hook?
+            (verbose-log! "qt-buffer-attach! post-buffer-attach-hook begin")
+            ;; Toggle image/editor display via hook (set up in qt/app.ss)
+            (run-hooks! 'post-buffer-attach-hook editor buf))
+          (verbose-log! "qt-buffer-attach! done buf=" (buffer-name buf)))
+        (updates-enabled! #t)))))
diff --git a/src/jerboa-emacs/qt/sci-shim.ss b/src/jerboa-emacs/qt/sci-shim.ss
index 9a455b3..805e86e 100644
--- a/src/jerboa-emacs/qt/sci-shim.ss
+++ b/src/jerboa-emacs/qt/sci-shim.ss
@@ -116,7 +116,7 @@
   ;; Toolbar
   qt-toolbar-add-action! qt-toolbar-add-separator! qt-toolbar-create qt-toolbar-set-movable!
   ;; Widget
-  qt-widget-close! qt-widget-create qt-widget-destroy! qt-widget-font
+  qt-widget-close! qt-widget-create qt-widget-parent qt-widget-destroy! qt-widget-font
   qt-widget-height qt-widget-hide! qt-widget-resize! qt-widget-set-focus!
   qt-widget-set-font-size! qt-widget-set-maximum-height! qt-widget-set-minimum-height!
   qt-widget-set-minimum-size! qt-widget-set-size-policy! qt-widget-set-style-sheet!
@@ -269,6 +269,12 @@
 (def (qt-last-key-widget)
   (ffi-qt-last-key-widget))
 
+(def ffi-qt-widget-parent
+  (foreign-procedure "qt_widget_parent" (void*) void*))
+
+(def (qt-widget-parent widget)
+  (ffi-qt-widget-parent widget))
+
 ;; Helpers
 (def (sci-check-handle! caller sci)
   (unless sci
diff --git a/src/jerboa-emacs/qt/window.ss b/src/jerboa-emacs/qt/window.ss
index d552dc8..29dc1cb 100644
--- a/src/jerboa-emacs/qt/window.ss
+++ b/src/jerboa-emacs/qt/window.ss
@@ -235,6 +235,8 @@
   (qt-window-check-handle! context "widget" widget)
   (qt-widget-destroy! widget))
 
+(def *qt-window-visual-state* (make-hash-table-eq))
+
 (def (qt-safe-widget-set-focus! context widget)
   (qt-window-check-handle! context "widget" widget)
   (qt-widget-set-focus! widget))
@@ -252,6 +254,7 @@
     (qt-safe-widget-destroy! context trash)))
 
 (def (qt-dispose-window-container! context container)
+  (hash-remove! *qt-window-visual-state* container)
   (qt-window-pre-container-destroy! container)
   (qt-dispose-widget! context container))
 
@@ -837,6 +840,7 @@
               ;; to avoid the double-free in the explicit container destroy below.
               (when (and parent-spl (not (eq? parent-spl (qt-frame-splitter fr))))
                 (when container
+                  (hash-remove! *qt-window-visual-state* container)
                   (qt-window-pre-container-destroy! container))
                 (set! container #f)   ;; cleared — Qt will destroy it via parent-spl
                 (qt-dispose-widget! "qt-frame-delete-window!" parent-spl))))))
@@ -898,7 +902,7 @@
 
 (def (qt-frame-update-visual-indicators! fr)
   "Update container borders to show which window is active.
-   Active window: blue border; inactive windows: no border."
+   Active window: blue border; inactive windows: subtle constant-width border."
   (let ((cur-idx (qt-frame-current-idx fr))
         (windows (qt-frame-windows fr)))
     (let loop ((wins windows) (i 0))
@@ -906,12 +910,16 @@
         (let* ((win (car wins))
                (container (qt-edit-window-container win))
                (is-current (= i cur-idx))
-               ;; Active: 2px solid blue border; inactive: 1px subtle gray
+               (state (if is-current 'active 'inactive))
+               ;; Keep border width constant. Changing 1px↔2px changes the
+               ;; QStackedWidget size hint and makes pane contents jitter.
                (border-style (if is-current
-                               "border: 2px solid #51afef;"
-                               "border: 1px solid #3a3a3a;")))
-          (qt-safe-widget-set-style-sheet! "qt-frame-update-visual-indicators!"
-                                           container border-style))
+                               "border: 2px solid #51afef; margin: 0px; padding: 0px;"
+                               "border: 2px solid #303030; margin: 0px; padding: 0px;")))
+          (unless (eq? (hash-get *qt-window-visual-state* container) state)
+            (qt-safe-widget-set-style-sheet! "qt-frame-update-visual-indicators!"
+                                             container border-style)
+            (hash-put! *qt-window-visual-state* container state)))
         (loop (cdr wins) (+ i 1))))))
 
 (def (qt-frame-other-window! fr)
diff --git a/support/vendor-overrides/qt_shim.cpp b/support/vendor-overrides/qt_shim.cpp
index 5ab9285..181ebd4 100644
--- a/support/vendor-overrides/qt_shim.cpp
+++ b/support/vendor-overrides/qt_shim.cpp
@@ -809,6 +809,11 @@ extern "C" qt_widget_t qt_widget_create(qt_widget_t parent) {
     QT_RETURN(qt_widget_t, new QWidget(static_cast<QWidget*>(parent)));
 }
 
+extern "C" qt_widget_t qt_widget_parent(qt_widget_t w) {
+    QT_NULL_CHECK_RET(w, nullptr);
+    QT_RETURN(qt_widget_t, static_cast<QWidget*>(w)->parentWidget());
+}
+
 extern "C" void qt_widget_show(qt_widget_t w) {
     QT_NULL_CHECK_VOID(w);
     QT_VOID(static_cast<QWidget*>(w)->show());
@@ -2369,8 +2374,13 @@ extern "C" void qt_scroll_area_set_vertical_scrollbar_policy(
 extern "C" qt_splitter_t qt_splitter_create(int orientation,
                                              qt_widget_t parent) {
     QT_RETURN(qt_splitter_t,
-        new QSplitter(static_cast<Qt::Orientation>(orientation),
-        static_cast<QWidget*>(parent)));
+        [&]() -> qt_splitter_t {
+            auto* splitter = new QSplitter(static_cast<Qt::Orientation>(orientation),
+                                           static_cast<QWidget*>(parent));
+            splitter->setChildrenCollapsible(false);
+            splitter->setOpaqueResize(false);
+            return static_cast<qt_splitter_t>(splitter);
+        }());
 }
 
 extern "C" void qt_splitter_add_widget(qt_splitter_t s, qt_widget_t w) {
diff --git a/support/vendor-overrides/qt_shim.h b/support/vendor-overrides/qt_shim.h
index f480f37..c90cddf 100644
--- a/support/vendor-overrides/qt_shim.h
+++ b/support/vendor-overrides/qt_shim.h
@@ -28,6 +28,7 @@ void             qt_application_destroy(qt_application_t app);
 
 /* --- Widget base (applies to all widget types) --- */
 qt_widget_t qt_widget_create(qt_widget_t parent);
+qt_widget_t qt_widget_parent(qt_widget_t w);
 void qt_widget_show(qt_widget_t w);
 void qt_widget_hide(qt_widget_t w);
 void qt_widget_close(qt_widget_t w);