Some checks are pending
CI / test (29.1) (push) Waiting to run
CI / test (30.2) (push) Waiting to run
CI / native-build (macos-latest) (push) Waiting to run
CI / native-build (ubuntu-latest) (push) Waiting to run
CI / native-build (windows-latest) (push) Waiting to run
CI / native-msrv (macos-latest) (push) Waiting to run
CI / native-msrv (ubuntu-latest) (push) Waiting to run
CI / native-msrv (windows-latest) (push) Waiting to run
217 lines
10 KiB
EmacsLisp
217 lines
10 KiB
EmacsLisp
;;; ebox-composite.el --- Pixel interval row composition -*- lexical-binding: t; -*-
|
|
|
|
;;; Commentary:
|
|
;; Compose opaque, already rendered rows without changing their source strings.
|
|
;; Resolve coverage first so every visible interval is cut from its original
|
|
;; source, independent of the number or ordering of hidden lower placements.
|
|
|
|
;;; Code:
|
|
|
|
(require 'cl-lib)
|
|
(require 'ebox-measure)
|
|
|
|
(declare-function ebox--grapheme-cluster-end "ebox-layout" (string start))
|
|
|
|
(defun ebox-composite--line-p (line)
|
|
"Return non-nil if LINE is a single-line string or nil."
|
|
(or (null line)
|
|
(and (stringp line) (not (string-match-p "\n" line)))))
|
|
|
|
(defun ebox-composite--replacement-p (display)
|
|
"Return non-nil when DISPLAY replaces an entire EQ property run."
|
|
(or (stringp display) (memq (car-safe display) '(space image))))
|
|
|
|
(defun ebox-composite--unit-end (line position)
|
|
"Return the complete display unit end in LINE at POSITION."
|
|
(let ((end (ebox--grapheme-cluster-end line position)))
|
|
(if (ebox-composite--replacement-p
|
|
(get-text-property position 'display line))
|
|
(max end (next-single-property-change
|
|
position 'display line (length line)))
|
|
end)))
|
|
|
|
(defun ebox-composite--space (line start end width)
|
|
"Copy LINE's fixed space at START through END using WIDTH pixels.
|
|
Retain the source properties and nested display values by identity."
|
|
(let* ((result (substring line start end))
|
|
(display (get-text-property start 'display line))
|
|
(properties (copy-sequence (cdr display))))
|
|
(put-text-property
|
|
0 (length result) 'display
|
|
(cons 'space (plist-put properties :width (list width))) result)
|
|
result))
|
|
|
|
(defun ebox-composite--join (parts)
|
|
"Join PARTS while retaining their separately measured pixel extents.
|
|
A neutral zero-pixel display boundary prevents adjacent EQ replacement runs
|
|
from collapsing or independently shaped source fragments from reshaping."
|
|
(let ((result "") (width 0))
|
|
(dolist (part parts result)
|
|
(unless (equal part "")
|
|
(let* ((next-width (+ width (ebox--string-pixel-width part)))
|
|
(joined (concat result part)))
|
|
(setq result
|
|
(if (= (ebox--string-pixel-width joined) next-width)
|
|
joined
|
|
(concat result
|
|
(propertize " " 'display '(space :width (0)))
|
|
part))
|
|
width next-width))))))
|
|
|
|
(defun ebox-composite--fit (line start end left right)
|
|
"Fit LINE from START through END in its original [LEFT, RIGHT) pixels.
|
|
Removing a left shaping context can change a retained unit's width. Resolve
|
|
that unit within its original pixel interval, then retry the remaining whole
|
|
run. A newly wider glyph becomes neutral fill rather than shifting the next
|
|
source glyph or borrowing its pixels."
|
|
(let ((position start) (pixel left) parts)
|
|
(while (< position end)
|
|
(let* ((remaining (substring line position end))
|
|
(width (ebox--string-pixel-width remaining)))
|
|
(if (= width (- right pixel))
|
|
(progn (push remaining parts) (setq position end))
|
|
(let* ((next (ebox-composite--unit-end line position))
|
|
(next-pixel (ebox--substring-pixel-width line 0 next))
|
|
(unit (substring line position next))
|
|
(unit-width (ebox--string-pixel-width unit))
|
|
(available (- next-pixel pixel)))
|
|
(cond
|
|
((<= unit-width available)
|
|
(push unit parts)
|
|
(push (ebox-pixel-space (- available unit-width)) parts))
|
|
((ebox--display-space-width
|
|
(get-text-property position 'display line))
|
|
(push (ebox-composite--space line position next available)
|
|
parts))
|
|
(t (push (ebox-pixel-space available) parts)))
|
|
(setq position next pixel next-pixel)))))
|
|
(ebox-composite--join (nreverse parts))))
|
|
|
|
(defun ebox-composite-slice (line start end)
|
|
"Extract LINE's pixel interval [START, END), exactly END minus START wide.
|
|
START and END must be finite nonnegative numbers with START <= END. LINE is
|
|
a single-line string or nil. Pixels beyond LINE are neutral blank space.
|
|
Keep complete supported graphemes and replacement display runs, preserving
|
|
their text property values by identity. Fixed pixel spaces can be cut at
|
|
either edge; other partial glyphs become neutral blanks at their original
|
|
positions. Measure original prefixes so adjacent shaping is not summed as
|
|
isolated characters. Neither LINE nor its property values are modified."
|
|
(unless (ebox-composite--line-p line)
|
|
(error "Ebox composite needs a single-line string: %S" line))
|
|
(unless (and (ebox-size--finite-number-p start) (>= start 0)
|
|
(ebox-size--finite-number-p end) (>= end start))
|
|
(error "Invalid Ebox composite pixel interval: %S %S" start end))
|
|
(let* ((line (or line ""))
|
|
(line-width (ebox--string-pixel-width line)))
|
|
(cond
|
|
((= start end) "")
|
|
((and (= start 0) (<= line-width end))
|
|
(ebox-composite--join
|
|
(list line (ebox-pixel-space (- end line-width)))))
|
|
((>= start line-width) (ebox-pixel-space (- end start)))
|
|
(t
|
|
(let ((position 0) (pixel 0) parts
|
|
run-start run-end run-left run-right)
|
|
(cl-labels
|
|
((flush ()
|
|
(when run-start
|
|
(push (ebox-composite--fit
|
|
line run-start run-end run-left run-right)
|
|
parts)
|
|
(setq run-start nil))))
|
|
(while (and (< position (length line)) (< pixel end))
|
|
(let* ((next (ebox-composite--unit-end line position))
|
|
(next-pixel (ebox--substring-pixel-width line 0 next))
|
|
(left (max start pixel))
|
|
(right (min end next-pixel)))
|
|
(cond
|
|
((and (>= pixel start) (<= next-pixel end))
|
|
(unless run-start
|
|
(setq run-start position run-left pixel))
|
|
(setq run-end next run-right next-pixel))
|
|
((< left right)
|
|
(flush)
|
|
(push (if (ebox--display-space-width
|
|
(get-text-property position 'display line))
|
|
(ebox-composite--space
|
|
line position next (- right left))
|
|
(ebox-pixel-space (- right left)))
|
|
parts)))
|
|
(setq position next pixel next-pixel)))
|
|
(flush))
|
|
(when (> end (max start pixel))
|
|
(push (ebox-pixel-space (- end (max start pixel))) parts))
|
|
(ebox-composite--join (nreverse parts)))))))
|
|
|
|
(defun ebox-composite--uncovered (start end covered)
|
|
"Return portions of [START, END) outside sorted merged COVERED intervals."
|
|
(let ((cursor start) result)
|
|
(dolist (interval covered)
|
|
(when (and (< cursor end) (> (cdr interval) cursor))
|
|
(when (> (car interval) cursor)
|
|
(push (cons cursor (min end (car interval))) result))
|
|
(setq cursor (max cursor (cdr interval)))))
|
|
(when (< cursor end) (push (cons cursor end) result))
|
|
(nreverse result)))
|
|
|
|
(defun ebox-composite--cover (start end covered)
|
|
"Return sorted merged COVERED intervals including [START, END).
|
|
Do not modify the input interval list or its cons cells."
|
|
(let (result)
|
|
(dolist (interval covered)
|
|
(cond
|
|
((< (cdr interval) start) (push interval result))
|
|
((> (car interval) end)
|
|
(push (cons start end) result)
|
|
(setq start (car interval) end (cdr interval)))
|
|
(t (setq start (min start (car interval))
|
|
end (max end (cdr interval))))))
|
|
(push (cons start end) result)
|
|
(nreverse result)))
|
|
|
|
(defun ebox-composite-line (base placements width)
|
|
"Compose BASE and opaque PLACEMENTS on a fixed WIDTH-pixel canvas.
|
|
BASE is a single-line string or nil. PLACEMENTS is a proper list of plists
|
|
with a single-line string :text and finite pixel :x, already ordered bottom
|
|
to top. Each placement covers the full measured width of its text, including
|
|
blanks. Negative and overflowing placements are clipped to the canvas.
|
|
WIDTH must be finite and nonnegative. Uncovered pixels beyond BASE receive
|
|
neutral fill. Compute coverage before extracting original source intervals,
|
|
so hidden placements cannot introduce cuts into a visible source glyph.
|
|
Do not modify BASE, PLACEMENTS, or any of their text property values."
|
|
(unless (and (ebox-composite--line-p base)
|
|
(proper-list-p placements)
|
|
(ebox-size--finite-number-p width) (>= width 0))
|
|
(error "Invalid Ebox composite canvas"))
|
|
(let (sources covered visible)
|
|
(dolist (placement placements)
|
|
(let ((text (plist-get placement :text))
|
|
(x (plist-get placement :x)))
|
|
(unless (and (stringp text) (ebox-composite--line-p text)
|
|
(ebox-size--finite-number-p x))
|
|
(error "Invalid Ebox composite placement: %S" placement))
|
|
(let* ((right (+ x (ebox--string-pixel-width text)))
|
|
(left (max 0 x))
|
|
(end (min width right)))
|
|
(unless (ebox-size--finite-number-p right)
|
|
(error "Ebox composite placement extent is not finite"))
|
|
(when (< left end)
|
|
(push (list left end text x) sources)))))
|
|
;; Sources were pushed from bottom to top. Only original source metadata
|
|
;; participates in occlusion; no sliced text is ever sliced again.
|
|
(dolist (source (append sources (list (list 0 width base 0))))
|
|
(pcase-let ((`(,start ,end ,text ,x) source))
|
|
(dolist (interval (ebox-composite--uncovered start end covered))
|
|
(push (list (car interval) (cdr interval) text x) visible))
|
|
(when (< start end)
|
|
(setq covered (ebox-composite--cover start end covered)))))
|
|
(ebox-composite--join
|
|
(mapcar
|
|
(lambda (interval)
|
|
(pcase-let ((`(,start ,end ,text ,x) interval))
|
|
(ebox-composite-slice text (- start x) (- end x))))
|
|
(sort visible (lambda (a b) (< (car a) (car b))))))))
|
|
|
|
(provide 'ebox-composite)
|
|
;;; ebox-composite.el ends here
|