;;; ebox-layout.el --- Generic layout composition for Ebox -*- lexical-binding: t; -*- ;;; Commentary: ;; Owns generic box, row, column, stack, and spacer layout operations. It does ;; not own flex/grid formatting or Emacs buffer patch execution. ;;; Code: (require 'cl-lib) (require 'seq) (require 'subr-x) (require 'ebox-layout-config) (require 'ebox-tree) (require 'ebox-measure) (require 'ebox-fragment) (require 'ebox-render-context) (require 'ebox-size) (require 'ebox-buffer-backend) (require 'ebox-runtime-index) (declare-function ekp-pixel-justify "ekp" (string line-pixel)) (declare-function ebox--render-grid "ebox-grid" (node)) (declare-function ebox--render-grid-box-children "ebox-grid" (box props children)) (declare-function ebox--render-flex "ebox-flex" (node)) (declare-function ebox--render-flex-box-children "ebox-flex" (box props children)) (declare-function ebox--render-with-cache "ebox-incremental" (node &optional force cache-probe)) (declare-function ebox-create "ebox" (&rest plist)) (declare-function ebox-get "ebox" (box property)) (declare-function ebox--ensure-node-id "ebox" (node)) (declare-function ebox--ensure-region-id "ebox" (box)) (declare-function ebox--scroll-clear-state "ebox" (region-id)) (declare-function ebox--scroll-set-state "ebox" (region-id state)) (declare-function ebox--apply-hidden-visibility "ebox" (string)) (declare-function ebox-string-lines "ebox" (string)) (declare-function ebox-string-height "ebox" (string)) (declare-function ebox-lines-join "ebox" (lines)) (declare-function ebox--maplines "ebox" (function string)) (declare-function ebox--line-has-non-content-properties-p "ebox" (line)) (declare-function ebox--line-content-metadata-uniform-p "ebox" (line)) (declare-function ebox--lines-align-vertical "ebox" (string height align)) (declare-function ebox--pad-lines-vertical "ebox" (lines height &optional offset padding-string)) (declare-function ebox--vertical-alignment-offset "ebox" (line-count height align)) (declare-function ebox--lines-concat-horizontal "ebox" (&rest strings)) (declare-function ebox--lines-stack-vertical "ebox" (&rest strings)) (declare-function ebox--add-content-owner "ebox-buffer-backend" (string region-id)) (declare-function ebox--pixel-border "ebox-buffer-backend" (pixel-width height &optional color)) (declare-function ebox--propertize-color "ebox-buffer-backend" (string color)) (declare-function ebox--propertize-colors "ebox-buffer-backend" (string color bgcolor)) (declare-function ebox--propertize-content-line "ebox-buffer-backend" (line region-id idx fallback)) (declare-function ebox--propertize-overline "ebox-buffer-backend" (string &optional color)) (declare-function ebox--propertize-region "ebox-buffer-backend" (string property region-id)) (declare-function ebox--propertize-typography "ebox-buffer-backend" (string style)) (declare-function ebox--propertize-text-decoration "ebox-buffer-backend" (string style)) (declare-function ebox--propertize-underline "ebox-buffer-backend" (string &optional color)) (declare-function ebox-buffer-validate-border-capability "ebox-buffer-backend" (style)) (defvar ebox--render-runtime-revision nil "Current buffer runtime revision for render-local cache validation.") (defvar ebox--render-source-index nil "Read-only source generation for the current private render extent.") (defvar ebox--render-source-generations nil "Ordered read-only source generations available to one proof transaction.") (defvar ebox--render-source-states nil "Ordered retained runtime states available to one proof transaction.") (defvar ebox--region-id-counter) (defvar ebox--runtime-node-id-counter) (defvar ebox--scroll-global-state) (defvar ebox--scroll-idle-prefetch-timers) (defvar ebox--smooth-scroll-state-table) (defvar ebox--region-box-table) (defvar ebox--render-region-id) (defvar ebox--defer-scroll-content-index) (defvar ebox--intrinsic-layout-measurement nil "Non-nil while measuring intrinsic layout contributions. Descendants still see the current containing block, but stack/column containers must not stretch their own outer width to that containing block.") (defvar ebox--inline-auto-width-intrinsic-p nil "Non-nil while a horizontal layout sizes auto-width children intrinsically. Explicit viewport units keep their root reference; `stretch' fills the containing block. An omitted or `auto' width must not consume the whole horizontal containing block merely because the row is rendered in it.") (defvar ebox--box-content-render-cache nil "Dynamic render-pass cache for lazy content and empty-content widths.") (defvar ebox--box-content-empty-width nil "Inline extent returned by the current empty content materialization. The content cache saves this fact with its node and render context because empty strings cannot carry distinct geometry through string identity.") (defvar ebox--box-content-cache-purpose nil "Intrinsic probe depth, or nil for ordinary content materialization.") (defvar ebox--containing-wrap-mode nil "Box-owned soft-wrap policy inherited by canonical Text descendants.") (defvar ebox--rendered-uniform-width-table (make-hash-table :test 'eq :weakness 'key) "Weak map from rendered strings to their proven uniform pixel width.") (defvar ebox--rendered-intrinsic-size-table (make-hash-table :test 'eq :weakness 'key) "Weak map from rendered strings to intrinsic extents excluding overflow.") (defvar ebox--transparent-preformatted-box-fast-path-disabled nil "Non-nil disables transparent preformatted box reuse for diagnostics.") (defvar ebox--flat-preformatted-box-fast-path-disabled nil "Non-nil disables one-pass preformatted wrapper rendering for diagnostics.") (declare-function ebox--strip-paint-origins! "ebox-render-context" (string)) (defun ebox--record-rendered-uniform-width (rendered pixel-width) "Record that every line of RENDERED is exactly PIXEL-WIDTH wide." (when (and (stringp rendered) (not (string-empty-p rendered)) (numberp pixel-width)) (puthash rendered pixel-width ebox--rendered-uniform-width-table)) rendered) (defun ebox--record-rendered-intrinsic-size (rendered width height) "Record RENDERED's definite intrinsic WIDTH pixels and HEIGHT lines. Nil on either axis leaves its ordinary string measurement in force. Layout uses this fact when cyclic percentage gaps produce ink outside the intrinsic container. The weak string association keeps candidate facts out of source nodes and remains valid when rendered content is reused from a cache. Empty content instead returns WIDTH through `ebox--box-content-empty-width'." (when (stringp rendered) (if (string-empty-p rendered) (setq ebox--box-content-empty-width width) (when (or (numberp width) (numberp height)) (puthash rendered (list :width width :height height) ebox--rendered-intrinsic-size-table)))) rendered) (defun ebox--rendered-intrinsic-size (rendered axis) "Return RENDERED's recorded intrinsic AXIS extent, or nil." (unless (or (null rendered) (string-empty-p rendered)) (plist-get (gethash rendered ebox--rendered-intrinsic-size-table) axis))) (defun ebox--preformatted-content-lines-fit-p (lines pixel-width) "Return non-nil when LINES fit the exact content PIXEL-WIDTH. Blank lines without data-bearing properties are safe because the wrapper replaces them with its full-width filler line." (cl-every (lambda (line) (or (and (string-blank-p line) (not (ebox--line-has-non-content-properties-p line))) (= (ebox--string-pixel-width line) pixel-width))) lines)) (defun ebox--every-rendered-line-nonempty-p (rendered) "Return non-nil when every line in RENDERED can carry text properties." (let ((start 0) (length (length rendered)) valid) (setq valid (> length 0)) (while (and valid (<= start length)) (let ((end (or (string-match "\n" rendered start) length))) (setq valid (< start end)) (setq start (1+ end)))) valid)) (defun ebox--finish-preformatted-content-ownership! (rendered region-id) "Finish line ownership for REGION-ID in pre-owned RENDERED." (let ((start 0) (index 0) (length (length rendered))) (while (<= start length) (let ((end (or (string-match "\n" rendered start) length))) (unless (text-property-not-all start end 'ebox-content nil rendered) (add-text-properties start end (list 'ebox-content region-id 'ebox-content-idx index) rendered)) (when (< end length) (remove-text-properties end (1+ end) '(ebox-content-owner nil ebox-content-owners nil) rendered)) (setq start (1+ end) index (1+ index))))) rendered) (defun ebox--apply-surface-properties (rendered properties) "Apply text-property PROPERTIES to RENDERED's character surface. Newlines are excluded. Inner values win independently; explicit nil also blocks enclosing values for Ebox's fixed native interaction properties." (when properties (unless (and (proper-list-p properties) (zerop (% (length properties) 2))) (error "ebox: :surface-properties must be an even property list")) (let ((tail properties)) (while tail (unless (symbolp (car tail)) (error "ebox: surface property name must be a symbol: %S" (car tail))) (setq tail (cddr tail)))) (let ((line-start 0) (limit (length rendered))) (while (< line-start limit) (let ((line-end (or (string-match "\n" rendered line-start) limit)) (tail properties)) (while tail (let ((property (pop tail)) (value (pop tail))) (ebox-interaction--fill-property rendered line-start line-end property value))) (setq line-start (if (< line-end limit) (1+ line-end) limit)))))) (ebox-interaction--refresh-hover! rendered 0 (length rendered))) (defun ebox--render-transparent-preformatted-box (box) "Render BOX by owning proven exact preformatted child content directly. Return nil unless BOX has no visual chrome or sizing behavior that requires the generic box pipeline." (let ((layout-kind (ebox--box-layout-kind box))) (when (and (not ebox--transparent-preformatted-box-fast-path-disabled) (or (ebox--box-content-node box) (memq layout-kind '(row column flex grid))) (not (eq (ebox-get box :visibility) 'hidden)) (or (ebox-style-no-soft-wrap-p (ebox-get box :wrap-mode)) ;; Typed Layout children are already a preformatted spatial ;; result. Their owning Box must not send that result back ;; through ordinary text wrapping merely because the public ;; Box default is word wrapping. (memq layout-kind '(row column flex grid))) (eq (ebox-get box :text-align) 'left) (eq (ebox-get box :vertical-align) 'top) (memq (ebox-get box :height) '(nil auto)) (zerop (ebox--resolve-size-content-height box (ebox-get box :min-height) 0)) (memq (ebox-get box :max-height) '(nil none)) (eq (ebox-get box :overflow) 'scroll) (equal (ebox-get box :padding-left-pixel) 0) (equal (ebox-get box :padding-right-pixel) 0) (equal (ebox-get box :padding-top-height) 0) (equal (ebox-get box :padding-bottom-height) 0) (equal (ebox-get box :margin-left-pixel) 0) (equal (ebox-get box :margin-right-pixel) 0) (equal (ebox-get box :margin-top-height) 0) (equal (ebox-get box :margin-bottom-height) 0) (equal (ebox-get box :border-left-pixel) 0) (equal (ebox-get box :border-right-pixel) 0) (= (ebox-get box :border-top-pixel) 0) (= (ebox-get box :border-bottom-pixel) 0) (null (and (ebox-style--text-paint-owner-p box) (ebox-get box :color))) (null (ebox-get box :bgcolor))) (let* ((content (ebox--box-content box)) (known-width (gethash content ebox--rendered-uniform-width-table)) (content-width (and (numberp known-width) (ebox--content-pixel box)))) (when (and (numberp known-width) (= known-width content-width) (ebox--every-rendered-line-nonempty-p content)) (let* ((region-id (or ebox--render-region-id (ebox--ensure-region-id box))) (rendered (ebox--add-content-owner content region-id))) (ebox--finish-preformatted-content-ownership! rendered region-id) (ebox--apply-surface-properties rendered (ebox-interaction--node-surface-properties box)) (puthash region-id box ebox--region-box-table) (unless ebox--intrinsic-layout-measurement (ebox--scroll-clear-state region-id)) (ebox--record-rendered-uniform-width rendered known-width))))))) (defun ebox--render-flat-preformatted-box (box) "Render simple BOX chrome in a single line-oriented pass. Return nil unless BOX has no child layout, no margins or vertical clipping, and its formatted content exactly fills the used height." (when (and (not ebox--flat-preformatted-box-fast-path-disabled) (not (eq (ebox-get box :visibility) 'hidden)) (null (ebox--box-content-node box)) (not (plist-member box :children)) (stringp (ebox-get box :content)) (eq (ebox-get box :vertical-align) 'top) (zerop (ebox--resolve-size-content-height box (ebox-get box :min-height) 0)) (memq (ebox-get box :max-height) '(nil none)) (equal (or (ebox-get box :scroll-offset) 0) 0) (equal (ebox-get box :margin-left-pixel) 0) (equal (ebox-get box :margin-right-pixel) 0) (equal (ebox-get box :margin-top-height) 0) (equal (ebox-get box :margin-bottom-height) 0)) (let* ((content (ebox--box-content box)) (exact-p (and (plist-get box :ebox-content-width-exact-p) (ebox-style-no-soft-wrap-p (ebox-get box :wrap-mode)) (eq (ebox-get box :text-align) 'left))) (formatted (if exact-p content (ebox--format-content-string box content))) (lines (ebox-string-lines formatted)) (text-height (length lines)) (content-height (ebox--content-height box text-height formatted)) (content-pixel (ebox--content-pixel box)) (lines-fit-p (ebox--preformatted-content-lines-fit-p lines content-pixel)) (underfilled-simple-scroll-p (and lines-fit-p (< text-height content-height) (eq (ebox-get box :overflow) 'scroll) (= (ebox-get box :padding-left-pixel) 0) (= (ebox-get box :padding-right-pixel) 0) (= (floor (ebox-get box :padding-top-height)) 0) (= (floor (ebox-get box :padding-bottom-height)) 0) (= (ebox-get box :border-left-pixel) 0) (= (ebox-get box :border-right-pixel) 0) (= (ebox-get box :border-top-pixel) 0) (= (ebox-get box :border-bottom-pixel) 0)))) (when (and lines-fit-p (or (= text-height content-height) underfilled-simple-scroll-p)) (let* ((region-id (or ebox--render-region-id (ebox--ensure-region-id box))) (padding-line-filler (ebox--pixel-blank content-pixel 1)) (rendered (if underfilled-simple-scroll-p (ebox-lines-join (ebox--render-underfilled-simple-scroll-lines lines region-id content-height padding-line-filler (and (ebox-style--text-paint-owner-p box) (ebox-get box :color)) (ebox-get box :bgcolor))) (ebox-lines-join (ebox--window-render-flat-wrapper-chunk-lines box lines 0 region-id padding-line-filler t t))))) (when underfilled-simple-scroll-p (ebox--apply-surface-properties rendered (ebox-interaction--node-surface-properties box))) (puthash region-id box ebox--region-box-table) (unless ebox--intrinsic-layout-measurement (ebox--scroll-clear-state region-id)) (ebox--record-rendered-uniform-width rendered (+ content-pixel (ebox--side-pixel box)))))))) (defvar ebox--scroll-window-render-disabled nil "Non-nil disables lazy scroll-window rendering for full materialization.") (defvar ebox--scroll-window-render-result nil "Dynamic metadata for the current box's lazy scroll-window render.") (defvar ebox--scroll-window-cached-state nil "Dynamic staged scroll state used by a bounded cached-window projection.") (defvar ebox--scroll-window-cached-rendered-lines-p nil "Non-nil when cached scroll lines already carry wrapper ownership.") (defvar ebox--scroll-window-skip-state-rebuild-p nil "Non-nil when a scroll projection already staged its derived line indexes.") (defvar ebox--scroll-rendered-content-lines-private-p nil "Non-nil when scroll content line inputs may receive ownership in place.") (defvar ebox--propertize-private-content-line-p) (defvar ebox--window-line-renderer-table (make-hash-table :test 'eq) "Node-type keyed renderers for lazy scroll window line production.") (defvar ebox--window-line-prewarmer-table (make-hash-table :test 'eq) "Node-type keyed prewarmers for yielding lazy scroll production.") (defvar ebox--scroll-window-yielding-prewarm-p nil "Non-nil lets an idle prefix slice prepare one atomic child then yield.") (defvar ebox--scroll-window-prewarm-state nil "Prefix-local state table used by yielding window-line prewarmers.") (defvar ebox-scroll-lazy-prefix-lookahead-lines) (defvar ebox-wheel-scroll-step) (defvar ebox--scroll-window-initial-lookahead-lines-override nil "Optional exact lookahead used by latency-sensitive root reflows.") (defvar ebox--render-cache-allow-viewport-dependent) (defvar ebox--render-cache-signature-cache) (defvar ebox--render-cache-table) (defvar ebox--render-cache-scroll-state-region-ids) (defvar ebox--render-cache-scroll-state-restorable-p) (defvar ebox--render-cache-scroll-state-retained-cost-cache) (declare-function ebox-style-cascade-active-p "ebox-style" ()) (declare-function ebox--add-render-face! "ebox-render-context" (string start end face &optional append)) (defun ebox--register-window-line-renderer (type function) "Register FUNCTION as TYPE's lazy window-line renderer. FUNCTION receives a node and a line limit. It should return a plist with `:lines' and `:complete', or nil when the node needs normal rendering. When returned lines are not already target-width lines, the plist may include `:pixel-width' so the generic stack window can pad them. Incremental producers may also return an exact `:prefix-delta' with equal-length rewrite and append payloads; in that case they must retain the returned list's cons prefix across extensions." (unless (symbolp type) (error "ebox: window-line renderer type must be a symbol: %S" type)) (unless (functionp function) (error "ebox: window-line renderer must be a function: %S" function)) (puthash type function ebox--window-line-renderer-table) function) (defun ebox--register-window-line-prewarmer (type function) "Register FUNCTION as TYPE's yielding lazy line prewarmer." (unless (symbolp type) (error "ebox: window-line prewarmer type must be a symbol: %S" type)) (unless (functionp function) (error "ebox: window-line prewarmer must be a function: %S" function)) (puthash type function ebox--window-line-prewarmer-table) function) (defun ebox--prewarm-node-window-line (node target-width) "Prepare at most one atomic child of NODE for TARGET-WIDTH. Return non-nil only when the caller should yield without publishing a line. Single-child wrappers are transparent to this scheduling boundary." (when (and ebox--scroll-window-yielding-prewarm-p (hash-table-p ebox--scroll-window-prewarm-state) (listp node) (not (stringp node))) (let* ((type (plist-get node :ebox-type)) (prewarmer (gethash type ebox--window-line-prewarmer-table))) (if prewarmer (funcall prewarmer node target-width) (let ((children (ebox-tree--children-raw node))) (when (and children (null (cdr children))) (ebox--prewarm-node-window-line (car children) target-width))))))) (defun ebox--render-node-window-lines (node limit) "Return NODE's lazy window lines through the registered type renderer." (when-let* ((type (and (listp node) (if (and (eq (plist-get node :ebox-kind) 'box) (when-let* ((config (plist-get node :ebox-layout-config))) (eq (ebox-layout-config-kind config) 'flex))) 'flex (plist-get node :ebox-type)))) (renderer (gethash type ebox--window-line-renderer-table))) (funcall renderer node limit))) (defun ebox--scroll-lazy-prefix-lookahead-lines () "Return the current lazy scroll prefix lookahead in lines." (if (boundp 'ebox-scroll-lazy-prefix-lookahead-lines) (max 0 (or ebox-scroll-lazy-prefix-lookahead-lines 0)) 8)) (defun ebox--scroll-window-initial-lookahead-lines () "Return extra lazy scroll lines retained after initial render." (if (numberp ebox--scroll-window-initial-lookahead-lines-override) (max 0 ebox--scroll-window-initial-lookahead-lines-override) (let ((prefix-lookahead (ebox--scroll-lazy-prefix-lookahead-lines)) (wheel-lookahead (if (boundp 'ebox-wheel-scroll-step) (max 0 (or ebox-wheel-scroll-step 0)) 16))) (if (and ebox--defer-scroll-content-index (boundp 'ebox-wheel-smooth-scroll) ebox-wheel-smooth-scroll) (max wheel-lookahead (+ prefix-lookahead (if (boundp 'ebox-wheel-smooth-scroll-lines-per-tick) (max 1 (or ebox-wheel-smooth-scroll-lines-per-tick 1)) 1))) (+ prefix-lookahead wheel-lookahead))))) (defun ebox--scroll-rendered-content-lines (box lines region-id &optional start-index) "Return LINES as rendered scroll content lines for BOX and REGION-ID. START-INDEX is the first source line index represented by LINES." (let* ((padding-line-filler (ebox-pixel-space (ebox--content-pixel box))) (color (and (ebox-style--text-paint-owner-p box) (ebox-get box :color))) (bgcolor (ebox-get box :bgcolor)) (rendered (cl-loop for line in lines for idx from (or start-index 0) collect (let* ((colored-p (or color bgcolor)) (owned-source (if colored-p (ebox--propertize-colors (if (string-empty-p line) padding-line-filler line) color bgcolor) line))) (let ((ebox--propertize-private-content-line-p (or colored-p ebox--scroll-rendered-content-lines-private-p))) (ebox--propertize-content-line owned-source region-id idx padding-line-filler)))))) rendered)) (defun ebox--render-underfilled-simple-scroll-lines (lines region-id content-height padding-line-filler color bgcolor) "Finish chrome-free top-aligned underfilled scroll LINES in one pass." (let ((ebox--propertize-private-content-line-p t) ;; Resolve paint inputs once, as `ebox--propertize-colors' does, while ;; retaining the private-line, in-place finishing pass below. (foreground-face (and color (ebox-buffer--color-face color 'foreground))) (background-face (and bgcolor (ebox-buffer--color-face bgcolor 'background))) rendered-lines) (cl-labels ((finish-line (line index source-p) (when source-p (setq line (ebox--propertize-content-line line region-id index padding-line-filler))) (when (and (string-blank-p line) (ebox--line-content-metadata-uniform-p line) (not (ebox--line-has-non-content-properties-p line))) (let ((content-id (get-text-property 0 'ebox-content line)) (content-idx (get-text-property 0 'ebox-content-idx line))) (setq line (if content-id (propertize padding-line-filler 'ebox-content content-id 'ebox-content-idx content-idx 'ebox-content-owner region-id) (propertize padding-line-filler 'ebox-content region-id 'ebox-content-idx 0 'ebox-content-owner region-id))))) ;; `ebox-string-lines' made these source lines private, and blank ;; normalization above creates a fresh filler. Apply the same face ;; properties in place instead of copying every viewport-wide line. (when foreground-face (ebox--add-render-face! line 0 (length line) foreground-face t)) (when background-face (ebox--add-render-face! line 0 (length line) background-face t)) line)) (cl-loop for line in lines for index from 0 do (push (finish-line line index t) rendered-lines)) (dotimes (_ (max 0 (- content-height (length lines)))) (push (finish-line padding-line-filler 0 nil) rendered-lines))) (nreverse rendered-lines))) (defun ebox--scroll-rendered-prefix-through-content (box content-lines content-count region-id rendered-lines rendered-count rendered-tail) "Extend RENDERED-LINES through CONTENT-COUNT and return cache metadata. The result is (LINES COUNT TAIL). Reuse the existing rendered prefix when it is aligned with CONTENT-LINES; otherwise rebuild the bounded content cache." (cond ((>= rendered-count content-count) (list rendered-lines rendered-count rendered-tail)) ((and rendered-lines (<= rendered-count content-count)) (let* ((suffix (seq-take (nthcdr rendered-count content-lines) (- content-count rendered-count))) (rendered-suffix (ebox--scroll-rendered-content-lines box suffix region-id rendered-count))) (pcase-let ((`(,new-lines ,new-tail) (ebox--line-list-append! rendered-lines rendered-tail rendered-suffix))) (list new-lines content-count new-tail)))) (t (let ((rebuilt (ebox--scroll-rendered-content-lines box content-lines region-id 0))) (list rebuilt content-count (last rebuilt)))))) (defun ebox--lines-justify (string pixel-width &optional align) "Justify lines of STRING to PIXEL-WIDTH." (let ((align (or align 'left))) (if (eq align 'left) (ebox--maplines (lambda (line) (let ((line-width (ebox--string-pixel-width line))) (if (>= line-width pixel-width) line (concat line (ebox-pixel-space (- pixel-width line-width)))))) string) (ebox--maplines (lambda (line) (ebox--pixel-reach line pixel-width align)) string)))) (defsubst ebox--cjk-char-p (char) "Check if CHAR is a CJK (Chinese, Japanese, Korean) character." (and char (>= char #x3000) (or (<= char #x303F) ; CJK Symbols and Punctuation (and (>= char #x3040) (<= char #x309F)) ; Hiragana (and (>= char #x30A0) (<= char #x30FF)) ; Katakana (and (>= char #x3400) (<= char #x4DBF)) ; CJK Extension A (and (>= char #x4E00) (<= char #x9FFF)) ; CJK Unified Ideographs (and (>= char #xFF00) (<= char #xFFEF))))) ; Fullwidth Forms (defsubst ebox--combining-char-p (char) "Return non-nil when CHAR is a combining mark." (and char (or (and (>= char #x0300) (<= char #x036F)) (and (>= char #x1AB0) (<= char #x1AFF)) (and (>= char #x1DC0) (<= char #x1DFF)) (and (>= char #x20D0) (<= char #x20FF)) (and (>= char #xFE20) (<= char #xFE2F))))) (defsubst ebox--variation-selector-char-p (char) "Return non-nil when CHAR is a Unicode variation selector." (and char (or (and (>= char #xFE00) (<= char #xFE0F)) (and (>= char #xE0100) (<= char #xE01EF))))) (defsubst ebox--emoji-modifier-char-p (char) "Return non-nil when CHAR is an emoji modifier." (and char (>= char #x1F3FB) (<= char #x1F3FF))) (defsubst ebox--regional-indicator-char-p (char) "Return non-nil when CHAR is a regional indicator symbol." (and char (>= char #x1F1E6) (<= char #x1F1FF))) (defsubst ebox--grapheme-extension-char-p (char) "Return non-nil when CHAR extends the previous grapheme cluster." (or (ebox--combining-char-p char) (ebox--variation-selector-char-p char) (ebox--emoji-modifier-char-p char) (eq char #x200D))) (defsubst ebox--grapheme-cluster-end (string start) "Return the end index of the grapheme cluster at START in STRING. This handles the common clusters that matter for UI text: combining marks, variation selectors, emoji modifiers, ZWJ sequences, and regional-indicator pairs. It is intentionally small; full Unicode segmentation is out of scope." (let* ((len (length string)) (end (min len (1+ start)))) ;; Adjacent ASCII codepoints cannot form any cluster recognized below. ;; Keep the full Unicode path when a non-ASCII extension follows an ASCII ;; base, so combining marks retain exactly the same grouping. (unless (and (< (aref string start) 128) (or (= end len) (< (aref string end) 128))) (when (and (< end len) (ebox--regional-indicator-char-p (aref string start)) (ebox--regional-indicator-char-p (aref string end))) (setq end (1+ end))) (while (and (< end len) (or (ebox--grapheme-extension-char-p (aref string end)) (eq (aref string (1- end)) #x200D))) (setq end (1+ end)))) end)) (defun ebox--grapheme-clusters (string) "Return STRING as grapheme-cluster metadata plists." (let ((pos 0) (len (length string)) clusters) (while (< pos len) (let* ((end (ebox--grapheme-cluster-end string pos)) (text (substring string pos end))) (push (list :start pos :end end :text text :width (ebox--string-pixel-width text) :cjk (ebox--cjk-char-p (aref string pos)) :space (= (aref string pos) ?\s)) clusters) (setq pos end))) (vconcat (nreverse clusters)))) (defun ebox--max-char-pixel (string) "Return the pixel width of the widest character in STRING." (if (string-empty-p string) (ebox--space-pixel-width) (let ((max-width 0) (clusters (ebox--grapheme-clusters string))) (dotimes (i (length clusters)) (let ((width (plist-get (aref clusters i) :width))) (when (> width max-width) (setq max-width width)))) (max max-width (ebox--space-pixel-width))))) (defun ebox--clip-line (line max-pixel) "Clip LINE to MAX-PIXEL without splitting supported grapheme clusters. Retain visible text properties. A partially visible fixed display space keeps its properties and uses the remaining width; a glyph that cannot fit is omitted as a whole. Any remaining width is neutral box content, never an interactive stand-in for an omitted glyph." ;; The prefix-plus-pixel-filler algorithm follows kinney's GPL-3.0-or-later ;; `s-pixel-floor' and `s-pixel-left' (2026), extended here for graphemes, ;; source properties and clipped display spaces without an extra dependency. ;; https://github.com/Kinneyzhang/s-pixel ;; commit a2a0d6ae6b3bd71c1a70084d0ea37f56463e5fe0. (if (<= (ebox--string-pixel-width line) max-pixel) line (let ((position 0) (end (length line)) (width 0) clipped) (while (and (< position end) (not clipped)) (let* ((display (get-text-property position 'display line)) (space-width (ebox--display-space-width display)) (next (if (and display (not space-width)) ;; A replacement string or image is one display unit. (max (ebox--grapheme-cluster-end line position) (or (next-single-property-change position 'display line end) end)) (ebox--grapheme-cluster-end line position))) ;; Measure the complete prefix, preserving the existing font ;; measurement contract when adjacent glyph shaping matters. (next-width (ebox--substring-pixel-width line 0 next)) (remaining (- max-pixel width))) (if (<= next-width max-pixel) (setq position next width next-width) (setq clipped (concat (substring line 0 position) (when (> remaining 0) (let ((space (ebox-pixel-space remaining))) (when space-width (add-text-properties 0 (length space) (text-properties-at position line) space) (put-text-property 0 (length space) 'display (cons 'space (plist-put (copy-tree (cdr display)) :width (list remaining))) space)) space))))))) (or clipped (substring line 0 position))))) (defun ebox--wrap-line (line max-pixel wrap-mode) "Wrap a single LINE to MAX-PIXEL width, preserving text properties. WRAP-MODE can be `word', `char', or `kp'. CJK characters are always wrapped by char. For `kp' mode, see `ebox--wrap-text' which delegates to ekp package. Returns a list of strings (lines)." (cond ((or (null line) (string-empty-p line)) (list "")) ;; Fast path: line fits ((<= (ebox--string-pixel-width line) max-pixel) (list line)) ;; Wrap needed (t (let* ((clusters (ebox--grapheme-clusters line)) (cluster-count (length clusters)) (result-lines '()) (line-start 0) (current-width 0) (index 0)) (cl-labels ((cluster-start (i) (plist-get (aref clusters i) :start)) (cluster-width (i) (plist-get (aref clusters i) :width)) (cluster-cjk-p (i) (plist-get (aref clusters i) :cjk)) (cluster-space-p (i) (plist-get (aref clusters i) :space)) (flush-before (i) (let ((end (cluster-start i))) (when (< line-start end) (push (substring line line-start end) result-lines)) (setq line-start end current-width 0)))) (while (< index cluster-count) (let ((cluster-width (cluster-width index))) (cond ;; CJK: Wrap by character ((cluster-cjk-p index) (when (> (+ current-width cluster-width) max-pixel) (flush-before index)) (setq current-width (+ current-width cluster-width)) (setq index (1+ index))) ;; Word mode: Wrap by word ((eq wrap-mode 'word) (let ((word-end index) (word-width 0)) ;; Scan word (while (and (< word-end cluster-count) (not (cluster-space-p word-end)) (not (cluster-cjk-p word-end))) (setq word-width (+ word-width (cluster-width word-end))) (setq word-end (1+ word-end))) (cond ;; Word too long: split by char ((> word-width max-pixel) (while (< index word-end) (let ((cw (cluster-width index))) (when (> (+ current-width cw) max-pixel) (flush-before index)) (setq current-width (+ current-width cw)) (setq index (1+ index))))) ;; Word fits on current line ((<= (+ current-width word-width) max-pixel) (setq current-width (+ current-width word-width)) (setq index word-end)) ;; Word goes to next line (t (flush-before index) (setq current-width word-width) (setq index word-end))) ;; Handle trailing space (when (and (< index cluster-count) (cluster-space-p index)) (let ((space-width (cluster-width index))) (if (<= (+ current-width space-width) max-pixel) (progn (setq current-width (+ current-width space-width)) (setq index (1+ index))) (flush-before index) (setq index (1+ index)) (setq line-start (if (< index cluster-count) (cluster-start index) (length line)))))))) ;; Char mode (t (when (> (+ current-width cluster-width) max-pixel) (flush-before index)) (setq current-width (+ current-width cluster-width)) (setq index (1+ index)))))) ;; Flush remaining (when (< line-start (length line)) (push (substring line line-start) result-lines))) (nreverse result-lines))))) (defun ebox--wrap-text (text max-pixel wrap-mode) "Wrap multi-line TEXT to MAX-PIXEL using WRAP-MODE. When WRAP-MODE is nil, return TEXT without pre-wrapping. When WRAP-MODE is `kp', use Knuth-Plass algorithm via ekp package." (cond ((memq wrap-mode '(nil none)) text) ((eq wrap-mode 'kp) (unless (require 'ekp nil t) (error "Ebox :wrap-mode kp requires EKP; install the ekp package")) (unless (fboundp 'ekp-pixel-justify) (error "Ebox :wrap-mode kp requires EKP with ekp-pixel-justify; update EKP")) (ekp-pixel-justify text max-pixel)) (t (let* ((lines (ebox-string-lines text)) (wrapped (mapcan (lambda (l) (ebox--wrap-line l max-pixel wrap-mode)) lines))) (ebox-lines-join wrapped))))) ;;; ============================================================ ;;; Render: Box Rendering Logic ;;; ============================================================ (defun ebox--size-font-fact (box) "Return the effective font fact for BOX or the current layout node." (or (plist-get box :ebox-font-fact) ebox--css-font-fact)) (defun ebox--size-line-height (&optional box) "Return BOX's normal computed line height in pixels. The buffer backend places vertical geometry on whole text lines. The font projection supplies its normal line height; the display default is used when the node has no font override." (let* ((fact (ebox--size-font-fact box)) (metrics (and fact (ebox-font-fact-measurement-metrics fact))) (height (plist-get metrics :height))) (max 1 (or height (frame-char-height))))) (defun ebox--size-character-width (&optional box) "Return the advance width of digit zero in BOX's effective font." (let* ((fact (ebox--size-font-fact box)) (metrics (and fact (ebox-font-fact-measurement-metrics fact))) (face (and fact (ebox-font-measurement-face fact))) (probe (if face (propertize "0" 'face face) "0"))) (max 1 (or (plist-get metrics :zero-width) (ebox--string-pixel-width probe))))) (cl-defun ebox--size-pixels (value &optional box axis (percent-base nil percent-base-supplied-p)) "Resolve canonical size VALUE to pixels for BOX along AXIS. AXIS is `width' (the default) or `height'. PERCENT-BASE, when supplied, overrides the containing block basis, including nil for an indefinite axis. Return nil for an unresolved percentage or unavailable viewport dimension. Numbers here are private engine pixel values, never public author syntax." (if (numberp value) value (let ((dependencies (ebox-size-dependencies value))) (ebox-size-resolve value (list :percent-base (if percent-base-supplied-p percent-base (if (eq axis 'height) ebox--css-containing-height (ebox--viewport-pixel-width nil))) :viewport-width (if ebox--css-size-context-active-p ebox--css-viewport-width ebox-viewport-width) :viewport-height (if ebox--css-size-context-active-p ebox--css-viewport-height (and (numberp ebox-viewport-height) (* ebox-viewport-height (frame-char-height)))) :ch (and (memq 'ch dependencies) (ebox--size-character-width box)) :lh (and (memq 'lh dependencies) (ebox--size-line-height box))))))) (defun ebox--edge-used-value (box property value) "Resolve derived edge PROPERTY's canonical VALUE in BOX's context. CSS padding and margin percentages use containing block width on both axes. Vertical engine fields store fractional line counts until final rendering." (if (or (numberp value) (not (consp value))) value (let ((pixels (or (ebox--size-pixels value box 'width) 0))) (when (and (< pixels 0) (string-prefix-p ":margin-" (symbol-name property))) (error "ebox: negative margins require overlapping layout, which this buffer backend does not support: %S" value)) (setq pixels (max 0 pixels)) (if (string-suffix-p "-height" (symbol-name property)) (/ pixels (float (ebox--size-line-height box))) pixels)))) (defun ebox--side-pixel (box &optional side) "Calculate side pixel width (padding + border + margin). SIDE can be `left', `right', or nil (both)." (let ((left (+ (ebox-get box :padding-left-pixel) (ebox-get box :border-left-pixel) (ebox-get box :margin-left-pixel))) (right (+ (ebox-get box :padding-right-pixel) (ebox-get box :border-right-pixel) (ebox-get box :margin-right-pixel)))) (pcase side ('left left) ('right right) (_ (+ left right))))) (defun ebox--side-height (box) "Calculate side height (padding + margin) in lines." (+ (floor (ebox-get box :padding-top-height)) (floor (ebox-get box :padding-bottom-height)) (floor (ebox-get box :margin-top-height)) (floor (ebox-get box :margin-bottom-height)))) (defun ebox--box-sizing-content-pixel (box value) "Return VALUE converted to content pixels according to BOX sizing." (when value (if (eq (ebox-get box :box-sizing) 'border-box) (max 0 (- value (+ (ebox-get box :padding-left-pixel) (ebox-get box :padding-right-pixel) (ebox-get box :border-left-pixel) (ebox-get box :border-right-pixel)))) value))) (defun ebox--box-sizing-content-height (box value) "Return VALUE converted to content lines according to BOX sizing." (when value (if (eq (ebox-get box :box-sizing) 'border-box) (max 0 (- value (+ (ebox-get box :padding-top-height) (ebox-get box :padding-bottom-height)))) value))) (defun ebox--viewport-pixel-width (&optional default) "Return the current viewport width in pixels, or DEFAULT." (if (and (numberp ebox-viewport-width) (>= ebox-viewport-width 0)) ebox-viewport-width default)) (defun ebox--viewport-size-value-p (value) "Return non-nil when canonical VALUE depends on viewport width." (and (ebox-size-value-p value) (memq 'viewport-width (ebox-size-dependencies value)))) (defun ebox--horizontal-size-pixels (value &optional default) "Return canonical VALUE as horizontal pixels, or DEFAULT if unresolved." (if (or (numberp value) (ebox-size-value-p value)) (or (ebox--size-pixels value) default) default)) (defun ebox--nonnegative-horizontal-size-pixels (value &optional default) "Return VALUE as non-negative horizontal pixels. Signal an error when VALUE resolves to a negative width-like size." (let ((pixels (ebox--horizontal-size-pixels value default))) (when (and pixels (< pixels 0)) (error "ebox: width values cannot be negative: %S" value)) pixels)) (defun ebox--viewport-height-lines (&optional fallback) "Return the dynamically bound viewport height in lines, or FALLBACK." (if (and (numberp ebox-viewport-height) (> ebox-viewport-height 0)) (floor ebox-viewport-height) fallback)) (defun ebox--viewport-height-size-value-p (value) "Return non-nil when vertical size VALUE depends on viewport height." (and (ebox-size-value-p value) (memq 'viewport-height (ebox-size-dependencies value)))) (defun ebox--resolve-height-expression-lines (value &optional box) "Resolve vertical size expression VALUE to raw outer height lines. Return nil for an indefinite percentage or unavailable viewport height. BOX supplies the effective font. Numbers are private engine line counts." (if (numberp value) value (when-let* ((pixels (ebox--size-pixels value box 'height))) (/ pixels (float (ebox--size-line-height box)))))) (defun ebox--resolve-size-content-height (box value fallback &optional intrinsic) "Resolve vertical size VALUE to BOX content height lines. FALLBACK is used for auto and indefinite lengths. INTRINSIC is the natural content height after inline layout; normal horizontal writing gives the same block-axis min-content and max-content contribution." (cond ((memq value '(nil auto)) fallback) ((eq value 'none) nil) ((memq value '(min-content max-content fit-content)) (or intrinsic fallback)) ((eq value 'stretch) (if (numberp ebox--css-containing-height) (max 0 (- (/ ebox--css-containing-height (float (ebox--size-line-height box))) (ebox--side-height box))) fallback)) ((or (numberp value) (ebox-size-value-p value)) (if-let* ((height (ebox--resolve-height-expression-lines value box))) (ebox--box-sizing-content-height box (max 0 height)) fallback)) (t (error "ebox: unsupported height value after expansion: %S" value)))) (defun ebox--box-content-node (box) "Return BOX's lazy child layout node, if any." (plist-get box :ebox-content-node)) (defun ebox--box-layout-kind (box) "Return canonical BOX's typed layout kind, or nil for a legacy box." (when-let* ((config (plist-get box :ebox-layout-config))) (ebox-layout-config-kind config))) (defun ebox--box-content-cache-context () "Return the current render context for lazy box content caching." (list ebox-viewport-width ebox-viewport-height ebox--css-viewport-width ebox--css-viewport-height ebox--css-containing-height ebox--css-font-fact ebox--intrinsic-layout-measurement ebox--inline-auto-width-intrinsic-p ebox--containing-wrap-mode ebox--box-content-cache-purpose ebox--render-region-id)) (defun ebox--render-box-content-cached (box renderer) "Return BOX content from RENDERER, reusing the render-pass cache. Return its empty inline extent through `ebox--box-content-empty-width'." (let* ((context (and ebox--box-content-render-cache (ebox--box-content-cache-context))) (entries (and ebox--box-content-render-cache (gethash box ebox--box-content-render-cache))) (cached (assoc context entries)) (result (if cached (cdr cached) (let* ((ebox--box-content-empty-width nil) (rendered (or (funcall renderer) "")) (result (cons rendered (and (string-empty-p rendered) ebox--box-content-empty-width)))) (when ebox--box-content-render-cache (puthash box (cons (cons context result) entries) ebox--box-content-render-cache)) result)))) (setq ebox--box-content-empty-width (cdr result)) (car result))) (defun ebox--record-box-content-width-exact (box content composite-p) "Record exact width for BOX CONTENT when COMPOSITE-P is non-nil." (let ((content-viewport (ebox--wrapper-content-viewport-pixel box))) (when (and content-viewport composite-p (not ebox--intrinsic-layout-measurement) (equal (gethash content ebox--rendered-uniform-width-table) content-viewport)) (plist-put box :ebox-content-width-exact-p t)))) (defun ebox--render-box-content-node (box node) "Render NODE as BOX content, using the render-pass cache when available." (let ((content (ebox--render-box-content-cached box (lambda () (ebox--render-node-as-box-content node box))))) ;; A child layout has already resolved wrapping, alignment, and overflow ;; against CONTENT-VIEWPORT. Preserve that fact on the owning box so the ;; generic string formatter does not traverse the same propertized output ;; a second time. Intrinsic measurement is deliberately excluded: it ;; produces a natural-width probe, not final box content. (ebox--record-box-content-width-exact box content (not (eq (and (listp node) (plist-get node :ebox-type)) 'box))) content)) (defun ebox--normal-text-units (string wrap-mode) "Return inline flow units for Text STRING under WRAP-MODE." (let ((lines (ebox-string-lines string)) units) (cl-loop for line in lines for line-tail on lines do (if (eq wrap-mode 'char) (let ((clusters (ebox--grapheme-clusters line))) (dotimes (index (length clusters)) (let* ((cluster (aref clusters index)) (part (substring line (plist-get cluster :start) (plist-get cluster :end)))) (push (list :kind 'char :rendered part :width (plist-get cluster :width)) units)))) (let ((position 0) (limit (length line))) (while (< position limit) (let* ((space-p (not (null (memq (aref line position) '(?\s ?\t))))) (end position)) (while (and (< end limit) (eq (not (null (memq (aref line end) '(?\s ?\t)))) space-p)) (setq end (1+ end))) (let ((part (substring line position end))) (push (list :kind (if space-p 'space 'word) :rendered part :width (ebox--string-pixel-width part)) units)) (setq position end))))) unless (null (cdr line-tail)) do (push '(:kind break) units)) (nreverse units))) (defun ebox--render-normal-box-children (box children) "Render canonical CHILDREN without flattening inline Box boundaries." (catch 'rendered (when (and (= (length children) 1) (eq (plist-get (car children) :ebox-kind) 'text) (not (eq (ebox-get box :wrap-mode) 'kp))) ;; Resolve against the outer containing block. Inside the content ;; context a responsive width would subtract this Box's chrome twice. (let ((width (ebox--wrapper-content-viewport-pixel box))) (throw 'rendered (ebox--call-with-box-content-context box (lambda () (let* ((child (car children)) (wrap-mode (ebox-get box :wrap-mode)) (rendered (let ((ebox--inline-auto-width-intrinsic-p t)) (ebox--render-with-cache child)))) (if (and width (> width 0) (ebox-style-soft-wrap-p wrap-mode)) (ebox--wrap-text rendered width wrap-mode) rendered))))))) (let* ((resolved-width (ebox--wrapper-content-viewport-pixel box)) (content-width (and resolved-width (> resolved-width 0) resolved-width)) (wrap-mode (ebox-get box :wrap-mode)) (soft-wrap-p (not (memq wrap-mode '(nil none kp)))) (line-width 0) (max-width 0) pending-empty-line line-has-empty-box line-parts segments) (cl-labels ((flush-line () (when line-parts (let ((parts (nreverse line-parts))) (when line-has-empty-box (let ((height (apply #'max (mapcar (lambda (part) (if (consp part) 0 (ebox-string-height part))) parts)))) (setq parts (mapcar (lambda (part) (if (consp part) (ebox--pixel-blank (cdr part) height) part)) parts)))) (push (apply #'ebox--lines-concat-horizontal parts) segments)) (setq max-width (max max-width line-width)) (setq line-parts nil line-width 0 line-has-empty-box nil))) (append-atomic (rendered width) (when (and soft-wrap-p content-width line-parts (> (+ line-width width) content-width)) (flush-line)) (if (string-empty-p rendered) (progn (setq line-has-empty-box t) (push (cons rendered width) line-parts)) (push rendered line-parts)) (setq line-width (+ line-width width) pending-empty-line nil)) (append-word (rendered width) (if (or (not soft-wrap-p) (null content-width) (<= width content-width)) (append-atomic rendered width) (let ((parts (ebox--wrap-line rendered content-width 'char))) (cl-loop for part in parts for tail on parts do (append-atomic part (ebox--string-pixel-width part)) unless (null (cdr tail)) do (flush-line))))) (append-display-space (rendered width) (let ((remaining width) (properties (cl-loop for (key value) on (text-properties-at 0 rendered) by #'cddr unless (eq key 'display) append (list key value)))) (while (> remaining 0) (let* ((chunk (min remaining content-width)) (piece (ebox-pixel-space chunk))) (when properties (add-text-properties 0 (length piece) properties piece)) (append-atomic piece chunk) (setq remaining (- remaining chunk)) (when (> remaining 0) (flush-line)))))) (force-break () (if line-parts (flush-line) (push (ebox-lines-join '("")) segments)) (setq pending-empty-line t)) (append-unit (unit) (pcase (plist-get unit :kind) ('break (force-break)) ('space (let ((width (plist-get unit :width))) (cond ((and soft-wrap-p content-width (> width content-width)) (let ((rendered (plist-get unit :rendered))) (if (and (= (length rendered) 1) (get-text-property 0 'display rendered)) (append-display-space rendered width) (append-word rendered width)))) ((null line-parts) (append-atomic (plist-get unit :rendered) width)) ((and soft-wrap-p content-width (> (+ line-width width) content-width)) (flush-line)) (t (append-atomic (plist-get unit :rendered) width))))) ('word (append-word (plist-get unit :rendered) (plist-get unit :width))) ('char (append-word (plist-get unit :rendered) (plist-get unit :width))) ('box (append-atomic (plist-get unit :rendered) (plist-get unit :width))))) (render-child (child) (let* ((inline-p (eq (ebox-tree-display-outer child) 'inline)) (rendered (let ((ebox--inline-auto-width-intrinsic-p (eq (ebox-tree-display-outer child) 'inline))) (ebox--render-with-cache child))) (width (if (and (string-empty-p rendered) (eq (plist-get child :ebox-kind) 'box)) (let ((ebox--inline-auto-width-intrinsic-p inline-p)) (ebox--total-pixel child)) (when (or (not inline-p) (eq (plist-get child :ebox-kind) 'box)) (ebox--string-max-pixel-width rendered))))) (if (not inline-p) (progn (flush-line) (setq pending-empty-line nil max-width (max max-width width)) (push rendered segments)) (unless (and (string-empty-p rendered) (zerop (or width 0))) (if (eq (plist-get child :ebox-kind) 'text) (dolist (unit (ebox--normal-text-units rendered wrap-mode)) (append-unit unit)) (append-unit (list :kind 'box :rendered rendered :width width)))))))) (ebox--call-with-box-content-context box (lambda () (dolist (child children) (render-child child)))) (when pending-empty-line (push (ebox-lines-join '("")) segments) (setq pending-empty-line nil)) (flush-line)) (let ((rendered (or (and segments (apply #'ebox--lines-stack-vertical (nreverse segments))) ""))) (when (string-empty-p rendered) (setq ebox--box-content-empty-width max-width)) rendered)))) (defun ebox--render-box-layout-children (box) "Render canonical BOX children through its typed LayoutConfig." (let* ((kind (ebox--box-layout-kind box)) (children (ebox-tree-layout-children box)) (content (ebox--render-box-content-cached box (lambda () (pcase kind ('normal (ebox--render-normal-box-children box children)) ('row (ebox--call-with-box-content-context box (lambda () (ebox--render-row-children children (ebox-layout-config-props (plist-get box :ebox-layout-config)))))) ('column (let ((content-viewport (ebox--wrapper-content-viewport-pixel box))) (ebox--call-with-box-content-context box (lambda () (ebox--render-column-children children (ebox-layout-config-props (plist-get box :ebox-layout-config)) content-viewport))))) ('flex (ebox--render-flex-box-children box (ebox-layout-config-props (plist-get box :ebox-layout-config)) children)) ('grid (ebox--render-grid-box-children box (ebox-layout-config-props (plist-get box :ebox-layout-config)) children)) (_ (error "Ebox Box has unsupported LayoutConfig: %S" kind))))))) (plist-put box :ebox-content-layout-complete-p (not (and (eq kind 'normal) (eq (ebox-get box :wrap-mode) 'kp)))) (ebox--record-box-content-width-exact box content (or (memq kind '(row column flex grid)) (consp (cdr children)))) content)) (defun ebox--box-content (box) "Return BOX content, rendering any lazy child layout content if present." (let ((content (cond ((or (plist-member box :children) (plist-get box :ebox-child-sequence)) (ebox--render-box-layout-children box)) ((when-let* ((node (ebox--box-content-node box))) (ebox--render-box-content-node box node))) (t (ebox-get box :content))))) ;; Canonical Text leaves materialize the inherited font exactly once. ;; A canonical Box owns geometry/background and must not wrap the same ;; typography contribution around already-rendered descendants. (if (eq (plist-get box :ebox-kind) 'box) content (ebox--propertize-text-decoration (ebox--propertize-typography content box) box)))) (defun ebox--intrinsic-box-content (box) "Return BOX content materialized independently of its used width. A typed Normal or Column Box soft-wraps text while composing its children. Its intrinsic widths must therefore measure a separate unwrapped materialization, not the content previously composed for the final viewport. Cache that materialization on the source BOX for the current render pass, while applying probe-only properties to a shallow copy so the canonical node remains unchanged." (if (and (memq (ebox--box-layout-kind box) '(normal column)) (or (plist-member box :children) (plist-get box :ebox-child-sequence))) (let ((ebox-viewport-width nil) (ebox--intrinsic-layout-measurement t) (ebox--inline-auto-width-intrinsic-p t) ;; A child's used content rendered during an ancestor's probe ;; must not reuse that child's separate unwrapped materialization. (ebox--box-content-cache-purpose (1+ (or ebox--box-content-cache-purpose 0)))) (ebox--render-box-content-cached box (lambda () (let ((probe (copy-sequence box))) (plist-put probe :width 'auto) (plist-put probe :min-width nil) (plist-put probe :max-width nil) (plist-put probe :wrap-mode 'none) (plist-put probe :ebox-content-width-exact-p nil) (plist-put probe :ebox-content-layout-complete-p nil) (ebox--box-content probe))))) (ebox--box-content box))) (defun ebox--content-max-pixel (box) "Return BOX's max-content width in content pixels." (let* ((ebox--box-content-empty-width nil) (content (ebox--intrinsic-box-content box))) (or (and (string-empty-p content) ebox--box-content-empty-width) (ebox--rendered-intrinsic-size content :width) (let ((lines (ebox-string-lines content))) (if lines (apply #'max (mapcar #'ebox--string-pixel-width lines)) 0))))) (defun ebox--line-min-content-pixel (line) "Return LINE's min-content width in content pixels." (let ((position 0) (end (length line)) (max-width 0) (run-width 0)) (cl-labels ((flush-run () (when (> run-width max-width) (setq max-width run-width)) (setq run-width 0))) (while (< position end) (let* ((cluster-end (ebox--grapheme-cluster-end line position)) (char (aref line position))) (cond ;; A separator width never contributes to either adjacent run. ;; Skip the pixel query instead of measuring and discarding it. ((= char ?\s) (flush-run)) (t (let ((width (if (and (< char 128) (= cluster-end (1+ position)) (null (text-properties-at position line))) (ebox--char-pixel-width char) (ebox--substring-pixel-width line position cluster-end)))) (if (ebox--cjk-char-p char) (progn (flush-run) (when (> width max-width) (setq max-width width))) (setq run-width (+ run-width width)))))) (setq position cluster-end))) (flush-run)) max-width)) (defun ebox--content-min-pixel (box) "Return BOX's min-content width in content pixels." (let ((wrap-mode (ebox-get box :wrap-mode))) (if (ebox-style-no-soft-wrap-p wrap-mode) (ebox--content-max-pixel box) (let* ((ebox--box-content-empty-width nil) (content (ebox--intrinsic-box-content box))) (or (and (string-empty-p content) ebox--box-content-empty-width) (ebox--rendered-intrinsic-size content :width) (let ((lines (ebox-string-lines content)) (max-width 0)) (dolist (line lines max-width) (setq max-width (max max-width (ebox--line-min-content-pixel line)))))))))) (defun ebox--stretch-content-pixel (box) "Return BOX's stretch-fit content width, or nil without a viewport." (when-let* ((viewport (ebox--viewport-pixel-width nil))) (max 0 (- viewport (ebox--side-pixel box))))) (defun ebox--fit-content-pixel (box limit) "Return BOX's fit-content width in content pixels. LIMIT is already resolved to content pixels, or nil for stretch-fit." (let ((min-content (ebox--content-min-pixel box)) (max-content (ebox--content-max-pixel box)) (available (or limit (ebox--stretch-content-pixel box) (ebox--content-max-pixel box)))) (max min-content (min max-content available)))) (defun ebox--definite-content-size-pixel (box value) "Resolve VALUE to BOX content pixels when it is independent of content. Return nil for intrinsic sizes such as min-content and max-content." (cond ((null value) nil) ((or (numberp value) (ebox-size-value-p value)) (when-let* ((pixels (ebox--size-pixels value box 'width))) (ebox--box-sizing-content-pixel box (max 0 pixels)))) ((memq value '(stretch fit-content)) (ebox--stretch-content-pixel box)) (t nil))) (defun ebox--wrapper-content-viewport-pixel (props) "Return PROPS' content viewport for rendering preformatted child layouts. PROPS are already engine-normalized; do not pass them back through public property parsers. Normal and Column text flow uses its final intrinsic bounds; the separate unwrapped measurement keeps that resolution non-recursive." (if (and (memq (ebox--box-layout-kind props) '(normal column)) (cl-some (lambda (property) (memq (ebox-get props property) '(min-content max-content fit-content))) '(:width :min-width :max-width))) (ebox--content-pixel props) (let* ((width-value (ebox-get props :width)) (min-width (ebox--definite-content-size-pixel props (ebox-get props :min-width))) (max-width (ebox--definite-content-size-pixel props (ebox-get props :max-width))) (preferred-width (or (ebox--definite-content-size-pixel props width-value) (and (not ebox--inline-auto-width-intrinsic-p) (or (null width-value) (eq width-value 'auto) (eq width-value 'fit-content)) (ebox--stretch-content-pixel props)) max-width))) (when preferred-width (max (or min-width 0) (min (or max-width 999999999) (max 0 preferred-width))))))) (defun ebox--call-with-box-content-context (props function) "Call FUNCTION in the content viewport established by box PROPS." (let ((content-viewport (ebox--wrapper-content-viewport-pixel props)) (content-height (when-let* ((preferred (ebox--resolve-size-content-height props (ebox-get props :height) nil))) (max (ebox--resolve-size-content-height props (ebox-get props :min-height) 0) (min (or (ebox--resolve-size-content-height props (ebox-get props :max-height) nil) preferred) preferred)))) (ebox--containing-wrap-mode (ebox-get props :wrap-mode))) (let ((ebox--css-containing-height (and content-height (* content-height (ebox--size-line-height props))))) (cond ((and (eq (ebox-get props :width) 'max-content) (not (memq (ebox--box-layout-kind props) '(normal column)))) (let ((ebox-viewport-width nil) (ebox--intrinsic-layout-measurement t) (ebox--inline-auto-width-intrinsic-p nil)) (funcall function))) (content-viewport (let ((ebox-viewport-width content-viewport) (ebox--inline-auto-width-intrinsic-p nil)) (funcall function))) (t (funcall function)))))) (defun ebox--render-node-as-box-content (node props) "Render NODE as preformatted content for a box with PROPS. When PROPS establish a content-box viewport, nested auto and percentage widths resolve against that inner content box instead of the outer caller viewport. A max-content wrapper instead measures the composite NODE without an inherited viewport." (let ((content-viewport (ebox--wrapper-content-viewport-pixel props))) (ebox--call-with-box-content-context props (lambda () (if (and content-viewport (hash-table-p ebox--render-cache-table) (not ebox--intrinsic-layout-measurement) (null ebox--render-region-id) (fboundp 'ebox--render-cache-probe) (fboundp 'ebox--render-with-cache)) (ebox--render-with-cache node nil (ebox--render-cache-probe node nil (list :box-content t :content-viewport content-viewport :viewport-height ebox-viewport-height :intrinsic ebox--intrinsic-layout-measurement :auto-width-intrinsic ebox--inline-auto-width-intrinsic-p :render-region-id ebox--render-region-id :scroll-lookahead ebox--scroll-window-initial-lookahead-lines-override :scroll-disabled ebox--scroll-window-render-disabled))) (ebox--render-node node)))))) (defun ebox--resolve-size-content-pixel (box value fallback) "Resolve a horizontal size VALUE to BOX content pixels. FALLBACK is used for nil and auto values." (cond ((null value) fallback) ((or (numberp value) (ebox-size-value-p value)) (if-let* ((pixels (ebox--size-pixels value box 'width))) (ebox--box-sizing-content-pixel box (max 0 pixels)) fallback)) ((eq value 'auto) fallback) ((eq value 'none) nil) ((eq value 'min-content) (ebox--content-min-pixel box)) ((eq value 'max-content) (ebox--content-max-pixel box)) ((eq value 'fit-content) (ebox--fit-content-pixel box nil)) ((eq value 'stretch) (or (ebox--stretch-content-pixel box) fallback)) (t (error "ebox: unsupported width value after expansion: %S" value)))) (defun ebox--content-pixel (box) "Calculate content pixel width, respecting box-sizing." (let* ((width-value (ebox-get box :width)) ;; Intrinsic measurement is only a fallback for auto-like widths. ;; Definite and intrinsic keywords resolve themselves, so eagerly ;; measuring max-content here only repeats GUI pixel work without ;; affecting the used width. (preferred-width (cond ((or (null width-value) (eq width-value 'auto)) (or (and (not ebox--inline-auto-width-intrinsic-p) (ebox--stretch-content-pixel box)) (ebox--content-max-pixel box))) ((eq width-value 'stretch) (or (ebox--resolve-size-content-pixel box width-value nil) (ebox--content-max-pixel box))) (t (ebox--resolve-size-content-pixel box width-value nil)))) (min-width (ebox--resolve-size-content-pixel box (ebox-get box :min-width) 0)) (max-width (ebox--resolve-size-content-pixel box (ebox-get box :max-width) nil))) (max (or min-width 0) (min (or max-width 999999999) (max 0 (or preferred-width (ebox--content-max-pixel box))))))) (defun ebox--content-height (box text-height &optional rendered) "Calculate BOX content height from TEXT-HEIGHT, respecting box-sizing. RENDERED supplies an intrinsic extent when its visible overflow must not contribute to the containing block's automatic height." (let* ((text-height (or (ebox--rendered-intrinsic-size rendered :height) text-height)) (min-height (ebox--resolve-size-content-height box (ebox-get box :min-height) 0 text-height)) (max-height (ebox--resolve-size-content-height box (ebox-get box :max-height) nil text-height)) (fixed-height (ebox--resolve-size-content-height box (ebox-get box :height) nil text-height)) (raw-height (or fixed-height text-height))) ;; The buffer renderer consumes complete text lines. Preserve fractional ;; lengths through constraint arithmetic and quantize once at this boundary. (max 0 (floor (max min-height (min (or max-height 999999999) raw-height)))))) (defun ebox--border-carrier-height (box content-height) "Return extra lines needed to paint BOX's border at CONTENT-HEIGHT. Horizontal borders share existing content or padding lines. With neither, one carrier line preserves their paint without inventing a content line." (if (and (zerop (+ content-height (floor (ebox-get box :padding-top-height)) (floor (ebox-get box :padding-bottom-height)))) (or (> (ebox-get box :border-top-pixel) 0) (> (ebox-get box :border-bottom-pixel) 0))) 1 0)) (defun ebox--format-content-string (box content) "Format BOX CONTENT (wrap and justify) returning a string." (let* ((wrap-mode (ebox-get box :wrap-mode)) (align (ebox-get box :text-align)) (known-width (gethash content ebox--rendered-uniform-width-table)) (formatted (if (and (eq align 'left) (or (and (ebox-style-no-soft-wrap-p wrap-mode) (or (plist-get box :ebox-content-width-exact-p) (and (numberp known-width) (= known-width (ebox--content-pixel box))))) (and (memq wrap-mode '(word char)) (numberp known-width) (= known-width (ebox--content-pixel box))))) content (let* ((pixel-width (ebox--content-pixel box)) (wrapped-content (if (plist-get box :ebox-content-layout-complete-p) content (ebox--wrap-text content pixel-width wrap-mode)))) ;; KP distributes inner gaps; Ebox still owns the Box geometry. ;; Pad ragged lines back to the requested content width. (ebox--record-rendered-intrinsic-size (ebox--lines-justify wrapped-content pixel-width align) (ebox--rendered-intrinsic-size content :width) (ebox--rendered-intrinsic-size content :height)))))) (if (eq (ebox-get box :overflow) 'hidden) (let ((pixel-width (ebox--content-pixel box))) (ebox--record-rendered-intrinsic-size (ebox-lines-join (mapcar (lambda (line) (ebox--clip-line line pixel-width)) (ebox-string-lines formatted))) (and (ebox--rendered-intrinsic-size content :width) pixel-width) (ebox--rendered-intrinsic-size content :height))) formatted))) (defun ebox--format-content (box) "Format BOX content (wrap and justify) returning a string." (or (ebox--format-scroll-window-content box) (ebox--format-content-string box (ebox--box-content box)))) (defun ebox--total-pixel (box) "Get the total pixel width of the BOX." (+ (ebox--content-pixel box) (ebox--side-pixel box))) (defun ebox--total-height (box) "Get the total height of the BOX in lines." (let* ((formatted-content (ebox--format-content box)) (text-height (ebox-string-height formatted-content)) (content-height (ebox--content-height box text-height formatted-content))) (+ content-height (ebox--border-carrier-height box content-height) (ebox--side-height box)))) (defun ebox--node-p (x) "Return non-nil if X is an ebox layout node." (and (listp x) (plist-member x :ebox-type))) (defun ebox--render-box (box) "Render BOX to a string. Internal implementation of `ebox-render' for box nodes." (or (ebox--render-transparent-preformatted-box box) (ebox--render-flat-preformatted-box box) (let* (;; Generate unique region ID for this box. ;; Priority: ;; 1. ebox--render-region-id — explicit single-box render override; ;; never bind it across a recursive layout render ;; 2. :region-id cached on box plist — survives full-rerender so callers ;; that stored the id after the first render can keep using it ;; 3. Fresh id stored back on box plist for all subsequent renders (region-id (or ebox--render-region-id (ebox--ensure-region-id box))) ;; Format content first (ebox--scroll-window-render-result nil) (formatted-content (ebox--format-content box)) (scroll-window-result ebox--scroll-window-render-result) (text-height (ebox-string-height formatted-content)) ;; Calculate dimensions (content-pixel (ebox--content-pixel box)) (content-height (or (plist-get scroll-window-result :content-height) (ebox--content-height box text-height formatted-content))) ;; overflow (overflow (ebox-get box :overflow)) ;; Padding (padding-left (ebox-get box :padding-left-pixel)) (padding-right (ebox-get box :padding-right-pixel)) (padding-top (floor (ebox-get box :padding-top-height))) (padding-bottom (floor (ebox-get box :padding-bottom-height))) ;; Margin (margin-left (ebox-get box :margin-left-pixel)) (margin-right (ebox-get box :margin-right-pixel)) (margin-top (floor (ebox-get box :margin-top-height))) (margin-bottom (floor (ebox-get box :margin-bottom-height))) ;; Border Dimensions and Colors (border-left-pixel (ebox-get box :border-left-pixel)) (border-right-pixel (ebox-get box :border-right-pixel)) (border-top-p (> (ebox-get box :border-top-pixel) 0)) (border-bottom-p (> (ebox-get box :border-bottom-pixel) 0)) ;; Style and Layout (color (and (ebox-style--text-paint-owner-p box) (ebox-get box :color))) (bgcolor (ebox-get box :bgcolor)) (padding-line-filler (ebox--pixel-blank content-pixel 1)) (scroll-offset (or (ebox-get box :scroll-offset) 0)) ;; Overflow handling (formatted-lines (ebox-string-lines formatted-content)) (private-formatted-lines-p (not (and (eq overflow 'scroll) (> text-height content-height)))) (overflow-lines (when (and (eq overflow 'visible) (> text-height content-height)) (seq-drop formatted-lines content-height))) ;; For scroll mode, take lines starting from scroll-offset (visible-start (if (eq overflow 'scroll) (max 0 (min scroll-offset (max 0 (- text-height content-height)))) 0)) (visible-end (min (+ visible-start content-height) text-height)) (visible-lines (if (eq overflow 'scroll) (seq-subseq formatted-lines visible-start visible-end) (seq-take formatted-lines content-height))) (underfilled-simple-scroll-lines (when (and (eq overflow 'scroll) (or (null scroll-window-result) (plist-get scroll-window-result :complete)) (< text-height content-height) (= padding-left 0) (= padding-right 0) (= padding-top 0) (= padding-bottom 0) (= margin-left 0) (= margin-right 0) (= margin-top 0) (= margin-bottom 0) (= border-left-pixel 0) (= border-right-pixel 0) (not border-top-p) (not border-bottom-p) (eq (ebox-get box :vertical-align) 'top)) (ebox--render-underfilled-simple-scroll-lines formatted-lines region-id content-height padding-line-filler color bgcolor))) (scroll-rendered-lines (when (and (eq overflow 'scroll) (>= text-height content-height) (= padding-left 0) (= padding-right 0) (= margin-left 0) (= margin-right 0) (= margin-top 0) (= margin-bottom 0) (= border-left-pixel 0) (= border-right-pixel 0) (eq (ebox-get box :vertical-align) 'top)) (if (plist-get scroll-window-result :cached-rendered-lines-p) ;; The staged producer already applied content/owner ;; properties. Re-running the generic ownership pass here ;; was the dominant grid-scroll stall. formatted-lines (let ((ebox--scroll-rendered-content-lines-private-p private-formatted-lines-p)) (ebox--scroll-rendered-content-lines box formatted-lines region-id 0))))) (simple-scroll-rendered-lines (or underfilled-simple-scroll-lines (and scroll-rendered-lines (= padding-top 0) (= padding-bottom 0) (not border-top-p) (not border-bottom-p) scroll-rendered-lines))) ;; The main content block - 给每行添加 ebox-content 属性用于增量更新 ;; 注意:空字符串无法承载 text-property,需先替换为有宽度的空白 (result (cond (underfilled-simple-scroll-lines (ebox-lines-join underfilled-simple-scroll-lines)) (simple-scroll-rendered-lines (ebox-lines-join (seq-subseq simple-scroll-rendered-lines visible-start visible-end))) (t (let ((ebox--propertize-private-content-line-p private-formatted-lines-p)) (cl-loop for line in visible-lines for idx from 0 collect (ebox--propertize-content-line line region-id idx padding-line-filler))))))) ;; Rendering is the first point where the complete formatted height is ;; known. Keep the host box and published scroll state on the same ;; canonical offset after data shrinkage (including underfill), otherwise ;; a later growth can resurrect an invisible stale position. (when (eq overflow 'scroll) (setq scroll-offset visible-start) ;; Intrinsic measurement renders live runtime subtrees against ;; placeholder content whose visible-start is always 0; writing ;; that back would reset the user's real scroll offset. (unless ebox--intrinsic-layout-measurement (plist-put box :scroll-offset visible-start))) ;; 1. Vertical Alignment of content within the box height. Reused scroll ;; lines already fill a top-aligned, chrome-free viewport. (unless simple-scroll-rendered-lines (let ((lines result)) (setq result (ebox--pad-lines-vertical lines content-height (ebox--vertical-alignment-offset (length lines) content-height (ebox-get box :vertical-align)))))) ;; Fill any empty lines generated by alignment with correct width spaces ;; 保留原有的 ebox-content/ebox-content-idx 属性 (unless simple-scroll-rendered-lines (setq result (mapcar (lambda (line) (if (and (string-empty-p (string-trim line)) (ebox--line-content-metadata-uniform-p line) (not (ebox--line-has-non-content-properties-p line))) (let ((content-id (get-text-property 0 'ebox-content line)) (content-idx (get-text-property 0 'ebox-content-idx line))) (if content-id (propertize padding-line-filler 'ebox-content content-id 'ebox-content-idx content-idx 'ebox-content-owner region-id) (propertize padding-line-filler 'ebox-content region-id 'ebox-content-idx 0 'ebox-content-owner region-id))) line)) result))) ;; Scroll state describes formatted content, independently of chrome. (puthash region-id box ebox--region-box-table) ;; Intrinsic measurement must leave published scroll state alone: its ;; placeholder content would clear the live lazy producer state. (unless (or ebox--intrinsic-layout-measurement ebox--scroll-window-skip-state-rebuild-p) (if (and (eq overflow 'scroll) (> text-height content-height)) (ebox--scroll-set-state region-id (append (list :scroll-offset scroll-offset :content-lines formatted-lines :content-height content-height :rendered-content-lines scroll-rendered-lines :box box) (when scroll-window-result (list :content-lines-complete-p (plist-get scroll-window-result :complete) :scroll-window-producer-spec (plist-get scroll-window-result :scroll-window-producer-spec) :render-content-prefix (plist-get scroll-window-result :render-content-prefix) :materialize-content-lines (plist-get scroll-window-result :materialize-content-lines))))) (ebox--scroll-clear-state region-id))) ;; Ownership, alignment and blank normalization are complete. Compose ;; padding, paint, borders and the host surface as lines, sharing the ;; window wrapper's decoration algorithm instead of repeatedly flattening ;; full-height padding/border strips and splitting the result again. (if simple-scroll-rendered-lines ;; The existing scroll path has already completed chrome and paint. (ebox--apply-surface-properties result (ebox-interaction--node-surface-properties box)) (setq result (ebox-lines-join (ebox--window-render-flat-wrapper-chunk-lines box result 0 region-id padding-line-filler t t t)))) ;; 7. Left/Right Margin - 添加区域标记 (when (or (> margin-left 0) (> margin-right 0)) (let* ((box-height (ebox-string-height result)) (ml-str (when (> margin-left 0) (ebox--propertize-region (ebox--pixel-blank margin-left box-height) 'ebox-ml region-id))) (mr-str (when (> margin-right 0) (ebox--propertize-region (ebox--pixel-blank margin-right box-height) 'ebox-mr region-id)))) (setq result (ebox--lines-concat-horizontal ml-str result mr-str)))) ;; 8. Top/Bottom Margin - 添加区域标记 (when (or (> margin-top 0) (> margin-bottom 0)) (let ((total-pixel-width (ebox--total-pixel box))) (let ((mt-str (when (> margin-top 0) (ebox--propertize-region (ebox--pixel-blank total-pixel-width margin-top) 'ebox-mt region-id))) (mb-str (when (> margin-bottom 0) (ebox--propertize-region (ebox--pixel-blank total-pixel-width margin-bottom) 'ebox-mb region-id)))) (setq result (ebox--lines-stack-vertical mt-str result mb-str))))) ;; 8. Overflow Content (Rendered outside the box if visible) (when overflow-lines (let* ((left-space (+ margin-left border-left-pixel padding-left)) (right-space (+ padding-right border-right-pixel margin-right)) (overflow-left-pad (ebox-pixel-space left-space)) (overflow-right-pad (ebox-pixel-space right-space)) (overflow-text (mapconcat (lambda (line) (let ((line (if color (ebox--propertize-color line color) (copy-sequence line)))) (when (> (length line) 0) (put-text-property 0 (length line) 'ebox-overflow-foreground-source region-id line)) (concat overflow-left-pad line overflow-right-pad))) overflow-lines "\n"))) (setq result (ebox--lines-stack-vertical result overflow-text)))) (when (and (eq overflow 'scroll) (> text-height content-height)) (add-text-properties 0 (length result) (list 'ebox-scroll-window region-id) result)) ;; Visibility suppresses every drop of the box's ink (content, ;; colors, borders, visible-overflow spill) while margins remain ;; the same blanks and all metadata properties survive verbatim. (when (eq (ebox-get box :visibility) 'hidden) (setq result (ebox--apply-hidden-visibility result))) (ebox--record-rendered-intrinsic-size result (and (ebox--rendered-intrinsic-size formatted-content :width) (+ content-pixel (ebox--side-pixel box))) (and (ebox--rendered-intrinsic-size formatted-content :height) (+ content-height (ebox--border-carrier-height box content-height) (ebox--side-height box))))))) (defvar ebox--surface-materialization-active nil "Non-nil while an Ebox TP producer is running raw layout.") (defun ebox--render-text-node (node) "Render canonical TextNode NODE without entering Box geometry." (let* ((region-id (or ebox--render-region-id (ebox--ensure-region-id node))) (value (copy-sequence (plist-get node :ebox-text-value))) (styled (ebox--propertize-text-decoration (ebox--propertize-typography value node) node)) (painted (if (or (ebox-get node :color) (ebox-get node :bgcolor)) (ebox--propertize-colors styled (ebox-get node :color) (ebox-get node :bgcolor)) styled)) (surfaced (ebox--apply-surface-properties painted (ebox-interaction--node-surface-properties node))) (fallback (ebox--pixel-blank 0 1)) (rendered (ebox-lines-join (cl-loop for line in (ebox-string-lines surfaced) for index from 0 collect (ebox--propertize-content-line line region-id index fallback))))) (puthash region-id node ebox--region-box-table) (unless ebox--intrinsic-layout-measurement (ebox--scroll-clear-state region-id)) rendered)) (defun ebox--render-layout (node) "Render layout NODE directly to a multi-line propertized string. NODE can be: - a box plist created by `ebox-create' - a concat node created by `ebox-concat' - a stack node created by `ebox-stack' - a flex node created by `ebox-flex' This function owns spatial layout only; it never creates a live TP surface." (ebox--with-validated-display-cache (let* ((ebox--css-viewport-width (if ebox--css-size-context-active-p ebox--css-viewport-width ebox-viewport-width)) (ebox--css-viewport-height (if ebox--css-size-context-active-p ebox--css-viewport-height (and (numberp ebox-viewport-height) (* ebox-viewport-height (frame-char-height))))) (ebox--css-containing-height (if ebox--css-size-context-active-p ebox--css-containing-height ebox--css-viewport-height)) (ebox--css-size-context-active-p t) (ebox--css-font-fact (or (plist-get node :ebox-font-fact) ebox--css-font-fact)) (ebox--css-viewport-dependent-subtree-cache (or ebox--css-viewport-dependent-subtree-cache (make-hash-table :test 'eq))) (ebox--box-content-render-cache (or ebox--box-content-render-cache (make-hash-table :test 'eq)))) (when (listp node) (ebox--ensure-node-id node) (ebox-buffer-validate-border-capability node)) (let ((type (plist-get node :ebox-type))) (cond ((eq (plist-get node :ebox-kind) 'text) (ebox--render-text-node node)) ((eq type 'box) (ebox--render-box node)) ((eq type 'concat) (ebox--render-concat node)) ((eq type 'stack) (ebox--render-stack node)) ((eq type 'flex) (ebox--render-flex node)) ((eq type 'grid) (ebox--render-grid node)) (t (error "ebox-render: unknown node type %S" type))))))) (defun ebox--render-ephemeral-static (node) "Render static NODE without constructing a TP object tree. The source remains caller-owned; the candidate copy carries all ephemeral region/node identities and render side tables for this one materialization." (let ((candidate (ebox-render-context-candidate-root node t))) (let ((ebox--surface-materialization-active t) (ebox--region-box-table (make-hash-table :test 'equal)) (ebox--scroll-global-state (make-hash-table :test 'equal)) (ebox--scroll-idle-prefetch-timers (make-hash-table :test 'equal)) (ebox--smooth-scroll-state-table (make-hash-table :test 'equal)) (ebox--render-cache-table (make-hash-table :test 'equal)) (ebox--render-cache-signature-cache (make-hash-table :test 'eq)) (ebox--box-content-render-cache (make-hash-table :test 'eq)) (ebox--viewport-dependent-node-ids-cache (make-hash-table :test 'eq)) (ebox--viewport-dependent-subtree-cache (make-hash-table :test 'eq)) (ebox--viewport-height-dependent-subtree-cache (make-hash-table :test 'eq)) (ebox--flex-content-min-width-table (make-hash-table :test 'eq)) (ebox--render-runtime-revision nil) (ebox--render-cache-allow-viewport-dependent nil) (ebox--render-cache-scroll-state-region-ids nil) (ebox--render-cache-scroll-state-restorable-p nil) (ebox--render-cache-scroll-state-retained-cost-cache nil)) (ebox--quantize-pixel-spaces (ebox--render-layout candidate))))) (defun ebox--render-node (node &optional base-source-index) "Render private layout NODE with optional BASE-SOURCE-INDEX. NODE is already below the canonical input boundary. Internal layout paths may also pass a pre-rendered string while an active surface owns runtime facts." (let* ((explicit-source-index-p (and base-source-index t)) (handle (and (listp node) (ebox-tree-node-source-handle node))) (node-id (and (listp node) (plist-get node :node-id))) (source-state (and (not explicit-source-index-p) node-id (cl-find-if (lambda (state) (let ((table (plist-get state :node-table))) (and (ebox-runtime-index-like-p table) (eq node (ebox-runtime-index-get node-id table))))) ebox--render-source-states))) (base-source-index (or base-source-index (plist-get source-state :source-index) (and handle (cl-find-if (lambda (index) (and index (ebox-source-index-handle-member-p index handle))) ebox--render-source-generations)) ebox--render-source-index)) (inherited-source-index-p (and (not explicit-source-index-p) base-source-index)) (ebox--render-source-index base-source-index) (source-index (and (not (stringp node)) (not ebox--surface-materialization-active) (not ebox--render-runtime-revision) (ebox-tree-source-index node t ebox--render-root-parent-kind base-source-index inherited-source-index-p))) (rendered (if (or ebox--surface-materialization-active ebox--render-runtime-revision) (ebox--render-layout node) (if (and (not (stringp node)) (not (ebox-style-cascade-active-p)) (zerop (ebox-tree-author-style-count node)) (or ;; A retained node already carries its ancestor cascade. ;; Probe an isolated copy of those computed facts instead ;; of publishing the detached subtree with a whole-tree ;; source index. Explicit input and active stylesheets ;; still use the complete canonical projection boundary. (and source-state (eq base-source-index (plist-get source-state :source-index)) (ecss-computed-style-p (plist-get node :ebox-computed-style))) (not (ebox-render-context-inline-inheritance-required-p node source-index)))) (let ((ebox--region-id-counter ebox--region-id-counter) (ebox--runtime-node-id-counter ebox--runtime-node-id-counter)) (ebox--render-ephemeral-static node)) (ebox-render-context-materialize-string node base-source-index))))) (unless ebox--paint-origin-capture-p (ebox--strip-paint-origins! rendered)) rendered)) ;;;###autoload (defun ebox-render (input) "Render canonical INPUT to a multi-line string through TP. INPUT atomically carries one typed Text/Box root and its source generation. The materialized surface is ephemeral and creates no live buffer state." (unless (ebox-canonical-input-p input) (signal 'wrong-type-argument (list 'ebox-canonical-input-p input))) (ebox--render-node (ebox-canonical-input--single-root input "ebox-render") (ebox-canonical-input--source-index input))) (defun ebox--width-pad (string extra-pixels) "Append a display-space of EXTRA-PIXELS to the right of every line in STRING. Used by `ebox--render-stack' to equalise rows to the same total width." (if (<= extra-pixels 0) string (let* ((display (ebox--register-render-owned-text-value 'display `(space :width (,extra-pixels)))) (pad (propertize " " 'display display))) (ebox--maplines (lambda (line) (concat line pad)) string)))) (defun ebox--height-pad (string target-height) "Pad STRING to TARGET-HEIGHT lines, appending blank pixel-wide lines. The blank lines match the pixel width of the first line of STRING." (let* ((lines (ebox-string-lines string)) (current-h (length lines)) (extra (- target-height current-h))) (if (<= extra 0) string (let* ((px (ebox--string-pixel-width string)) (blank (ebox-pixel-space px))) (ebox-lines-join (append lines (make-list extra blank))))))) (defun ebox--render-row-children (children &optional config) "Render flat row CHILDREN with optional typed Row CONFIG. Auto-width children are rendered at intrinsic inline size while explicit viewport/definite widths keep their containing-block semantics. Shorter children are padded with blank lines so all reach the same height before horizontal concatenation." (let* ((gap (max 0 (or (ebox--size-pixels (or (plist-get config :item-gap) '(px 0)) nil 'width) 0))) (cross-align (or (plist-get config :cross-align) 'stretch)) (vertical-align (pcase cross-align ('center 'center) ('end 'bottom) (_ 'top))) (rendered-items (mapcar (lambda (child) ;; A row owns the horizontal concatenation. Its auto ;; width children must therefore measure intrinsically; ;; explicit viewport/stretch widths still resolve against ;; the ambient containing block. (let ((ebox--inline-auto-width-intrinsic-p t)) (let ((rendered (ebox--render-with-cache child))) (cons rendered (when (and (string-empty-p rendered) (eq (plist-get child :ebox-kind) 'box)) (ebox--total-pixel child)))))) children)) (max-h (if rendered-items (apply #'max (mapcar (lambda (item) (ebox-string-height (car item))) rendered-items)) 0)) pieces) (cl-loop for item in rendered-items for tail on rendered-items do (let ((string (car item)) (empty-width (cdr item))) (when (or (not (string-empty-p string)) (and empty-width (> empty-width 0))) (push (if empty-width (ebox--pixel-blank empty-width max-h) (ebox--lines-align-vertical string max-h vertical-align)) pieces))) unless (null (cdr tail)) do (when (> gap 0) (push (ebox--pixel-blank gap max-h) pieces))) (let ((rendered (if pieces (or (apply #'ebox--lines-concat-horizontal (nreverse pieces)) "") ""))) (when (string-empty-p rendered) (setq ebox--box-content-empty-width (+ (apply #'+ (mapcar (lambda (item) (or (cdr item) (ebox--string-max-pixel-width (car item)))) rendered-items)) (* gap (max 0 (1- (length rendered-items))))))) rendered))) (defun ebox--render-concat (node) "Render a legacy concat NODE to a string." (ebox--render-row-children (ebox--layout-children node))) (defun ebox--stack-leaves (node) "Return NODE's vertical stack leaves in render order." (let (leaves) (cl-labels ((collect (current) (cond ((and (listp current) (eq (plist-get current :ebox-type) 'stack)) (dolist (child (ebox--layout-children current)) (collect child))) ((ebox--scroll-window-canonical-column-p current) ;; A canonical Column is itself the formatting context. ;; Its direct children are the lazy sequence; nested ;; Columns remain material leaves because they may own ;; independent Box chrome. (dolist (child (ebox-tree-layout-children current)) (push child leaves))) (t (push current leaves))))) (collect node)) (nreverse leaves))) (defun ebox--render-stack-window-leaf-lines (leaf limit target-width) "Render LEAF up to LIMIT lines for a stack scroll window. TARGET-WIDTH is the containing block width used to pad narrower leaves." (if (ebox--prewarm-node-window-line leaf target-width) (list :lines nil :line-count 0 :line-tail nil :complete nil) (let* ((window (ebox--render-node-window-lines leaf limit)) (window-lines (plist-get window :lines)) (rendered (unless window (ebox--render-with-cache leaf))) (wrap-mode (and (eq (plist-get leaf :ebox-kind) 'text) (or ebox--containing-wrap-mode (ebox-get leaf :wrap-mode)))) (rendered (if (and rendered (> target-width 0) (ebox-style-soft-wrap-p wrap-mode)) (ebox--lines-justify (ebox--wrap-text rendered target-width wrap-mode) target-width) rendered)) (width (cond ((plist-member window :pixel-width) (plist-get window :pixel-width)) ((and window (> target-width 0)) target-width) ((and (eq (and (listp leaf) (plist-get leaf :ebox-type)) 'box) (not (eq (plist-get leaf :ebox-kind) 'text))) (ebox--total-pixel leaf)) (t (ebox--string-pixel-width rendered)))) (needs-padding (and (> target-width 0) (< width target-width))) (lines (cond ((and window (not needs-padding)) window-lines) (window (ebox-string-lines (ebox--width-pad (ebox-lines-join window-lines) (- target-width width)))) (needs-padding (ebox-string-lines (ebox--width-pad rendered (- target-width width)))) (t (ebox-string-lines rendered))))) (list :lines lines :line-count (or (and window (not needs-padding) (plist-get window :line-count)) (length lines)) :line-tail (or (and window (not needs-padding) (plist-get window :line-tail)) (last lines)) :complete (if window (plist-get window :complete) t) :prefix-delta (and window (not needs-padding) (plist-get window :prefix-delta)) :completion-prefix-tail-rewrite-count (and window (plist-get window :completion-prefix-tail-rewrite-count)) :completion-prefix-tail-rewrite-known-p (and window (plist-member window :completion-prefix-tail-rewrite-count)))))) (defun ebox--stack-window-cache-compatible-p (cache node target-width) "Return non-nil when CACHE can extend NODE at TARGET-WIDTH." (and cache (eq (plist-get cache :node) node) (equal (plist-get cache :target-width) target-width))) (defun ebox--stack-window-cache-put (node target-width leaves lines line-count line-tail index pending partial-index partial-count complete) "Return a stack scroll-window cache plist." (list :node node :target-width target-width :leaves leaves :lines lines :line-count line-count :line-tail line-tail :index index :pending-lines pending :partial-index partial-index :partial-count partial-count :complete complete)) (defun ebox--line-list-append! (lines tail new-lines) "Append NEW-LINES to LINES through TAIL and return (LINES TAIL). The existing prefix cons cells stay stable so lazy consumers can retain a cursor at their published boundary." (if (null new-lines) (list lines tail) (let ((new-tail (last new-lines))) (if tail (setcdr tail new-lines) (setq lines new-lines)) (list lines new-tail)))) (defun ebox--line-list-rewrite! (lines start replacement) "Replace LINES at zero-based START with equal-length REPLACEMENT in place." (let ((cursor (nthcdr start lines))) (dolist (line replacement) (unless cursor (error "Prefix rewrite exceeds cached line list")) (setcar cursor line) (setq cursor (cdr cursor)))) lines) (defun ebox--line-list-rewrite-range! (lines line-count tail range replacement) "Apply equal-length REPLACEMENT to RANGE in LINES. The common one-line tail completion uses TAIL directly; other ranges retain a conservative indexed fallback." (unless (= (length replacement) (- (cdr range) (car range))) (error "Prefix rewrite payload does not match its range")) (if (and (= (length replacement) 1) (= (car range) (1- line-count)) (= (cdr range) line-count) tail) (setcar tail (car replacement)) (ebox--line-list-rewrite! lines (car range) replacement)) lines) (defun ebox--prefix-delta-valid-p (delta base-count line-count) "Return non-nil when DELTA exactly extends BASE-COUNT to LINE-COUNT. DELTA uses zero-based, half-open line coordinates. Its optional rewrite range must stay inside the reused prefix; lines after BASE-COUNT are append only." (and (plist-get delta :exact-p) (integerp (plist-get delta :base-count)) (= (plist-get delta :base-count) base-count) (<= 0 base-count line-count) (let ((range (plist-get delta :rewrite-range))) (or (null range) (and (consp range) (integerp (car range)) (integerp (cdr range)) (<= 0 (car range)) (< (car range) (cdr range)) (<= (cdr range) base-count)))))) (defun ebox--prefix-delta-project (delta base-count full-line-count line-count) "Project exact DELTA onto an exposed BASE-COUNT line prefix. FULL-LINE-COUNT is the complete line list described by DELTA, while LINE-COUNT is the newly exposed prefix length. Lazy sources may cache extra wrapper chrome or prefetched lines beyond the prefix published to scroll state." (let* ((delta-base (plist-get delta :base-count)) (range (plist-get delta :rewrite-range)) (projected-range (and range (< (car range) base-count) (cons (car range) (min (cdr range) base-count))))) (when (and (integerp delta-base) (ebox--prefix-delta-payload-valid-p delta delta-base full-line-count) (<= 0 base-count delta-base) (<= base-count line-count full-line-count)) (list :exact-p t :base-count base-count :rewrite-range projected-range :rewrite-lines (and projected-range (seq-take (plist-get delta :rewrite-lines) (- (cdr projected-range) (car projected-range)))))))) (defun ebox--prefix-delta-payload-valid-p (delta base-count line-count) "Return non-nil when DELTA has exact rewrite and append payloads." (and (ebox--prefix-delta-valid-p delta base-count line-count) (let ((range (plist-get delta :rewrite-range)) (rewrite-lines (plist-get delta :rewrite-lines)) (append-lines (plist-get delta :append-lines))) (and (= (length rewrite-lines) (if range (- (cdr range) (car range)) 0)) (= (length append-lines) (- line-count base-count)))))) (defun ebox--render-stack-window-lines (node limit &optional cache) "Render stack NODE until LIMIT content lines are available. Return a plist with :lines and :complete. This mirrors the normal stack renderer for the top window of a scroll container, but stops before hidden leaves are rendered. When CACHE is supplied, rendering continues from the last cached leaf instead of walking the stack from the beginning." (let* ((target-w (if ebox--inline-auto-width-intrinsic-p 0 (or (ebox--viewport-pixel-width nil) 0))) (cached (and (ebox--stack-window-cache-compatible-p cache node target-w) cache)) (leaves (or (plist-get cached :leaves) (ebox--stack-leaves node))) (lines (plist-get cached :lines)) (count (or (plist-get cached :line-count) (length lines))) (line-tail (or (plist-get cached :line-tail) (last lines))) (index (or (plist-get cached :index) 0)) (pending (copy-sequence (plist-get cached :pending-lines))) (partial-index (plist-get cached :partial-index)) (partial-count (or (plist-get cached :partial-count) 0)) (complete (plist-get cached :complete)) (base-count count) (delta-exact-p (and cached (>= limit base-count))) invalid-prefix-cache-p rewrite-range rewrite-lines appended-lines appended-tail (ebox--render-cache-allow-viewport-dependent t)) (unless (and cached complete) (setq complete nil) (catch 'limit (while (< count limit) (cond (pending (let* ((need (- limit count)) (chunk (seq-take pending need))) (pcase-let ((`(,new-lines ,new-tail) (ebox--line-list-append! appended-lines appended-tail (copy-sequence chunk)))) (setq appended-lines new-lines appended-tail new-tail)) (setq count (+ count (length chunk))) (setq pending (nthcdr (length chunk) pending)) (when pending (throw 'limit nil)))) ((>= index (length leaves)) (setq complete t) (throw 'limit nil)) (t (let* ((leaf (nth index leaves)) (need (max 1 (- limit count))) (leaf-limit (if (equal partial-index index) (+ partial-count need) need)) (window (ebox--render-stack-window-leaf-lines leaf leaf-limit target-w)) (leaf-lines (plist-get window :lines)) (leaf-count (or (plist-get window :line-count) (length leaf-lines))) (leaf-complete (plist-get window :complete)) (leaf-delta (plist-get window :prefix-delta)) (leaf-prefix-exact-p (and (equal partial-index index) (ebox--prefix-delta-payload-valid-p leaf-delta partial-count leaf-count))) (completion-tail-rewrite-metadata (plist-get window :completion-prefix-tail-rewrite-count)) (completion-tail-rewrite-known-p (plist-get window :completion-prefix-tail-rewrite-known-p)) (completion-tail-rewrite-metadata-valid-p (and completion-tail-rewrite-known-p (or (null completion-tail-rewrite-metadata) (and (integerp completion-tail-rewrite-metadata) (>= completion-tail-rewrite-metadata 0))))) (completion-tail-rewrite-count (if completion-tail-rewrite-metadata-valid-p (or completion-tail-rewrite-metadata 0) 0)) (completion-tail-rewrite-leaf-range (and (> completion-tail-rewrite-count 0) (>= partial-count completion-tail-rewrite-count) (cons (- partial-count completion-tail-rewrite-count) partial-count))) (completion-tail-rewrite-from-delta-p (and leaf-prefix-exact-p (equal (plist-get leaf-delta :rewrite-range) completion-tail-rewrite-leaf-range))) (new-lines (cond (leaf-prefix-exact-p (plist-get leaf-delta :append-lines)) ((equal partial-index index) (nthcdr partial-count leaf-lines)) (t leaf-lines))) (chunk (seq-take new-lines need)) (chunk-count (length chunk))) ;; A producer may explicitly declare completion-only rewrites ;; to the tail already stored for a partial leaf. Flex uses one ;; line for a wrapper's bottom border. The partial leaf is ;; always the cache tail, so reconcile only that declared suffix ;; before appending any newly produced lines. (when (and (equal partial-index index) (> partial-count 0) leaf-complete (or (not completion-tail-rewrite-metadata-valid-p) (> completion-tail-rewrite-count 0))) (if (and completion-tail-rewrite-metadata-valid-p (> partial-count 0) (>= leaf-count partial-count) (>= count completion-tail-rewrite-count) (>= partial-count completion-tail-rewrite-count)) (progn (if rewrite-range (setq delta-exact-p nil) (setq rewrite-range (cons (- count completion-tail-rewrite-count) count))) (setq rewrite-lines (if completion-tail-rewrite-from-delta-p (plist-get leaf-delta :rewrite-lines) (seq-subseq leaf-lines (- partial-count completion-tail-rewrite-count) partial-count)))) ;; A producer that declares an impossible rewrite cannot ;; safely reuse any cached prefix: the cached strings may ;; already be stale even if downstream consumers rebuild. (setq invalid-prefix-cache-p t) (throw 'limit nil))) (pcase-let ((`(,new-lines ,new-tail) (ebox--line-list-append! appended-lines appended-tail (copy-sequence chunk)))) (setq appended-lines new-lines appended-tail new-tail)) (setq count (+ count chunk-count)) (cond ((and leaf-complete (<= (length new-lines) need)) (setq index (1+ index)) (setq partial-index nil) (setq partial-count 0) (when (> (length new-lines) chunk-count) (setq pending (nthcdr chunk-count new-lines)) (throw 'limit nil))) (leaf-complete (setq index (1+ index)) (setq partial-index nil) (setq partial-count 0) (setq pending (nthcdr chunk-count new-lines)) (throw 'limit nil)) (t (setq partial-index index) (setq partial-count (+ partial-count chunk-count)) (throw 'limit nil))))))))) (if invalid-prefix-cache-p (ebox--render-stack-window-lines node limit nil) (let ((inhibit-quit t)) (when rewrite-range (ebox--line-list-rewrite-range! lines base-count line-tail rewrite-range rewrite-lines)) (pcase-let ((`(,new-lines ,new-tail) (ebox--line-list-append! lines line-tail appended-lines))) (setq lines new-lines line-tail new-tail))) (let* ((result-lines (if (<= count limit) lines (seq-take lines limit))) (result-count (min count limit)) (cache (ebox--stack-window-cache-put node target-w leaves lines count line-tail index pending partial-index partial-count complete))) (list :lines result-lines :line-count result-count :line-tail (if (= result-count count) line-tail (last result-lines)) :complete complete :cache cache :prefix-delta (list :exact-p (and delta-exact-p (<= base-count result-count)) :base-count base-count :rewrite-range rewrite-range :rewrite-lines rewrite-lines :append-lines appended-lines)))))) (defun ebox--scroll-window-content-height (box) "Return BOX's definite scroll content height, or nil." (when-let* ((fixed-height (ebox--resolve-size-content-height box (ebox-get box :height) nil))) (ebox--content-height box fixed-height))) (defun ebox--scroll-window-materializer (box viewport-width viewport-height) "Return a function that materializes BOX scroll content." (lambda (&rest _) (let ((ebox-viewport-width viewport-width) (ebox-viewport-height viewport-height) (ebox--scroll-window-render-disabled t) (ebox--scroll-window-render-result nil)) (let* ((full (ebox--format-content box)) (lines (ebox-string-lines full))) (list :content-lines lines :content-height (ebox--content-height box (length lines) full) :box box))))) (defun ebox--scroll-window-producer-spec (prefix-viewport-width materialize-viewport-width viewport-height) "Return portable dimensions for one lazy scroll producer pair." (list :prefix-viewport-width prefix-viewport-width :materialize-viewport-width materialize-viewport-width :viewport-height viewport-height)) (defun ebox--scroll-window-prefix-renderer (box source viewport-width viewport-height &optional initial-source-cache initial-content-lines) "Return a function that renders BOX scroll content up to a line limit." (let* ((source-cache initial-source-cache) (content-lines-cache initial-content-lines) (content-lines-count (length initial-content-lines)) (content-lines-tail (last initial-content-lines)) (source-lines-head (plist-get initial-source-cache :rendered-lines)) (source-lines-boundary-tail (and (> content-lines-count 0) (nthcdr (1- content-lines-count) source-lines-head))) (rendered-content-lines-cache nil) (rendered-content-lines-count 0) (rendered-content-lines-tail nil) (signature-cache (make-hash-table :test 'eq)) ;; The lazy prefix belongs to one validated display signature. Keep ;; its render-local measurement caches across idle slices so repeated ;; card chrome and labels are not sent back through GUI font ;; measurement on every callback. (pixel-width-cache (or ebox--render-string-pixel-width-cache (make-hash-table :test 'equal))) (max-pixel-width-cache (or ebox--render-string-max-pixel-width-cache (make-hash-table :test 'eq))) (recached-source-node-cache (or ebox--render-recached-source-node-cache (make-hash-table :test 'eq))) (prewarm-state (make-hash-table :test 'eq)) (source-revision ebox--render-runtime-revision) (complete-cache nil)) (lambda (state region-id limit) (let ((prefix-dirty (or (plist-get state :lazy-scroll-prefix-dirty) (not (equal source-revision ebox--render-runtime-revision))))) (when prefix-dirty (setq source-cache nil) (setq content-lines-cache nil) (setq content-lines-count 0) (setq content-lines-tail nil) (setq source-lines-head nil) (setq source-lines-boundary-tail nil) (setq rendered-content-lines-cache nil) (setq rendered-content-lines-count 0) (setq rendered-content-lines-tail nil) (setq signature-cache (make-hash-table :test 'eq)) (clrhash pixel-width-cache) (clrhash max-pixel-width-cache) (clrhash recached-source-node-cache) (clrhash prewarm-state) (setq complete-cache nil) (setq source-revision ebox--render-runtime-revision)) (let ((ebox-viewport-width viewport-width) (ebox-viewport-height viewport-height) (ebox--render-cache-signature-cache signature-cache) (ebox--render-string-pixel-width-cache pixel-width-cache) (ebox--render-string-max-pixel-width-cache max-pixel-width-cache) (ebox--render-recached-source-node-cache recached-source-node-cache) (ebox--surface-materialization-active t) (ebox--scroll-window-prewarm-state prewarm-state) (ebox--scroll-window-render-disabled t) (ebox--scroll-window-render-result nil)) (unless rendered-content-lines-cache (unless prefix-dirty (setq rendered-content-lines-cache (plist-get state :rendered-content-lines)) (setq rendered-content-lines-count (length rendered-content-lines-cache)) (setq rendered-content-lines-tail (last rendered-content-lines-cache)))) (let* ((limit (max 1 limit)) (content-height (ebox--scroll-window-content-height box)) (old-count content-lines-count) (cached-hit (and content-lines-cache (or complete-cache (>= content-lines-count limit))))) ;; The producer may have returned a bounded private chunk beyond ;; the published scroll state. Paint that small suffix before a ;; source extension so the exact delta can append instead of ;; rebuilding every rendered line from zero. (when (and (not cached-hit) rendered-content-lines-cache (< rendered-content-lines-count content-lines-count)) (pcase-let ((`(,lines ,count ,tail) (ebox--scroll-rendered-prefix-through-content box content-lines-cache content-lines-count region-id rendered-content-lines-cache rendered-content-lines-count rendered-content-lines-tail))) (setq rendered-content-lines-cache lines rendered-content-lines-count count rendered-content-lines-tail tail))) (unless cached-hit (condition-case err (let* ((window (let ((ebox-viewport-width (or (ebox--wrapper-content-viewport-pixel box) ebox-viewport-width)) (ebox--containing-wrap-mode (ebox-get box :wrap-mode))) (ebox--render-scroll-window-source source limit source-cache))) (next-source-cache (plist-get window :cache)) (next-complete-cache (plist-get window :complete)) (source-lines-exact-p (ebox--scroll-window-source-lines-exact-p box window)) (formatted-lines (ebox--scroll-window-formatted-lines box window)) (full-line-count (if source-lines-exact-p (or (plist-get window :line-count) (length formatted-lines)) (length formatted-lines))) (new-count (if (plist-get window :complete) full-line-count (min full-line-count limit))) (content-prefix-delta (and source-lines-exact-p (eq formatted-lines source-lines-head) (ebox--prefix-delta-project (plist-get window :prefix-delta) old-count full-line-count new-count))) (append-count (- new-count old-count)) (source-append-start (if source-lines-boundary-tail (cdr source-lines-boundary-tail) source-lines-head)) (candidate-appended-lines (and content-prefix-delta (>= append-count 0) (seq-take source-append-start append-count))) (exact-extension-p (and content-prefix-delta (= (length candidate-appended-lines) append-count) rendered-content-lines-cache (= rendered-content-lines-count old-count)))) (if exact-extension-p (let* ((rewrite-range (plist-get content-prefix-delta :rewrite-range)) (rewrite-lines (plist-get content-prefix-delta :rewrite-lines)) (rendered-rewrite-lines (and rewrite-range (ebox--scroll-rendered-content-lines box rewrite-lines region-id (car rewrite-range)))) (rendered-append-lines (ebox--scroll-rendered-content-lines box candidate-appended-lines region-id old-count))) ;; All rendering that can signal completes before ;; published content/rendered lists are mutated. (unless (and (= (length rendered-rewrite-lines) (length rewrite-lines)) (= (length rendered-append-lines) append-count)) (error "Prefix renderer returned inconsistent delta")) (let ((inhibit-quit t)) (when rewrite-range (ebox--line-list-rewrite-range! content-lines-cache old-count content-lines-tail rewrite-range rewrite-lines)) (pcase-let ((`(,new-lines ,new-tail) (ebox--line-list-append! content-lines-cache content-lines-tail candidate-appended-lines))) (setq content-lines-cache new-lines content-lines-tail new-tail)) (when rewrite-range (ebox--line-list-rewrite-range! rendered-content-lines-cache old-count rendered-content-lines-tail rewrite-range rendered-rewrite-lines)) (pcase-let ((`(,new-lines ,new-tail) (ebox--line-list-append! rendered-content-lines-cache rendered-content-lines-tail rendered-append-lines))) (setq rendered-content-lines-cache new-lines rendered-content-lines-tail new-tail)) (setq content-lines-count new-count) (setq rendered-content-lines-count new-count) (when (> append-count 0) (setq source-lines-boundary-tail (nthcdr (1- append-count) source-append-start))))) (let* ((new-content-lines (seq-take formatted-lines new-count)) (new-rendered-lines (ebox--scroll-rendered-content-lines box new-content-lines region-id 0))) ;; Commit the rebuilt pair only after rendering succeeds. (setq content-lines-cache new-content-lines content-lines-count new-count content-lines-tail (last new-content-lines) rendered-content-lines-cache new-rendered-lines rendered-content-lines-count new-count rendered-content-lines-tail (last new-rendered-lines)) (if source-lines-exact-p (setq source-lines-head formatted-lines source-lines-boundary-tail (and (> new-count 0) (nthcdr (1- new-count) formatted-lines))) (setq source-lines-head nil source-lines-boundary-tail nil)))) (setq source-cache next-source-cache complete-cache next-complete-cache)) ((error quit) ;; A producer may have extended private stack/flex caches in ;; place before a later wrapper or face renderer signalled. ;; Published content remains intact; discard every private ;; continuation so the next attempt starts from that prefix. (setq source-cache nil source-lines-head nil source-lines-boundary-tail nil complete-cache nil signature-cache (make-hash-table :test 'eq)) (when (hash-table-p ebox--render-cache-table) (clrhash ebox--render-cache-table)) (signal (car err) (cdr err))))) (let* ((published-count (if complete-cache content-lines-count (min content-lines-count limit))) (content-lines (if (= published-count content-lines-count) content-lines-cache (seq-take content-lines-cache published-count)))) (when (< rendered-content-lines-count published-count) (pcase-let ((`(,lines ,count ,tail) (ebox--scroll-rendered-prefix-through-content box content-lines-cache content-lines-count region-id rendered-content-lines-cache rendered-content-lines-count rendered-content-lines-tail))) (setq rendered-content-lines-cache lines rendered-content-lines-count count rendered-content-lines-tail tail))) (list :content-lines content-lines :content-height content-height :rendered-content-lines (if (= published-count rendered-content-lines-count) rendered-content-lines-cache (seq-take rendered-content-lines-cache published-count)) :complete complete-cache :box box)))))))) (defun ebox--scroll-window-canonical-column-p (node) "Return non-nil for the Column subset supported by lazy scroll layout." (and (eq (plist-get node :ebox-kind) 'box) (when-let* ((config (plist-get node :ebox-layout-config))) (and (eq (ebox-layout-config-kind config) 'column) (let ((props (ebox-layout-config-props config))) (and (zerop (or (ebox--size-pixels (or (plist-get props :item-gap) '(lh 0)) node 'height) 0)) (eq (or (plist-get props :cross-align) 'stretch) 'stretch))))))) (defun ebox--scroll-window-transparent-column-p (node) "Return non-nil when NODE's Column Box adds no visible wrapper chrome." (and (ebox--scroll-window-canonical-column-p node) (memq (ebox-get node :height) '(nil auto)) (zerop (ebox--resolve-size-content-height node (ebox-get node :min-height) 0)) (memq (ebox-get node :max-height) '(nil none)) (cl-every (lambda (property) (zerop (or (ebox-get node property) 0))) '(:padding-left-pixel :padding-right-pixel :padding-top-height :padding-bottom-height :margin-left-pixel :margin-right-pixel :margin-top-height :margin-bottom-height :border-left-pixel :border-right-pixel)) (= (ebox-get node :border-top-pixel) 0) (= (ebox-get node :border-bottom-pixel) 0) (null (ebox-get node :color)) (null (ebox-get node :bgcolor)) (or (null (plist-get node :ebox-font-fact)) (ebox-font-fact-neutral-p (plist-get node :ebox-font-fact))) (null (ebox-get node :surface-properties)) (eq (ebox-get node :visibility) 'visible) (eq (ebox-get node :text-align) 'left) (eq (ebox-get node :vertical-align) 'top))) (defun ebox--scroll-window-stack-source (node) "Return lazy scroll stack source metadata for NODE, or nil." (cond ((eq (plist-get node :ebox-type) 'stack) (list :stack node)) ((ebox--scroll-window-canonical-column-p node) (list :stack node)) ((and (eq (plist-get node :ebox-kind) 'box) (when-let* ((children (ebox-tree-layout-children node))) (and (= (length children) 1) (ebox--scroll-window-transparent-column-p (car children))))) (list :stack (car (ebox-tree-layout-children node)))) ((and (eq (plist-get node :ebox-type) 'box) (eq (plist-get (plist-get node :ebox-content-node) :ebox-type) 'stack)) (list :stack (plist-get node :ebox-content-node) :wrapper node)))) (defun ebox--scroll-window-rebind-state-producers (state box) "Rebind lazy scroll producers in STATE to candidate runtime BOX. The stored producer descriptor carries only viewport dimensions; source nodes are always resolved from BOX so no closure can retain a replaced runtime." (when (or (plist-get state :render-content-prefix) (plist-get state :materialize-content-lines)) (let* ((spec (plist-get state :scroll-window-producer-spec)) (prefix-viewport-width (plist-get spec :prefix-viewport-width)) (materialize-viewport-width (plist-get spec :materialize-viewport-width)) (viewport-height (plist-get spec :viewport-height)) (source (or (ebox--scroll-window-stack-source (ebox--box-content-node box)) (ebox--scroll-window-stack-source box)))) (unless (and (numberp prefix-viewport-width) (> prefix-viewport-width 0) (numberp materialize-viewport-width) (> materialize-viewport-width 0) (numberp viewport-height) (> viewport-height 0)) (error "Ebox lazy scroll state lacks a portable producer descriptor")) (if source (progn (when (plist-get state :render-content-prefix) (setq state (plist-put state :render-content-prefix (ebox--scroll-window-prefix-renderer box source prefix-viewport-width viewport-height)))) (when (plist-get state :materialize-content-lines) (setq state (plist-put state :materialize-content-lines (ebox--scroll-window-materializer box materialize-viewport-width viewport-height))))) ;; A changed candidate may no longer have a lazy source. Its dirty ;; render will install the new state; never carry old-tree closures ;; across that preparation boundary. (setq state (plist-put state :render-content-prefix nil)) (setq state (plist-put state :materialize-content-lines nil))))) state) (defun ebox--scroll-window-source-lines-exact-p (box window) "Return non-nil when WINDOW lines can be used as BOX formatted content." (and (plist-get window :content-width-exact-p) (or (ebox-style-no-soft-wrap-p (ebox-get box :wrap-mode)) ;; A Column already wrapped its Text leaves; its Box/Flex/Grid ;; children retain their own geometry, including explicit overflow. (ebox--scroll-window-canonical-column-p box)) (eq (ebox-get box :text-align) 'left) (let ((content-viewport (ebox--wrapper-content-viewport-pixel box))) (and (numberp content-viewport) (= (ebox--content-pixel box) content-viewport))))) (defun ebox--scroll-window-formatted-lines (box window) "Return formatted scroll content lines for BOX from WINDOW." (if (ebox--scroll-window-source-lines-exact-p box window) (plist-get window :lines) (ebox-string-lines (ebox--format-content-string box (or (plist-get window :rendered) (ebox-lines-join (plist-get window :rendered-lines))))))) (defun ebox--scroll-window-wrapper-supported-p (box) "Return non-nil when BOX can wrap an incomplete lazy scroll prefix." (or (null box) (and (memq (ebox-get box :height) '(nil auto)) (zerop (ebox--resolve-size-content-height box (ebox-get box :min-height) 0)) (memq (ebox-get box :max-height) '(nil none)) (ebox-style-no-soft-wrap-p (ebox-get box :wrap-mode)) (eq (ebox-get box :text-align) 'left) (eq (ebox-get box :vertical-align) 'top)))) (defun ebox--scroll-window-render-wrapper-full (wrapper content content-width-exact-p) "Render WRAPPER around CONTENT through the normal box renderer." (let* ((copy (copy-sequence wrapper)) (region-id (ebox--ensure-region-id wrapper))) (plist-put copy :region-id region-id) (plist-put copy :content content) (plist-put copy :ebox-content-node nil) (plist-put copy :ebox-content-width-exact-p content-width-exact-p) (prog1 (ebox--render-node copy) (puthash region-id wrapper ebox--region-box-table)))) (defun ebox--window-render-flat-wrapper-chunk-lines (box content-lines start-index region-id padding-line-filler &optional include-top include-bottom content-prepared-p) "Render BOX's border box around CONTENT-LINES in one pass. START-INDEX and REGION-ID identify the wrapped content lines. INCLUDE-TOP adds top padding and border chrome. INCLUDE-BOTTOM adds bottom padding and border chrome. This helper does not consume BOX's margins; the caller owns their outer composition. CONTENT-PREPARED-P means CONTENT-LINES already carry final ownership, indices and blank normalization, letting a complete Box and a window chunk share the same chrome compositor." (let* ((padding-left (ebox-get box :padding-left-pixel)) (padding-right (ebox-get box :padding-right-pixel)) (padding-top (if include-top (floor (ebox-get box :padding-top-height)) 0)) (padding-bottom (if include-bottom (floor (ebox-get box :padding-bottom-height)) 0)) (border-left-pixel (ebox-get box :border-left-pixel)) (border-right-pixel (ebox-get box :border-right-pixel)) (border-top-p (and include-top (> (ebox-get box :border-top-pixel) 0))) (border-top-color (ebox-get box :border-top-color)) (border-bottom-p (and include-bottom (> (ebox-get box :border-bottom-pixel) 0))) (border-bottom-color (ebox-get box :border-bottom-color)) (color (and (ebox-style--text-paint-owner-p box) (ebox-get box :color))) (bgcolor (ebox-get box :bgcolor)) (padding-left-line (when (> padding-left 0) (let ((line (ebox--propertize-region (ebox--pixel-blank padding-left 1) 'ebox-pl region-id))) (if (or color bgcolor) (ebox--propertize-colors line color bgcolor) line)))) (padding-right-line (when (> padding-right 0) (let ((line (ebox--propertize-region (ebox--pixel-blank padding-right 1) 'ebox-pr region-id))) (if (or color bgcolor) (ebox--propertize-colors line color bgcolor) line)))) (border-left-line (when (> border-left-pixel 0) (ebox--propertize-region (ebox--pixel-border border-left-pixel 1 (ebox-get box :border-left-color)) 'ebox-bl region-id))) (border-right-line (when (> border-right-pixel 0) (ebox--propertize-region (ebox--pixel-border border-right-pixel 1 (ebox-get box :border-right-color)) 'ebox-br region-id)))) (let* ((top-core-line (when (> padding-top 0) (let ((line (ebox--propertize-region padding-line-filler 'ebox-pt region-id))) (if (or color bgcolor) (ebox--propertize-colors line color bgcolor) line)))) (top-lines (when top-core-line (cl-loop repeat padding-top collect (concat border-left-line padding-left-line top-core-line padding-right-line border-right-line)))) (bottom-core-line (when (> padding-bottom 0) (let ((line (ebox--propertize-region padding-line-filler 'ebox-pb region-id))) (if (or color bgcolor) (ebox--propertize-colors line color bgcolor) line)))) (bottom-lines (when bottom-core-line (cl-loop repeat padding-bottom collect (concat border-left-line padding-left-line bottom-core-line padding-right-line border-right-line)))) (wrapped-lines (cl-loop for source-line in content-lines for idx from start-index collect (let* ((blank-replacement-p (and (not content-prepared-p) (string-blank-p source-line) (ebox--line-content-metadata-uniform-p source-line) (not (ebox--line-has-non-content-properties-p source-line)))) (line (cond (content-prepared-p (if (or color bgcolor) (ebox--propertize-colors source-line color bgcolor) source-line)) (blank-replacement-p (let ((content-id (get-text-property 0 'ebox-content source-line)) (content-idx (get-text-property 0 'ebox-content-idx source-line)) (content-owner (get-text-property 0 'ebox-content-owner source-line)) (content-owners (get-text-property 0 'ebox-content-owners source-line))) (if (or content-owner content-owners) (ebox--propertize-content-line (propertize padding-line-filler 'ebox-content content-id 'ebox-content-idx content-idx 'ebox-content-owner (or content-owner content-id) 'ebox-content-owners (or content-owners (list content-owner))) region-id idx padding-line-filler) (if content-id (propertize padding-line-filler 'ebox-content content-id 'ebox-content-idx content-idx 'ebox-content-owner region-id) (propertize padding-line-filler 'ebox-content region-id 'ebox-content-idx idx 'ebox-content-owner region-id))))) (t (let* ((colored-p (or color bgcolor)) (owned-source (if colored-p (ebox--propertize-colors source-line color bgcolor) source-line))) (let ((ebox--propertize-private-content-line-p colored-p)) (ebox--propertize-content-line owned-source region-id idx padding-line-filler))))))) (when (and blank-replacement-p (or color bgcolor)) (setq line (ebox--propertize-colors line color bgcolor))) (concat border-left-line padding-left-line line padding-right-line border-right-line)))) (lines (nconc top-lines wrapped-lines bottom-lines))) (when (and (null lines) (or border-top-p border-bottom-p)) (setq lines (list (concat border-left-line padding-left-line (if (or color bgcolor) (ebox--propertize-colors padding-line-filler color bgcolor) padding-line-filler) padding-right-line border-right-line)))) (when (and border-top-p lines) (setcar lines (propertize (ebox--propertize-overline (car lines) border-top-color) 'ebox-bt region-id))) (when (and border-bottom-p lines) (let ((tail (last lines))) (setcar tail (propertize (ebox--propertize-underline (car tail) border-bottom-color) 'ebox-bb region-id)))) (when-let* ((properties (ebox-interaction--node-surface-properties box))) (setq lines (mapcar (lambda (line) (ebox--apply-surface-properties line properties)) lines))) lines))) (defun ebox--scroll-window-render-wrapper-chunk-lines (box content-lines start-index include-top) "Render BOX wrapper chrome around newly appended CONTENT-LINES. START-INDEX is the first content-line index inside BOX. INCLUDE-TOP means the chunk begins at the wrapper top and should include top padding, margin, and border chrome. Bottom chrome is intentionally omitted because this helper is only used for incomplete lazy prefixes." (if (not box) content-lines (let* ((region-id (ebox--ensure-region-id box)) (content-pixel (ebox--content-pixel box)) (padding-line-filler (ebox--pixel-blank content-pixel 1)) (padding-left (ebox-get box :padding-left-pixel)) (padding-right (ebox-get box :padding-right-pixel)) (padding-top (if include-top (floor (ebox-get box :padding-top-height)) 0)) (margin-left (ebox-get box :margin-left-pixel)) (margin-right (ebox-get box :margin-right-pixel)) (margin-top (if include-top (floor (ebox-get box :margin-top-height)) 0)) (border-left-pixel (ebox-get box :border-left-pixel)) (border-left-color (ebox-get box :border-left-color)) (border-right-pixel (ebox-get box :border-right-pixel)) (border-right-color (ebox-get box :border-right-color)) (border-top-p (and include-top (> (ebox-get box :border-top-pixel) 0))) (border-top-color (ebox-get box :border-top-color)) (color (and (ebox-style--text-paint-owner-p box) (ebox-get box :color))) (bgcolor (ebox-get box :bgcolor)) (inner-height (+ (length content-lines) padding-top)) result) (puthash region-id box ebox--region-box-table) (if (and (= margin-left 0) (= margin-right 0) (= margin-top 0)) (ebox--window-render-flat-wrapper-chunk-lines box content-lines start-index region-id padding-line-filler include-top) (progn (setq result (ebox-lines-join (cl-loop for line in content-lines for idx from start-index collect (ebox--propertize-content-line line region-id idx padding-line-filler)))) (setq result (ebox--maplines (lambda (line) (if (and (string-empty-p (string-trim line)) (ebox--line-content-metadata-uniform-p line) (not (ebox--line-has-non-content-properties-p line))) (let ((content-id (get-text-property 0 'ebox-content line)) (content-idx (get-text-property 0 'ebox-content-idx line))) (if content-id (propertize padding-line-filler 'ebox-content content-id 'ebox-content-idx content-idx 'ebox-content-owner region-id) (propertize padding-line-filler 'ebox-content region-id 'ebox-content-idx 0 'ebox-content-owner region-id))) line)) result)) (when (> padding-top 0) (setq result (ebox--lines-stack-vertical (ebox--propertize-region (ebox--pixel-blank content-pixel padding-top) 'ebox-pt region-id) result))) (let ((pl-str (when (> padding-left 0) (ebox--propertize-region (ebox--pixel-blank padding-left inner-height) 'ebox-pl region-id))) (pr-str (when (> padding-right 0) (ebox--propertize-region (ebox--pixel-blank padding-right inner-height) 'ebox-pr region-id)))) (when (or pl-str pr-str) (setq result (ebox--lines-concat-horizontal pl-str result pr-str)))) (when (or color bgcolor) (setq result (ebox--maplines (lambda (line) (ebox--propertize-colors line color bgcolor)) result))) (when (or (> border-left-pixel 0) (> border-right-pixel 0)) (let ((bl-str (when (> border-left-pixel 0) (ebox--propertize-region (ebox--pixel-border border-left-pixel inner-height border-left-color) 'ebox-bl region-id))) (br-str (when (> border-right-pixel 0) (ebox--propertize-region (ebox--pixel-border border-right-pixel inner-height border-right-color) 'ebox-br region-id)))) (setq result (ebox--lines-concat-horizontal bl-str result br-str)))) (when border-top-p (let ((lines (ebox-string-lines result))) (setf (car lines) (propertize (ebox--propertize-overline (car lines) border-top-color) 'ebox-bt region-id)) (setq result (ebox-lines-join lines)))) (ebox--apply-surface-properties result (ebox-interaction--node-surface-properties box)) (when (or (> margin-left 0) (> margin-right 0)) (let* ((box-height (ebox-string-height result)) (ml-str (when (> margin-left 0) (ebox--propertize-region (ebox--pixel-blank margin-left box-height) 'ebox-ml region-id))) (mr-str (when (> margin-right 0) (ebox--propertize-region (ebox--pixel-blank margin-right box-height) 'ebox-mr region-id)))) (setq result (ebox--lines-concat-horizontal ml-str result mr-str)))) (when (> margin-top 0) (setq result (ebox--lines-stack-vertical (ebox--propertize-region (ebox--pixel-blank (ebox--total-pixel box) margin-top) 'ebox-mt region-id) result))) (ebox-string-lines result)))))) (defun ebox--render-scroll-window-source (source limit &optional cache) "Render lazy scroll SOURCE until LIMIT parent content lines are available." (let* ((stack (plist-get source :stack)) (wrapper (plist-get source :wrapper)) (cached-source-lines (plist-get cache :source-lines)) (cached-count (or (plist-get cache :source-count) (length cached-source-lines))) (cached-rendered-lines (plist-get cache :rendered-lines)) (cached-rendered-count (or (plist-get cache :rendered-count) (length cached-rendered-lines))) (cached-rendered-tail (or (plist-get cache :rendered-tail) (last cached-rendered-lines))) (content-viewport (and wrapper (ebox--wrapper-content-viewport-pixel wrapper))) (window (let ((ebox-viewport-width (or content-viewport ebox-viewport-width))) (ebox--render-stack-window-lines stack limit (plist-get cache :stack)))) (source-lines (plist-get window :lines)) (source-count (or (plist-get window :line-count) (length source-lines))) (complete (plist-get window :complete)) (stack-delta (plist-get window :prefix-delta)) (stack-prefix-exact-p (and (eq source-lines cached-source-lines) (ebox--prefix-delta-payload-valid-p stack-delta cached-count source-count))) (rewrite-range (and stack-prefix-exact-p (plist-get stack-delta :rewrite-range))) (wrapper-prefix-count (- cached-rendered-count cached-count)) (wrapper-reuse-p (and wrapper (not complete) stack-prefix-exact-p (ebox--scroll-window-wrapper-supported-p wrapper) cached-source-lines cached-rendered-lines (>= wrapper-prefix-count 0))) (mapped-rewrite-range (and wrapper-reuse-p rewrite-range (if (= (car rewrite-range) 0) (cons 0 (+ wrapper-prefix-count (cdr rewrite-range))) (cons (+ wrapper-prefix-count (car rewrite-range)) (+ wrapper-prefix-count (cdr rewrite-range)))))) (rewritten-lines (and wrapper-reuse-p rewrite-range (ebox--scroll-window-render-wrapper-chunk-lines wrapper (plist-get stack-delta :rewrite-lines) (car rewrite-range) (= (car rewrite-range) 0)))) (appended-source-lines (and wrapper-reuse-p (plist-get stack-delta :append-lines))) (appended-lines (and appended-source-lines (ebox--scroll-window-render-wrapper-chunk-lines wrapper appended-source-lines cached-count nil))) (wrapper-prefix-exact-p (and wrapper-reuse-p (or (null mapped-rewrite-range) (= (length rewritten-lines) (- (cdr mapped-rewrite-range) (car mapped-rewrite-range)))) (= (length appended-lines) (length appended-source-lines)))) rendered-lines rendered-count rendered-tail prefix-delta) (cond ((not wrapper) (setq rendered-lines source-lines rendered-count source-count rendered-tail (plist-get window :line-tail) prefix-delta (if stack-prefix-exact-p stack-delta (list :exact-p nil :base-count cached-count :rewrite-range nil :rewrite-lines nil :append-lines nil)))) (wrapper-prefix-exact-p (let ((inhibit-quit t)) (when mapped-rewrite-range (ebox--line-list-rewrite-range! cached-rendered-lines cached-rendered-count cached-rendered-tail mapped-rewrite-range rewritten-lines)) (pcase-let ((`(,new-lines ,new-tail) (ebox--line-list-append! cached-rendered-lines cached-rendered-tail appended-lines))) (setq rendered-lines new-lines rendered-tail new-tail)) (setq rendered-count (+ cached-rendered-count (length appended-lines)))) (setq prefix-delta (list :exact-p t :base-count cached-rendered-count :rewrite-range mapped-rewrite-range :rewrite-lines rewritten-lines :append-lines appended-lines))) (t (setq rendered-lines (if (and (not complete) (ebox--scroll-window-wrapper-supported-p wrapper)) (ebox--scroll-window-render-wrapper-chunk-lines wrapper source-lines 0 t) (ebox-string-lines (ebox--scroll-window-render-wrapper-full wrapper (ebox-lines-join source-lines) t)))) (setq rendered-count (length rendered-lines) rendered-tail (last rendered-lines) prefix-delta (list :exact-p nil :base-count cached-rendered-count :rewrite-range nil :rewrite-lines nil :append-lines nil)))) (list :complete complete :cache (list :stack (plist-get window :cache) :source-lines source-lines :source-count source-count :rendered-lines rendered-lines :rendered-count rendered-count :rendered-tail rendered-tail) :lines rendered-lines :line-count rendered-count :line-tail rendered-tail :content-width-exact-p t :rendered-lines rendered-lines :prefix-delta prefix-delta))) (defun ebox--format-scroll-window-content (box) "Return a lazily rendered scroll content window for BOX, or nil." (catch 'ebox-scroll-window-cached (let* ((cached ebox--scroll-window-cached-state) (cached-box (and cached (plist-get cached :box))) (cached-id (and cached-box (plist-get cached-box :node-id))) (box-id (and (listp box) (plist-get box :node-id))) (cached-rendered-lines (and cached (plist-get cached :rendered-content-lines))) (use-cached-rendered-lines-p (and ebox--scroll-window-skip-state-rebuild-p cached-rendered-lines)) (cached-lines (and cached (or (and use-cached-rendered-lines-p cached-rendered-lines) (plist-get cached :content-lines))))) (when (and cached cached-box cached-lines box-id (equal box-id cached-id) (eq (ebox-get box :overflow) 'scroll)) (setq ebox--scroll-window-render-result (list :complete (plist-get cached :content-lines-complete-p) :content-height (plist-get cached :content-height) :scroll-window-producer-spec (plist-get cached :scroll-window-producer-spec) :render-content-prefix (plist-get cached :render-content-prefix) :materialize-content-lines (plist-get cached :materialize-content-lines) :cached-rendered-lines-p (and use-cached-rendered-lines-p t))) (throw 'ebox-scroll-window-cached (ebox-lines-join cached-lines)))) (let ((content-height (ebox--scroll-window-content-height box)) (scroll-offset (max 0 (or (ebox-get box :scroll-offset) 0))) (content-viewport (ebox--wrapper-content-viewport-pixel box)) (source (or (ebox--scroll-window-stack-source (ebox--box-content-node box)) (ebox--scroll-window-stack-source box)))) (when (and (not ebox--scroll-window-render-disabled) (eq (ebox-get box :overflow) 'scroll) (ebox--viewport-height-size-value-p (ebox-get box :height)) content-height (> content-height 0) (ebox--viewport-pixel-width nil) source) (if ebox--intrinsic-layout-measurement (progn (setq ebox--scroll-window-render-result (list :content-height content-height :measurement-placeholder t)) (or (ebox--pixel-blank (ebox--content-pixel box) content-height) "")) ;; Source children use the inner viewport. Formatting BOX and its ;; retained producers still requires the containing viewport, or its ;; padding and borders would be subtracted from that inner width again. (let ((viewport-width ebox-viewport-width) (viewport-height ebox-viewport-height)) (let* ((target-lines (+ scroll-offset content-height 1 (ebox--scroll-window-initial-lookahead-lines))) (window (let ((ebox-viewport-width (or content-viewport viewport-width)) (ebox--containing-wrap-mode (ebox-get box :wrap-mode))) (ebox--render-scroll-window-source source target-lines))) (formatted-lines (ebox--scroll-window-formatted-lines box window))) (setq ebox--scroll-window-render-result (list :complete (plist-get window :complete) :content-height content-height :scroll-window-producer-spec (ebox--scroll-window-producer-spec viewport-width viewport-width viewport-height) :render-content-prefix (ebox--scroll-window-prefix-renderer box source viewport-width viewport-height (plist-get window :cache) formatted-lines) :materialize-content-lines (ebox--scroll-window-materializer box viewport-width viewport-height))) (ebox-lines-join (if (plist-get window :complete) formatted-lines (seq-take formatted-lines target-lines)))))))))) (defun ebox--render-column-children (children &optional config content-viewport) "Render flat column CHILDREN with optional typed Column CONFIG. CONTENT-VIEWPORT is the owning Box's resolved content width, when definite. Stack leaves are rendered independently. When a containing-block viewport is bound, narrower rows are padded to that available width, while wider definite children keep their own width and overflow only on their own rows. During intrinsic measurement, descendants still receive the containing block but the stack itself uses its natural max child width unless CONTENT-VIEWPORT retains an independently sized owning Box. A stack used as an intrinsic inline child follows the same natural-width rule and does not consume the ambient horizontal viewport." (let* ((gap (max 0 (floor (/ (or (ebox--size-pixels (or (plist-get config :item-gap) '(lh 0)) nil 'height) 0) (float (ebox--size-line-height)))))) (cross-align (or (plist-get config :cross-align) 'stretch)) (horizontal-align (pcase cross-align ('center 'center) ('end 'right) (_ 'left))) (content-width (or content-viewport (and (not ebox--intrinsic-layout-measurement) (not ebox--inline-auto-width-intrinsic-p) (ebox--viewport-pixel-width nil)))) (rendered-items (mapcar (lambda (leaf) (let* ((rendered (ebox--render-with-cache leaf)) (wrap-mode (and (eq (plist-get leaf :ebox-kind) 'text) (or ebox--containing-wrap-mode (ebox-get leaf :wrap-mode)))) (rendered (if (and content-width (> content-width 0) (ebox-style-soft-wrap-p wrap-mode)) (ebox--lines-justify (ebox--wrap-text rendered content-width wrap-mode) content-width horizontal-align) rendered))) (cons rendered (if (and (string-empty-p rendered) (eq (plist-get leaf :ebox-kind) 'box)) (let ((ebox--inline-auto-width-intrinsic-p t)) (ebox--total-pixel leaf)) (ebox--string-max-pixel-width rendered))))) children)) (max-w (if rendered-items (apply #'max (mapcar #'cdr rendered-items)) 0)) (target-w (if (and (null content-viewport) (or ebox--intrinsic-layout-measurement ebox--inline-auto-width-intrinsic-p)) max-w (or content-width max-w)))) (let (pieces) (cl-loop for item in rendered-items for tail on rendered-items do (push (let ((rendered (car item)) (width (cdr item))) (if (< width target-w) (ebox--lines-justify rendered target-w horizontal-align) rendered)) pieces) unless (null (cdr tail)) do (when (> gap 0) (push (ebox--pixel-blank target-w gap) pieces))) (let ((rendered (if pieces (or (apply #'ebox--lines-stack-vertical (nreverse pieces)) "") ""))) (when (string-empty-p rendered) (setq ebox--box-content-empty-width (max target-w max-w))) ;; When no child overflows the containing block, stack padding proves ;; every output line already has the exact target width. A wrapping box ;; can then preserve this preformatted content without measuring every ;; line again. Wider children deliberately keep their own overflow rows ;; and therefore must not publish uniform-width metadata. (if (<= max-w target-w) (ebox--record-rendered-uniform-width rendered target-w) rendered))))) (defun ebox--render-stack (node) "Render a legacy stack NODE to a string." (ebox--render-column-children (ebox--stack-leaves node))) (provide 'ebox-layout) ;;; ebox-layout.el ends here