Reduce flicker: buffered draw, update batching, opaque paint attribute

ober

447d4b55ae1c9820ecc15a9458addf9ef4934bc7

diff --git a/lib/jerboa-emacs/qt/buffer.sls b/lib/jerboa-emacs/qt/buffer.sls
index 59616ee..2ea7868 100644
--- a/lib/jerboa-emacs/qt/buffer.sls
+++ b/lib/jerboa-emacs/qt/buffer.sls
@@ -39,8 +39,9 @@
          (hash-remove! *doc-buffer-map* doc)
          (buffer-list-remove! buf)))
   (def (qt-buffer-attach! editor buf)
-       "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."
+       "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)
@@ -53,4 +54,5 @@
          (run-hooks! 'post-buffer-attach-hook editor buf)
          (verbose-log!
            "qt-buffer-attach! done buf="
-           (buffer-name buf)))))
+           (buffer-name buf)))
+       (qt-widget-set-updates-enabled! editor #t)))
diff --git a/lib/jerboa-emacs/qt/sci-shim.sls b/lib/jerboa-emacs/qt/sci-shim.sls
index e8893fe..fe5c1b2 100644
--- a/lib/jerboa-emacs/qt/sci-shim.sls
+++ b/lib/jerboa-emacs/qt/sci-shim.sls
@@ -117,23 +117,23 @@
    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-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-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)
   (import
     (except (chezscheme) make-hash-table hash-table? iota \x31;+ \x31;-
       getenv path-extension path-absolute? thread? make-mutex
diff --git a/lib/jerboa-emacs/qt/window.sls b/lib/jerboa-emacs/qt/window.sls
index 172e0ef..de470a5 100644
--- a/lib/jerboa-emacs/qt/window.sls
+++ b/lib/jerboa-emacs/qt/window.sls
@@ -232,43 +232,43 @@
                          (parse-hex-color (face-bg region-face))])
              (sci-send ed 2068 1 (rgb->sci r g b))))))
   (def (qt-scintilla-setup-editor! ed)
-       "Configure QScintilla editor: theme, margins, caret, save-point signals."
-       (qt-apply-editor-theme! ed)
-       (sci-send ed SCI_SETMARGINTYPEN 0 SC_MARGIN_NUMBER)
-       (sci-send
-         ed
-         SCI_SETMARGINWIDTHN
-         0
-         (max 30 (* *default-font-size* 3)))
-       (sci-send ed SCI_SETMARGINWIDTHN 1 0)
-       (sci-send ed SCI_SETMARGINWIDTHN 2 0)
-       (sci-send ed SCI_SETMARGINWIDTHN 3 0)
-       (sci-send ed SCI_SETMARGINWIDTHN 4 0)
-       (let ([bg (let ([f (face-get 'default)])
-                   (if (and f (face-bg f))
-                       (let-values ([(r g b)
-                                     (parse-hex-color (face-bg f))])
-                         (rgb->sci r g b))
-                       (rgb->sci 30 30 46)))])
-         (sci-send ed 2290 1 bg)
-         (sci-send ed 2291 1 bg))
-       (sci-send ed SCI_SETCARETLINEVISIBLE 1)
-       (sci-send ed SCI_SETTABWIDTH 4)
-       (sci-send ed SCI_SETINDENT 4) (sci-send ed 2563 1)
-       (sci-send ed 2565 1) (sci-send ed 2608 1)
-       (sci-send ed 2567 1)
-       (qt-on-scintilla-save-point-reached!
-         ed
-         (lambda ()
-           (let* ([doc (sci-send ed SCI_GETDOCPOINTER)]
-                  [buf (hash-get *doc-buffer-map* doc)])
-             (when buf (buffer-modified-set! buf #f)))))
-       (qt-on-scintilla-save-point-left!
-         ed
-         (lambda ()
-           (let* ([doc (sci-send ed SCI_GETDOCPOINTER)]
-                  [buf (hash-get *doc-buffer-map* doc)])
-             (when buf (buffer-modified-set! buf #t))))))
+   "Configure QScintilla editor: theme, margins, caret, save-point signals."
+   (qt-apply-editor-theme! ed)
+   (sci-send ed SCI_SETMARGINTYPEN 0 SC_MARGIN_NUMBER)
+   (sci-send
+     ed
+     SCI_SETMARGINWIDTHN
+     0
+     (max 30 (* *default-font-size* 3)))
+   (sci-send ed SCI_SETMARGINWIDTHN 1 0)
+   (sci-send ed SCI_SETMARGINWIDTHN 2 0)
+   (sci-send ed SCI_SETMARGINWIDTHN 3 0)
+   (sci-send ed SCI_SETMARGINWIDTHN 4 0)
+   (let ([bg (let ([f (face-get 'default)])
+               (if (and f (face-bg f))
+                   (let-values ([(r g b) (parse-hex-color (face-bg f))])
+                     (rgb->sci r g b))
+                   (rgb->sci 30 30 46)))])
+     (sci-send ed 2290 1 bg)
+     (sci-send ed 2291 1 bg))
+   (sci-send ed SCI_SETCARETLINEVISIBLE 1)
+   (sci-send ed SCI_SETTABWIDTH 4)
+   (sci-send ed SCI_SETINDENT 4) (sci-send ed 2563 1)
+   (sci-send ed 2565 1) (sci-send ed 2608 1)
+   (sci-send ed 2567 1) (sci-send ed 2035 1)
+   (sci-send ed 2284 1) (qt-widget-set-attribute! ed 4 #t)
+   (qt-on-scintilla-save-point-reached!
+     ed
+     (lambda ()
+       (let* ([doc (sci-send ed SCI_GETDOCPOINTER)]
+              [buf (hash-get *doc-buffer-map* doc)])
+         (when buf (buffer-modified-set! buf #f)))))
+   (qt-on-scintilla-save-point-left!
+     ed
+     (lambda ()
+       (let* ([doc (sci-send ed SCI_GETDOCPOINTER)]
+              [buf (hash-get *doc-buffer-map* doc)])
+         (when buf (buffer-modified-set! buf #t))))))
   (def (qt-make-new-window! container-parent buf)
        "Create a new qt-edit-window with a fresh editor in a new container.\n   CONTAINER-PARENT is the QSplitter that will own the container."
        (let* ([container (qt-stacked-widget-create
@@ -334,6 +334,7 @@
               [main-win (qt-frame-main-win fr)]
               [saved-w (and main-win (qt-widget-width main-win))]
               [saved-h (and main-win (qt-widget-height main-win))])
+         (when main-win (qt-widget-set-updates-enabled! main-win #f))
          (let ([result (cond
                          [(and parent
                                (= (split-node-orientation parent)
@@ -484,6 +485,7 @@
              (qt-widget-resize! main-win saved-w saved-h))
            (when result (qt-widget-set-focus! result))
            (qt-frame-update-visual-indicators! fr)
+           (when main-win (qt-widget-set-updates-enabled! main-win #t))
            result)))
   (def (qt-frame-split! fr)
        "Split vertically: add a new window below. Returns the new editor."
diff --git a/src/jerboa-emacs/qt/buffer.ss b/src/jerboa-emacs/qt/buffer.ss
index 400ae40..f866c9d 100644
--- a/src/jerboa-emacs/qt/buffer.ss
+++ b/src/jerboa-emacs/qt/buffer.ss
@@ -52,8 +52,12 @@
   "Switch editor to display this buffer's document.
    Re-applies the document's read-only state after swap because QScintilla
    may have a widget-level readOnly flag that persists across document switches.
-   Runs post-buffer-attach-hook to handle image/text display toggling."
+   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)
@@ -67,4 +71,5 @@
     (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))))
+    (verbose-log! "qt-buffer-attach! done buf=" (buffer-name buf)))
+  (qt-widget-set-updates-enabled! editor #t))
diff --git a/src/jerboa-emacs/qt/sci-shim.ss b/src/jerboa-emacs/qt/sci-shim.ss
index 9f07d99..ee81806 100644
--- a/src/jerboa-emacs/qt/sci-shim.ss
+++ b/src/jerboa-emacs/qt/sci-shim.ss
@@ -118,7 +118,7 @@
   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-updates-enabled!
+  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 constants re-exported for modules that only import sci-shim
diff --git a/src/jerboa-emacs/qt/window.ss b/src/jerboa-emacs/qt/window.ss
index 5003ec4..4d7df33 100644
--- a/src/jerboa-emacs/qt/window.ss
+++ b/src/jerboa-emacs/qt/window.ss
@@ -282,6 +282,16 @@
   (sci-send ed 2565 1)  ; SCI_SETADDITIONALSELECTIONTYPING
   (sci-send ed 2608 1)  ; SCI_SETADDITIONALCARETSVISIBLE
   (sci-send ed 2567 1)  ; SCI_SETADDITIONALCARETSBLINK
+  ;; Scintilla internal double-buffering — reduces flicker during rapid
+  ;; style changes and text updates (on top of Qt's own double-buffering).
+  (sci-send ed 2035 1)  ; SCI_SETBUFFEREDDRAW = on
+  ;; Two-phase drawing: background painted first, then text on top.
+  ;; Smoother than single-phase for syntax-highlighted text.
+  (sci-send ed 2284 1)  ; SCI_SETTWOPHASEDRAW = on
+  ;; WA_OpaquePaintEvent (4): tell Qt not to clear the widget background
+  ;; before painting.  Prevents "white flash" when widgets are created,
+  ;; resized, or switch documents.
+  (qt-widget-set-attribute! ed 4 #t)
   ;; Save-point signals for modified state tracking
   (qt-on-scintilla-save-point-reached! ed
     (lambda ()
@@ -371,6 +381,9 @@
          (saved-w   (and main-win (qt-widget-width main-win)))
          (saved-h   (and main-win (qt-widget-height main-win))))
 
+    ;; Suppress intermediate repaints during split: widget creation,
+    ;; reparenting, and size equalization flash without batching.
+    (when main-win (qt-widget-set-updates-enabled! main-win #f))
     (let ((result
       (cond
         ;; ── Case A: parent has same orientation — add sibling ─────────────────
@@ -470,6 +483,8 @@
       (when result (qt-widget-set-focus! result))
       ;; Update visual indicators
       (qt-frame-update-visual-indicators! fr)
+      ;; Re-enable updates — single
+      (when main-win (qt-widget-set-updates-enabled! main-win #t))
       result)))
 
 (def (qt-frame-split! fr)
diff --git a/vendor/chez-qt-ffi-static.ss b/vendor/chez-qt-ffi-static.ss
index c7b1a60..f47ebba 100644
--- a/vendor/chez-qt-ffi-static.ss
+++ b/vendor/chez-qt-ffi-static.ss
@@ -23,7 +23,7 @@
     ffi-qt-widget-set-minimum-width ffi-qt-widget-set-minimum-height
     ffi-qt-widget-set-maximum-width ffi-qt-widget-set-maximum-height
     ffi-qt-widget-set-cursor ffi-qt-widget-unset-cursor
-    ffi-qt-widget-resize ffi-qt-widget-set-style-sheet
+    ffi-qt-widget-resize ffi-qt-widget-set-style-sheet ffi-qt-widget-set-attribute
     ffi-qt-widget-set-tooltip ffi-qt-widget-set-font-size
     ffi-qt-widget-destroy
 
@@ -967,6 +967,8 @@
 
   (define ffi-qt-widget-set-style-sheet
     (foreign-procedure "qt_widget_set_style_sheet" (void* string) void))
+  (define ffi-qt-widget-set-attribute
+    (foreign-procedure "qt_widget_set_attribute" (void* int int) void))
 
   (define ffi-qt-widget-set-tooltip
     (foreign-procedure "qt_widget_set_tooltip" (void* string) void))
diff --git a/vendor/chez-qt-qt.ss b/vendor/chez-qt-qt.ss
index 055804a..b5d5269 100644
--- a/vendor/chez-qt-qt.ss
+++ b/vendor/chez-qt-qt.ss
@@ -30,7 +30,7 @@
     qt-widget-set-minimum-width! qt-widget-set-minimum-height!
     qt-widget-set-maximum-width! qt-widget-set-maximum-height!
     qt-widget-set-cursor! qt-widget-unset-cursor!
-    qt-widget-resize! qt-widget-set-style-sheet!
+    qt-widget-resize! qt-widget-set-style-sheet! qt-widget-set-attribute!
     qt-widget-set-tooltip! qt-widget-set-font-size!
     qt-widget-destroy!
 
@@ -928,6 +928,7 @@
   (define (qt-widget-unset-cursor! w) (ffi-qt-widget-unset-cursor w))
   (define (qt-widget-resize! w width height) (ffi-qt-widget-resize w width height))
   (define (qt-widget-set-style-sheet! w css) (ffi-qt-widget-set-style-sheet w css))
+  (define (qt-widget-set-attribute! w attr on) (ffi-qt-widget-set-attribute w attr (if on 1 0)))
   (define (qt-widget-set-tooltip! w text) (ffi-qt-widget-set-tooltip w text))
   (define (qt-widget-set-font-size! w size) (ffi-qt-widget-set-font-size w size))
 
diff --git a/vendor/qt_shim.cpp b/vendor/qt_shim.cpp
index 66db32c..869a390 100644
--- a/vendor/qt_shim.cpp
+++ b/vendor/qt_shim.cpp
@@ -633,6 +633,12 @@ extern "C" void qt_widget_set_style_sheet(qt_widget_t w, const char* css) {
     QT_VOID(static_cast<QWidget*>(w)->setStyleSheet(QString::fromUtf8(css)));
 }
 
+extern "C" void qt_widget_set_attribute(qt_widget_t w, int attribute, int on) {
+    QT_NULL_CHECK_VOID(w);
+    QT_VOID(static_cast<QWidget*>(w)->setAttribute(
+        static_cast<Qt::WidgetAttribute>(attribute), on != 0));
+}
+
 extern "C" void qt_widget_set_tooltip(qt_widget_t w, const char* text) {
     QT_NULL_CHECK_VOID(w);
     QT_VOID(static_cast<QWidget*>(w)->setToolTip(QString::fromUtf8(text)));
diff --git a/vendor/qt_shim.h b/vendor/qt_shim.h
index 93d50cf..c75f013 100644
--- a/vendor/qt_shim.h
+++ b/vendor/qt_shim.h
@@ -46,6 +46,7 @@ void qt_widget_set_cursor(qt_widget_t w, int shape);
 void qt_widget_unset_cursor(qt_widget_t w);
 void qt_widget_resize(qt_widget_t w, int width, int height);
 void qt_widget_set_style_sheet(qt_widget_t w, const char* css);
+void qt_widget_set_attribute(qt_widget_t w, int attribute, int on);
 void qt_widget_set_tooltip(qt_widget_t w, const char* text);
 void qt_widget_set_font_size(qt_widget_t w, int size);
 void qt_widget_destroy(qt_widget_t w);