From 1edc6d5bb6d6a61e703853cb7ffbbd8f26f25f75 Mon Sep 17 00:00:00 2001 From: Kinneyzhang Date: Sat, 22 Aug 2026 08:21:08 +0800 Subject: [PATCH] Fix interactive Ebox publication and viewport bounds --- ebox-buffer-backend.el | 18 +++++-- ebox-surface.el | 19 ++++--- ebox.el | 89 ++++++++++++++++++--------------- tests/ebox-core-render-tests.el | 52 +++++++++++++++++++ tests/ebox-surface-tests.el | 22 ++++++-- 5 files changed, 146 insertions(+), 54 deletions(-) diff --git a/ebox-buffer-backend.el b/ebox-buffer-backend.el index 8a24f2f..7d32174 100644 --- a/ebox-buffer-backend.el +++ b/ebox-buffer-backend.el @@ -223,6 +223,18 @@ immediately after BODY can trigger GC before the updated buffer is visible." (or (plist-get style :background-color) (plist-get style :bgcolor)))) +(defun ebox-buffer--default-foreground-face () + "Return a face that resets only foreground to the resolved default. +Fall back to inheriting `default' when a headless frame has no concrete +foreground color." + (let ((foreground (face-attribute 'default :foreground nil t))) + (if (or (null foreground) + (eq foreground 'unspecified) + (and (stringp foreground) + (string-prefix-p "unspecified" foreground))) + '(:inherit default) + (list :foreground foreground)))) + (defun ebox-buffer-paint-text-properties (style role) "Return Emacs text properties for computed STYLE in paint ROLE. This backend mapper intentionally accepts computed style facts and emits only @@ -231,7 +243,7 @@ buffer-facing paint properties. Layout-only properties never pass through." (when-let ((foreground (ebox-buffer--paint-color style role))) (setq face (if (eq foreground 'ebox/default-foreground) - (plist-put face :inherit 'default) + (append (ebox-buffer--default-foreground-face) face) (plist-put face :foreground foreground)))) (when-let ((background (ebox-buffer--paint-background-color style role))) (setq face (plist-put face :background background))) @@ -282,7 +294,7 @@ buffer-facing paint properties. Layout-only properties never pass through." (ebox--add-render-face! string 0 length (if (eq color 'ebox/default-foreground) - '(:inherit default) + (ebox-buffer--default-foreground-face) `(:foreground ,color)) t) (ebox--register-render-owned-face-values source string))) @@ -296,7 +308,7 @@ buffer-facing paint properties. Layout-only properties never pass through." (ebox--add-render-face! string 0 length (if (eq color 'ebox/default-foreground) - '(:inherit default) + (ebox-buffer--default-foreground-face) `(:foreground ,color)) t)) (when bgcolor diff --git a/ebox-surface.el b/ebox-surface.el index e7c4e9c..339d6f9 100644 --- a/ebox-surface.el +++ b/ebox-surface.el @@ -305,8 +305,8 @@ construction time and can use the static projection path." Some Emacs GUI builds return a column-like half-width even when `window-body-width' is called with PIXELWISE non-nil. When the result is clearly inconsistent with the outer pixel width, prefer the outer width and -keep a one-pixel exclusive boundary. Headless/test windows retain the body -width fallback." +reserve two character columns for Emacs continuation/truncation display. +Headless/test windows retain the body width fallback." (let ((body (condition-case nil (window-body-width window t) @@ -315,11 +315,16 @@ width fallback." (condition-case nil (window-pixel-width window) (error nil)))) - (max 0 - (1- - (if (and body outer (> outer (* 1.8 body))) - outer - (or body outer 0)))))) + (let* ((char-width + (condition-case nil + (frame-char-width (window-frame window)) + (error 1))) + (reserve (max 2 (* 2 (max 1 char-width))))) + (max 0 + (- (if (and body outer (> outer (* 1.8 body))) + outer + (or body outer 0)) + reserve))))) (defun ebox-surface--ensure-signals (buffer values) "Return BUFFER's context signals for VALUES and whether they were created." diff --git a/ebox.el b/ebox.el index 758f804..acbd3c6 100644 --- a/ebox.el +++ b/ebox.el @@ -4519,46 +4519,55 @@ FRAMEWORK-ROLLBACK, when supplied, receives the same report if publication or a later transaction phase fails, and is contained if it violates no-throw. Return the successful publication report stored by `ebox-buffer-update-report'." - (unless (or (null framework-publish) (functionp framework-publish)) - (signal 'wrong-type-argument (list 'functionp framework-publish))) - (unless (or (null framework-rollback) (functionp framework-rollback)) - (signal 'wrong-type-argument (list 'functionp framework-rollback))) - (when (and framework-rollback (null framework-publish)) - (error "Ebox framework rollback requires framework publish")) - (let ((buffer (get-buffer buffer-or-name))) - (unless (buffer-live-p buffer) - (error "Ebox declarative commit requires an existing live buffer: %S" - buffer-or-name)) - (let* ((commit-input - (if (ebox-candidate-p next-root) - (ebox-incremental-consume-candidate buffer next-root) - (ebox-incremental-prepare-root-commit buffer next-root))) - (source (plist-get commit-input :root)) - (callback - (or framework-publish - ebox-incremental--after-declarative-publication)) - (participant - (ebox-surface--make-framework-participant - :publish callback :rollback framework-rollback - :state 'unpublished :diagnostics nil)) - (_surface - (if-let ((scope-node-ids - (plist-get commit-input :scope-node-ids))) - (ebox-surface-update-buffer-scoped - buffer source scope-node-ids - (plist-get commit-input :report-base) - (plist-get commit-input :state-overrides) - callback nil nil - (plist-get commit-input :projection-kind) - t participant) - (ebox-surface-mount-buffer - buffer source - (plist-get commit-input :report-base) - callback - (plist-get commit-input :preserve-identities-p) - (plist-get commit-input :state-overrides) - participant)))) - (ebox-surface--framework-participant-report participant)))) + (let ((execute + (lambda () + (unless (or (null framework-publish) (functionp framework-publish)) + (signal 'wrong-type-argument (list 'functionp framework-publish))) + (unless (or (null framework-rollback) + (functionp framework-rollback)) + (signal 'wrong-type-argument (list 'functionp framework-rollback))) + (when (and framework-rollback (null framework-publish)) + (error "Ebox framework rollback requires framework publish")) + (let ((buffer (get-buffer buffer-or-name))) + (unless (buffer-live-p buffer) + (error + "Ebox declarative commit requires an existing live buffer: %S" + buffer-or-name)) + (let* ((commit-input + (if (ebox-candidate-p next-root) + (ebox-incremental-consume-candidate buffer next-root) + (ebox-incremental-prepare-root-commit buffer next-root))) + (source (plist-get commit-input :root)) + (callback + (or framework-publish + ebox-incremental--after-declarative-publication)) + (participant + (ebox-surface--make-framework-participant + :publish callback :rollback framework-rollback + :state 'unpublished :diagnostics nil)) + (_surface + (if-let ((scope-node-ids + (plist-get commit-input :scope-node-ids))) + (ebox-surface-update-buffer-scoped + buffer source scope-node-ids + (plist-get commit-input :report-base) + (plist-get commit-input :state-overrides) + callback nil nil + (plist-get commit-input :projection-kind) + t participant) + (ebox-surface-mount-buffer + buffer source + (plist-get commit-input :report-base) + callback + (plist-get commit-input :preserve-identities-p) + (plist-get commit-input :state-overrides) + participant)))) + (ebox-surface--framework-participant-report participant)))))) + (if noninteractive + (ebox--with-render-gc + (funcall execute)) + (ebox--with-deferred-render-gc + (funcall execute))))) ;;;###autoload (defun ebox-rerender-buffer-with-context diff --git a/tests/ebox-core-render-tests.el b/tests/ebox-core-render-tests.el index fd1a184..697a8c0 100644 --- a/tests/ebox-core-render-tests.el +++ b/tests/ebox-core-render-tests.el @@ -557,6 +557,20 @@ (let ((face (get-text-property (match-beginning 0) 'face))) (should (equal (car face) '(:inherit default)))))))) +(ert-deftest ebox-default-foreground-reset-preserves-explicit-background () + "Resolve GUI default foreground without inheriting its background." + (let ((original (symbol-function 'face-attribute))) + (cl-letf (((symbol-function 'face-attribute) + (lambda (face attribute &rest arguments) + (if (and (eq face 'default) (eq attribute :foreground)) + "Black" + (apply original face attribute arguments))))) + (should + (equal + (ebox-buffer-paint-text-properties + '(:color ebox/default-foreground :bgcolor "#111827") 'content) + '(face (:foreground "Black" :background "#111827"))))))) + (ert-deftest ebox-flex-layout-properties-come-from-ecss-cascade () "An ECSS rule should determine a Flex container's actual main axis." (ebox-test--reset-runtime-state) @@ -3000,6 +3014,44 @@ (should (= gc-cons-threshold (* 512 1024 1024)))) (ebox--deferred-render-gc-restore)))) +(ert-deftest ebox-interactive-commit-owns-deferred-render-gc () + "Keep GC outside an interactive declarative commit and defer restoration." + (ebox-test--reset-runtime-state) + (let ((buffer nil)) + (unwind-protect + (progn + (setq buffer + (ebox-render-to-buffer + (generate-new-buffer-name " *ebox-test*") + (ebox-create :content "before"))) + (let ((noninteractive nil) + (gc-cons-threshold 1000) + (gc-cons-percentage 0.1) + (ebox-deferred-render-gc-cons-threshold 4000) + (ebox-render-gc-cons-percentage 0.8) + (ebox--deferred-render-gc-state nil) + (ebox--deferred-render-gc-timer nil) + (ebox--deferred-render-gc-depth 0) + observed-threshold) + (unwind-protect + (progn + (ebox-commit + buffer (ebox-create :content "after") + (lambda (_report) + (setq observed-threshold gc-cons-threshold))) + (should (= observed-threshold 4000)) + (should (= gc-cons-threshold 4000)) + (should (= gc-cons-percentage 0.8)) + (should ebox--deferred-render-gc-state) + (should (timerp ebox--deferred-render-gc-timer))) + (when (timerp ebox--deferred-render-gc-timer) + (cancel-timer ebox--deferred-render-gc-timer)) + (setq ebox--deferred-render-gc-state nil + ebox--deferred-render-gc-timer nil + ebox--deferred-render-gc-depth 0)))) + (when (and buffer (buffer-live-p buffer)) + (kill-buffer buffer))))) + (ert-deftest ebox-deferred-render-gc-can-raise-an-active-burst-budget () "Measured layout pressure should only raise an active burst budget." (let ((gc-cons-threshold 1000) diff --git a/tests/ebox-surface-tests.el b/tests/ebox-surface-tests.el index 1c3028a..8592d10 100644 --- a/tests/ebox-surface-tests.el +++ b/tests/ebox-surface-tests.el @@ -60,7 +60,7 @@ ((symbol-function 'window-body-height) (lambda (_window) 31))) (let ((values (ebox-surface--context-values buffer nil nil))) - (should (= 776 (plist-get values :viewport-width))) + (should (= 775 (plist-get values :viewport-width))) (should (= 31 (plist-get values :viewport-height))))) (kill-buffer buffer)))) @@ -93,10 +93,24 @@ ((symbol-function 'window-body-height) (lambda (_window) 62))) (let ((values (ebox-surface--context-values buffer nil nil))) - (should (= 1429 (plist-get values :viewport-width))) + (should (= 1428 (plist-get values :viewport-width))) (should (= 62 (plist-get values :viewport-height))))) (kill-buffer buffer)))) +(ert-deftest ebox-surface-window-width-reserves-two-display-columns () + "Reserve both continuation columns from a live GUI content width." + (cl-letf (((symbol-function 'window-body-width) + (lambda (_window pixelwise) + (should pixelwise) + 987)) + ((symbol-function 'window-pixel-width) + (lambda (_window) 987)) + ((symbol-function 'window-frame) + (lambda (_window) nil)) + ((symbol-function 'frame-char-width) + (lambda (&optional _frame) 8))) + (should (= (ebox-surface--window-content-width (selected-window)) 971)))) + (ert-deftest ebox-surface-context-prefers-selected-target-window () "Ignore stale cross-frame lookup when selected window shows BUFFER." (let* ((buffer (generate-new-buffer " *ebox-selected-viewport-test*")) @@ -119,7 +133,7 @@ (should (eq candidate window)) 60))) (let ((values (ebox-surface--context-values buffer nil nil))) - (should (= 1399 (plist-get values :viewport-width))) + (should (= 1398 (plist-get values :viewport-width))) (should (= 60 (plist-get values :viewport-height)))))) (when (window-live-p window) (set-window-buffer window old-buffer)) (kill-buffer buffer)))) @@ -150,7 +164,7 @@ ((symbol-function 'window-body-height) (lambda (_window) 31))) (let ((values (ebox-surface--context-values buffer nil nil))) - (should (= 900 (plist-get values :viewport-width))) + (should (= 899 (plist-get values :viewport-width))) (should (= 31 (plist-get values :viewport-height))))) (kill-buffer buffer) (when (get-buffer " *other-window*")