Smooth Qt redraws during pane updates
ober
958ce03932b65622679be4b725e51b2142538f91
--- 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)))))) --- 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! --- 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)." --- 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))))) --- 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 --- 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) --- 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) { --- 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);