fix: preserve paint across clipped overlap edges
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
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
This commit is contained in:
parent
993e09be6b
commit
8da2fe68cc
@ -78,8 +78,9 @@ another public node or wrapper.
|
||||
|
||||
`hidden` enforces the content width after composing children, preserving whole
|
||||
supported text clusters and excluding the container's own padding and border
|
||||
from the clipping area. Clipped-away child properties are removed with their
|
||||
text. Partial display spaces can be shortened; oversized glyphs/images are
|
||||
from the clipping area. Clipped-away child interaction properties are removed;
|
||||
partial-glyph fill retains the source faces and their paint dependencies.
|
||||
Partial display spaces can be shortened; oversized glyphs/images are
|
||||
omitted, not pixel-masked. Vertical clipping uses whole lines. `visible` allows
|
||||
overflow, and `scroll` provides vertical scrolling. No ellipsis or horizontal
|
||||
scrolling is implied.
|
||||
@ -151,7 +152,9 @@ an error before publication.
|
||||
This is an opaque text compositor: horizontal positions are pixels, vertical
|
||||
positions share the host's row grid. Positioned descendants must share their
|
||||
host's effective font line height. Whole supported text clusters and display
|
||||
units survive clipping; partial glyphs/images become neutral space. There is
|
||||
units survive clipping; partial glyphs/images become noninteractive space
|
||||
retaining source faces, including background paint. Canvas gaps remain neutral.
|
||||
There is
|
||||
no alpha blending, partial-glyph masking, or child-frame rendering. Layered
|
||||
trees use Elisp when the native representation cannot express their placement.
|
||||
See the [layer guide](ebox-user-guide.en.md#retained-layers) for examples and
|
||||
|
||||
@ -72,7 +72,8 @@ participation property 就是普通子 Box property,不会创建另一种公
|
||||
| 图层放置 | `:position`、`:left`、`:top`、`:z-index`、`:layer`、`:anchor`、`:placement` |
|
||||
|
||||
`hidden` 在组合子节点后约束内容宽度,保留完整的受支持文本簇;容器自身的 padding 与
|
||||
border 不在裁剪区域内。被裁掉的子文本同时移除其属性。固定 display space 可以缩短,
|
||||
border 不在裁剪区域内。被裁掉的子文本不再保留交互属性,半字形补白仍保留原 face
|
||||
及其绘制依赖。固定 display space 可以缩短,
|
||||
放不下的字形/图像则整体省去,不做像素遮罩。纵向按完整行裁剪。`visible` 允许溢出,
|
||||
`scroll` 提供纵向滚动;这些设置不隐含省略号或横向滚动。
|
||||
|
||||
@ -129,7 +130,8 @@ margin 不参与遮挡。绘制裁剪到宿主画布。
|
||||
|
||||
这是不透明文本合成:横向用像素,纵向共享宿主的文本行网格。定位子树必须与宿主
|
||||
使用相同的有效字体行高。裁剪保留完整的受支持文本簇和 display unit;不完整字形或
|
||||
图像以中性空白替代。不提供 alpha 混合、半字形遮罩或 child frame。native 表示
|
||||
图像以保留原 face(含背景色)的非交互空白替代,源内容之外的画布空白仍保持中性。
|
||||
不提供 alpha 混合、半字形遮罩或 child frame。native 表示
|
||||
无法表达图层放置时使用 Elisp。示例和 retained 更新行为见
|
||||
[图层指南](ebox-user-guide.zh.md#retained-layers)。
|
||||
|
||||
|
||||
@ -242,7 +242,8 @@ part of the author contract. `:overflow hidden` clips excess vertical lines and
|
||||
limits each content line to the measured content width. It retains the fitting
|
||||
prefix without splitting supported text clusters; a glyph or image that cannot
|
||||
fit is omitted as a whole. Fixed display spaces can be shortened to the exact
|
||||
remaining pixel width. Any remaining gap is filled with box content space, so
|
||||
remaining pixel width. A cut glyph's remaining gap keeps its source faces,
|
||||
including background paint, so
|
||||
padding and borders retain their positions. This is pixel-width truncation,
|
||||
not partial-glyph/image masking or automatic ellipsis. Omitted child text does
|
||||
not leave its help, hover, pointer, or keymap active in the filler.
|
||||
@ -342,7 +343,9 @@ dependency cycles are errors.
|
||||
Composition uses horizontal pixels and one shared vertical text-row grid.
|
||||
Positioned subtrees must have the same effective font line height as their
|
||||
host. Whole supported clusters survive cuts; partially covered glyphs/images
|
||||
are replaced with neutral space, and fixed display spaces can be shortened.
|
||||
are replaced with noninteractive space retaining the source faces (including
|
||||
background paint), and fixed display spaces can be shortened. Blank canvas
|
||||
outside the source remains neutral.
|
||||
There is no alpha blending, half-glyph masking, or child frame. Layered trees
|
||||
fall back to Elisp when native reflow cannot represent them.
|
||||
|
||||
|
||||
@ -210,7 +210,7 @@ Box 高度为零;`(box :height (lh 1))` 明确保留一行空白。
|
||||
接受纵向 `px` 长度。
|
||||
`:overflow hidden` 裁掉超出的纵向行,并将每行内容限制在测量后的内容宽度内。
|
||||
保留放得下的前缀,不拆开受支持的文本簇;放不下的字形或图像整体省去。固定 display
|
||||
space 可以缩短到精确的剩余像素宽度,剩余空隙用盒子内容空白补齐,使 padding 和边框
|
||||
space 可以缩短到精确的剩余像素宽度,被裁字形的剩余空隙保留原 face(含背景色),使 padding 和边框
|
||||
保持位置。这是按像素宽度截断,不是显示半个字形/图像,也不自动添加省略号。
|
||||
被省去的子文本不会在填充空白上留下自己的帮助、hover、指针或 keymap。
|
||||
|
||||
@ -292,7 +292,8 @@ Text 的样式声明只接受字体、前景/背景和文本装饰 property;
|
||||
缺失或不可见时不绘制面板;ID 歧义及放置依赖循环会报错。
|
||||
|
||||
合成横向使用像素,纵向共享一套文本行网格。定位子树的有效字体行高必须与宿主
|
||||
相同。裁剪保留完整的受支持文本簇,不完整字形或图像以中性空白替代,固定 display
|
||||
相同。裁剪保留完整的受支持文本簇,不完整字形或图像以保留原 face(含背景色)的
|
||||
非交互空白替代;源内容之外的画布空白仍保持中性。固定 display
|
||||
space 可以缩短。不提供 alpha 混合、半字形遮罩或 child frame。native reflow
|
||||
无法表示图层时回退到 Elisp。
|
||||
|
||||
|
||||
@ -9,6 +9,7 @@
|
||||
|
||||
(require 'cl-lib)
|
||||
(require 'ebox-measure)
|
||||
(require 'ebox-render-context)
|
||||
|
||||
(declare-function ebox--grapheme-cluster-end "ebox-layout" (string start))
|
||||
|
||||
@ -45,6 +46,31 @@ Retain the source properties and nested display values by identity."
|
||||
(cons 'space (plist-put properties :width (list width))) result)
|
||||
result))
|
||||
|
||||
(defun ebox-composite--fill (line position width)
|
||||
"Replace LINE's glyph at POSITION with WIDTH pixels of painted blank.
|
||||
Keep face values by identity so named paint slots remain live. Retain paint
|
||||
dependencies on the transient origin carrier, separately from the omitted
|
||||
glyph's display, interaction and semantic ownership properties."
|
||||
(let ((space (ebox-pixel-space width)))
|
||||
(dolist (property '(face font-lock-face))
|
||||
(when-let* ((value (get-text-property position property line)))
|
||||
(put-text-property 0 (length space) property value space)))
|
||||
(let ((origin (get-text-property position ebox--paint-origin-property line)))
|
||||
(when (or origin ebox--paint-origin-capture-p)
|
||||
(put-text-property
|
||||
0 (length space) ebox--paint-origin-property
|
||||
(ebox--paint-origin-create
|
||||
:baseline (if origin (ebox--paint-origin-baseline origin)
|
||||
(copy-tree (get-text-property position 'face line)))
|
||||
:source-roles
|
||||
(delete-dups
|
||||
(append (and origin
|
||||
(copy-sequence (ebox--paint-origin-source-roles origin)))
|
||||
(ebox--render-role-ids-from-properties
|
||||
(text-properties-at position line)))))
|
||||
space)))
|
||||
space))
|
||||
|
||||
(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
|
||||
@ -66,7 +92,7 @@ from collapsing or independently shaped source fragments from reshaping."
|
||||
"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
|
||||
run. A newly wider glyph becomes painted fill rather than shifting the next
|
||||
source glyph or borrowing its pixels."
|
||||
(let ((position start) (pixel left) parts)
|
||||
(while (< position end)
|
||||
@ -82,12 +108,13 @@ source glyph or borrowing its pixels."
|
||||
(cond
|
||||
((<= unit-width available)
|
||||
(push unit parts)
|
||||
(push (ebox-pixel-space (- available unit-width)) parts))
|
||||
(push (ebox-composite--fill
|
||||
line position (- 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)))
|
||||
(t (push (ebox-composite--fill line position available) parts)))
|
||||
(setq position next pixel next-pixel)))))
|
||||
(ebox-composite--join (nreverse parts))))
|
||||
|
||||
@ -97,9 +124,10 @@ 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."
|
||||
either edge; other partial glyphs become noninteractive blanks retaining their
|
||||
source faces 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)
|
||||
@ -139,7 +167,7 @@ isolated characters. Neither LINE nor its property values are modified."
|
||||
(get-text-property position 'display line))
|
||||
(ebox-composite--space
|
||||
line position next (- right left))
|
||||
(ebox-pixel-space (- right left)))
|
||||
(ebox-composite--fill line position (- right left)))
|
||||
parts)))
|
||||
(setq position next pixel next-pixel)))
|
||||
(flush))
|
||||
|
||||
@ -708,8 +708,8 @@ pairs. It is intentionally small; full Unicode segmentation is out of scope."
|
||||
"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."
|
||||
is omitted as a whole. Its remaining width retains the source faces, never
|
||||
the interaction properties of the 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.
|
||||
@ -740,7 +740,7 @@ an interactive stand-in for an omitted glyph."
|
||||
(concat
|
||||
(substring line 0 position)
|
||||
(when (> remaining 0)
|
||||
(let ((space (ebox-pixel-space remaining)))
|
||||
(let ((space (ebox-composite--fill line position remaining)))
|
||||
(when space-width
|
||||
(add-text-properties
|
||||
0 (length space)
|
||||
|
||||
@ -12,6 +12,7 @@
|
||||
(defvar ebox-layer--active-p)
|
||||
(defvar ebox-incremental--render-portals-p)
|
||||
(defvar ebox--render-source-index)
|
||||
(defvar ebox-region-types)
|
||||
|
||||
(declare-function ebox-interaction--refresh-hover! "ebox-interaction"
|
||||
(string start end &optional face))
|
||||
@ -102,8 +103,8 @@
|
||||
|
||||
(cl-defstruct (ebox--paint-origin
|
||||
(:constructor ebox--paint-origin-create))
|
||||
"Transient caller-face baseline captured before Ebox paint composition."
|
||||
baseline)
|
||||
"Transient caller-face baseline and optional paint-only source roles."
|
||||
baseline source-roles)
|
||||
|
||||
(defconst ebox--paint-origin-property 'ebox--paint-origin
|
||||
"Private transient property carrying one render-time paint baseline.")
|
||||
@ -115,6 +116,19 @@
|
||||
(make-hash-table :test #'eq :weakness 'key)
|
||||
"Weak map from rendered strings to Ebox-owned property values.")
|
||||
|
||||
(defun ebox--render-role-ids-from-properties (properties)
|
||||
"Return namespaced Ebox role/id pairs from rendered PROPERTIES."
|
||||
(let (roles)
|
||||
(when-let* ((region-id
|
||||
(plist-get properties 'ebox-overflow-foreground-source)))
|
||||
(push (cons 'overflow-foreground region-id) roles))
|
||||
(dolist (region-id (plist-get properties 'ebox-content-owners))
|
||||
(cl-pushnew (cons 'content-owner region-id) roles :test #'equal))
|
||||
(dolist (entry ebox-region-types)
|
||||
(when-let* ((region-id (plist-get properties (cdr entry))))
|
||||
(cl-pushnew (cons (car entry) region-id) roles :test #'equal)))
|
||||
(nreverse roles)))
|
||||
|
||||
(defun ebox--render-owned-text-values-for (property &optional create)
|
||||
"Return the active candidate registry for PROPERTY when CREATE is non-nil."
|
||||
(when (hash-table-p ebox--render-owned-text-values)
|
||||
|
||||
101
ebox-surface.el
101
ebox-surface.el
@ -3680,7 +3680,7 @@ properties. Its local renderer cannot prove equivalence for such an owner."
|
||||
(defun ebox-surface--retained-property-value-equal-p (left right)
|
||||
"Return non-nil when retaining LEFT instead of RIGHT preserves its value.
|
||||
Opaque payloads, including callbacks, keymaps and records, must keep identity.
|
||||
Render-created paint origins compare their captured baselines instead."
|
||||
Render-created paint origins compare their baselines and paint source roles."
|
||||
(let ((seen (make-hash-table :test #'eq)))
|
||||
(cl-labels
|
||||
((same
|
||||
@ -3688,8 +3688,10 @@ Render-created paint origins compare their captured baselines instead."
|
||||
(cond
|
||||
((eq old new) t)
|
||||
((and (ebox--paint-origin-p old) (ebox--paint-origin-p new))
|
||||
(same (ebox--paint-origin-baseline old)
|
||||
(ebox--paint-origin-baseline new)))
|
||||
(and (same (ebox--paint-origin-baseline old)
|
||||
(ebox--paint-origin-baseline new))
|
||||
(same (ebox--paint-origin-source-roles old)
|
||||
(ebox--paint-origin-source-roles new))))
|
||||
((or (functionp old) (functionp new)
|
||||
(keymapp old) (keymapp new)) nil)
|
||||
((memq new (gethash old seen)) t)
|
||||
@ -3786,18 +3788,21 @@ Return nil on a proof miss. The caller still validates the final footprint."
|
||||
(origin (with-current-buffer buffer (point-min)))
|
||||
result)
|
||||
(cl-labels
|
||||
((baseline-for
|
||||
((origin-for
|
||||
(start end published-properties)
|
||||
(while (and fragments (<= (plist-get (car fragments) :end) start))
|
||||
(setq fragments (cdr fragments)))
|
||||
(let ((remaining fragments) (position start) seen baseline)
|
||||
(let ((remaining fragments) (position start) seen baseline source-roles)
|
||||
(while (< position end)
|
||||
(let ((fragment (car remaining)))
|
||||
(unless (and fragment
|
||||
(<= (plist-get fragment :start) position)
|
||||
(< position (plist-get fragment :end)))
|
||||
(throw 'slot-property-miss nil))
|
||||
(let ((value
|
||||
(let ((roles (cl-set-difference
|
||||
(plist-get fragment :paint-role-ids)
|
||||
(plist-get fragment :role-ids) :test #'equal))
|
||||
(value
|
||||
(cond
|
||||
((plist-get fragment :face-baseline-known-p)
|
||||
(plist-get fragment :face-baseline))
|
||||
@ -3806,13 +3811,16 @@ Return nil on a proof miss. The caller still validates the final footprint."
|
||||
(plist-get published-properties 'face))
|
||||
(t (throw 'slot-property-miss nil)))))
|
||||
(when (and seen
|
||||
(not (ebox-surface--retained-property-value-equal-p
|
||||
baseline value)))
|
||||
(or (not (ebox-surface--retained-property-value-equal-p
|
||||
baseline value))
|
||||
(not (equal source-roles roles))))
|
||||
(throw 'slot-property-miss nil))
|
||||
(setq baseline value seen t))
|
||||
(setq baseline value source-roles roles seen t))
|
||||
(setq position (min end (plist-get fragment :end))
|
||||
remaining (cdr remaining))))
|
||||
baseline))
|
||||
(ebox--paint-origin-create
|
||||
:baseline (copy-tree baseline)
|
||||
:source-roles (copy-tree source-roles))))
|
||||
(pixel-filler-p
|
||||
(line start end properties values)
|
||||
(let ((display (plist-get properties 'display)))
|
||||
@ -3876,11 +3884,9 @@ Return nil on a proof miss. The caller still validates the final footprint."
|
||||
;; old fragment coordinates or peeling a composed face.
|
||||
(put-text-property
|
||||
new-position new-end ebox--paint-origin-property
|
||||
(ebox--paint-origin-create
|
||||
:baseline (copy-tree
|
||||
(baseline-for (+ (- (car span) origin) old-position)
|
||||
(+ (- (car span) origin) old-end)
|
||||
published-props)))
|
||||
(origin-for (+ (- (car span) origin) old-position)
|
||||
(+ (- (car span) origin) old-end)
|
||||
published-props)
|
||||
line)
|
||||
(setq old-position old-end new-position new-end)))
|
||||
(unless (and (= old-position old-length) (= new-position new-length))
|
||||
@ -3911,7 +3917,11 @@ captured baselines; an unknown baseline remains unknown for later proofs."
|
||||
(- (min end (plist-get fragment :end)) start)
|
||||
ebox--paint-origin-property
|
||||
(ebox--paint-origin-create
|
||||
:baseline (copy-tree (plist-get fragment :face-baseline)))
|
||||
:baseline (copy-tree (plist-get fragment :face-baseline))
|
||||
:source-roles
|
||||
(copy-tree
|
||||
(cl-set-difference (plist-get fragment :paint-role-ids)
|
||||
(plist-get fragment :role-ids) :test #'equal)))
|
||||
line))))))))
|
||||
lines)
|
||||
|
||||
@ -5485,16 +5495,7 @@ than replaying old coordinates, properties or revision authority."
|
||||
|
||||
(defun ebox-surface--role-ids-from-properties (properties)
|
||||
"Return namespaced Ebox role/id pairs from PROPERTIES."
|
||||
(let (roles)
|
||||
(when-let* ((region-id
|
||||
(plist-get properties 'ebox-overflow-foreground-source)))
|
||||
(push (cons 'overflow-foreground region-id) roles))
|
||||
(dolist (region-id (plist-get properties 'ebox-content-owners))
|
||||
(cl-pushnew (cons 'content-owner region-id) roles :test #'equal))
|
||||
(dolist (entry ebox-region-types)
|
||||
(when-let* ((region-id (plist-get properties (cdr entry))))
|
||||
(cl-pushnew (cons (car entry) region-id) roles :test #'equal)))
|
||||
(nreverse roles)))
|
||||
(ebox--render-role-ids-from-properties properties))
|
||||
|
||||
(defun ebox-surface--role-ids-at (rendered position &optional cache)
|
||||
"Return namespaced Ebox role/id pairs at POSITION in RENDERED.
|
||||
@ -5511,27 +5512,29 @@ one output projection."
|
||||
roles))))
|
||||
|
||||
(defun ebox-surface--paint-address-at
|
||||
(rendered position role-cache ordinal-table)
|
||||
(rendered position role-cache ordinal-table &optional paint-source-roles)
|
||||
"Return the semantic paint address at POSITION in RENDERED.
|
||||
ROLE-CACHE is the per-render role extraction cache and ORDINAL-TABLE tracks
|
||||
the first-to-last occurrence of an otherwise equal owner/content address.
|
||||
PAINT-SOURCE-ROLES identifies paint-only producers of clipped glyph fill.
|
||||
The result deliberately contains semantic owner/role facts only; it never
|
||||
uses a string, buffer, marker, or TP object identity as an address."
|
||||
(let* ((properties (text-properties-at position rendered))
|
||||
(roles (ebox-surface--role-ids-at rendered position role-cache))
|
||||
(content-owner (plist-get properties 'ebox-content-owner))
|
||||
(content-index (plist-get properties 'ebox-content-idx))
|
||||
(prefix (and (or roles content-owner (integerp content-index))
|
||||
(list :roles (copy-tree roles)
|
||||
:content-owner content-owner
|
||||
:content-index content-index)))
|
||||
(prefix (and (or roles content-owner (integerp content-index)
|
||||
paint-source-roles)
|
||||
(append
|
||||
(list :roles (copy-tree roles)
|
||||
:content-owner content-owner
|
||||
:content-index content-index)
|
||||
(when paint-source-roles
|
||||
(list :paint-source-roles (copy-tree paint-source-roles))))))
|
||||
(ordinal (and prefix (gethash prefix ordinal-table 0))))
|
||||
(when prefix
|
||||
(puthash prefix (1+ ordinal) ordinal-table)
|
||||
(list :roles (copy-tree roles)
|
||||
:content-owner content-owner
|
||||
:content-index content-index
|
||||
:ordinal ordinal))))
|
||||
(append prefix (list :ordinal ordinal)))))
|
||||
|
||||
(defun ebox-surface--candidate-plan-text (rendered owned-values)
|
||||
"Return a private snapshot of RENDERED for an owned TP plan.
|
||||
@ -5654,22 +5657,29 @@ when it needs to mutate them."
|
||||
ebox--paint-origin-property
|
||||
rendered))
|
||||
(roles (ebox-surface--role-ids-at rendered position role-cache))
|
||||
(paint-roles
|
||||
(if (and origin (ebox--paint-origin-source-roles origin))
|
||||
(delete-dups
|
||||
(append (copy-sequence (ebox--paint-origin-source-roles origin))
|
||||
roles))
|
||||
roles))
|
||||
(address
|
||||
(ebox-surface--paint-address-at
|
||||
rendered position role-cache ordinal-table))
|
||||
rendered position role-cache ordinal-table
|
||||
(cl-set-difference paint-roles roles :test #'equal)))
|
||||
(baseline
|
||||
(and origin
|
||||
(copy-tree
|
||||
(ebox--paint-origin-baseline origin)))))
|
||||
(push (list :text rendered :text-source-p t
|
||||
:start position :end next
|
||||
:line line :paint-role-ids roles
|
||||
:line line :paint-role-ids paint-roles
|
||||
:role-ids roles
|
||||
:paint-address address
|
||||
:paint-token
|
||||
(and address
|
||||
(list :baseline (copy-tree baseline)
|
||||
:roles (copy-tree roles)
|
||||
:roles (copy-tree paint-roles)
|
||||
:content-owner
|
||||
(plist-get address :content-owner)
|
||||
:content-index
|
||||
@ -6561,12 +6571,21 @@ relative face contributions and performs the final merge during prepare."
|
||||
fragment region-id ebox-surface--paint-base-roles)))
|
||||
(pair (cons role region-id)))
|
||||
(dolist (key '(:paint-role-ids :role-ids))
|
||||
(let ((roles (cl-remove pair (plist-get fragment key) :test #'equal)))
|
||||
(plist-put fragment key (if anchor-p (append roles (list pair)) roles))))
|
||||
(let* ((roles (cl-remove pair (plist-get fragment key) :test #'equal))
|
||||
(owned-p
|
||||
(or (eq key :paint-role-ids)
|
||||
(cl-some
|
||||
(lambda (entry)
|
||||
(and (equal (cdr entry) region-id)
|
||||
(memq (car entry) ebox-surface--paint-base-roles)))
|
||||
roles))))
|
||||
(plist-put fragment key
|
||||
(if (and anchor-p owned-p)
|
||||
(append roles (list pair)) roles))))
|
||||
(when (> (length (plist-get fragment :text)) 0)
|
||||
(if-let* ((owner
|
||||
(cdr (cl-find role
|
||||
(plist-get fragment :paint-role-ids)
|
||||
(plist-get fragment :role-ids)
|
||||
:key #'car :test #'eq :from-end t))))
|
||||
(put-text-property 0 (length (plist-get fragment :text))
|
||||
property owner (plist-get fragment :text))
|
||||
|
||||
@ -10,6 +10,7 @@
|
||||
(require 'cl-lib)
|
||||
(require 'ebox)
|
||||
(require 'ebox-composite)
|
||||
(require 'ebox-fixtures)
|
||||
|
||||
(defun ebox-composite-test--plain-width (text)
|
||||
"Measure TEXT with seven-pixel glyphs, zero-width marks, and fi shaping."
|
||||
@ -90,6 +91,266 @@
|
||||
'keymap nil output)))))
|
||||
(should (equal (ebox-composite-slice "abc" 2 2) ""))))
|
||||
|
||||
(ert-deftest ebox-composite-cut-fill-keeps-source-paint-without-interaction ()
|
||||
"Cut glyphs keep their face, including dynamic slots, but not source actions."
|
||||
(ebox-composite-test--with-metrics
|
||||
(let* ((slot (tp-paint-slot-create '(:background "#eee5ce")))
|
||||
(face (list 'bold (tp-paint-slot-face slot)))
|
||||
(font-lock-face (list :background "#765fa1"))
|
||||
(source (propertize "ABCD" 'face face 'font-lock-face font-lock-face
|
||||
'keymap (make-sparse-keymap) 'help-echo "hidden"
|
||||
'mouse-face 'highlight 'ebox-owner '(owner)))
|
||||
(before (copy-sequence source))
|
||||
(output (ebox-composite-slice source 3 24)))
|
||||
(dolist (position '(0 3))
|
||||
(should (eq (get-text-property position 'face output) face))
|
||||
(should (eq (get-text-property position 'font-lock-face output)
|
||||
font-lock-face))
|
||||
(dolist (property '(keymap help-echo mouse-face ebox-owner))
|
||||
(should-not (get-text-property position property output))))
|
||||
(ebox-composite-test--assert-width output 21)
|
||||
(tp-paint-slot-update slot '(:background "#aabbcc"))
|
||||
(should (memq (tp-paint-slot-face slot)
|
||||
(get-text-property 0 'face output)))
|
||||
(should (equal (face-attribute (tp-paint-slot-face slot) :background)
|
||||
"#aabbcc"))
|
||||
(should (equal-including-properties source before)))))
|
||||
|
||||
(ert-deftest ebox-composite-cut-fill-covers-shaping-and-replacement-boundaries ()
|
||||
"Reshaped glyphs, images and replacement strings retain source paint."
|
||||
(ebox-composite-test--with-metrics
|
||||
(let ((face (list :background "#eee5ce")))
|
||||
(dolist (source (list (propertize "fiZ" 'face face)
|
||||
(propertize "abc" 'face face 'display "WXYZ")
|
||||
(propertize "abc" 'face face 'display
|
||||
'(image :type xpm :width 28))))
|
||||
(let ((output (ebox-composite-slice source 7 18)))
|
||||
(should (eq (get-text-property 0 'face output) face))
|
||||
(ebox-composite-test--assert-width output 11))))))
|
||||
|
||||
(ert-deftest ebox-composite-overlap-keeps-underlay-paint-at-both-cut-edges ()
|
||||
"Occluding part of two glyphs preserves both exposed background strips."
|
||||
(ebox-composite-test--with-metrics
|
||||
(let* ((under (list :background "#eee5ce"))
|
||||
(over (list :background "#20353e"))
|
||||
(base (propertize "ABCD" 'face under))
|
||||
(top (propertize (ebox-pixel-space 10) 'face over))
|
||||
(output (ebox-composite-line base (list (list :text top :x 3)) 35)))
|
||||
(should (eq (get-text-property 0 'face output) under))
|
||||
(should (eq (get-text-property 1 'face output) over))
|
||||
(should (eq (get-text-property 2 'face output) under))
|
||||
(should-not (get-text-property (1- (length output)) 'face output))
|
||||
(ebox-composite-test--assert-width output 35))))
|
||||
|
||||
(ert-deftest ebox-composite-layout-cut-keeps-rich-child-paint ()
|
||||
"Layout clipping preserves child paint and does not resurrect child actions."
|
||||
(ebox-composite-test--with-metrics
|
||||
(dolist (child '("ABCD"
|
||||
(text :background-color "red" :help-echo "child" "ABCD")
|
||||
(box :background-color "red" :help-echo "child" "ABCD")))
|
||||
(let* ((output (ebox-render
|
||||
(ebox-build
|
||||
`(box :width (px 17) :wrap-mode none :overflow hidden
|
||||
:background-color "blue" ,child))))
|
||||
(fill (1- (length output)))
|
||||
(expected (if (stringp child) "blue" "red")))
|
||||
(should (equal (substring-no-properties output) "AB "))
|
||||
(should (member expected
|
||||
(flatten-tree (get-text-property fill 'face output))))
|
||||
(should-not (get-text-property fill 'help-echo output))
|
||||
(ebox-composite-test--assert-width output 17)))))
|
||||
|
||||
(ert-deftest ebox-composite-shaped-shorter-unit-keeps-painted-remainder ()
|
||||
"A detached narrower glyph leaves painted fill in its original allocation."
|
||||
(ebox-composite-test--with-metrics
|
||||
(cl-letf (((symbol-function 'ebox-composite-test--plain-width)
|
||||
(lambda (text)
|
||||
(+ (* 7 (length text))
|
||||
(if (string-match-p "fi" text) 3 0)))))
|
||||
(let* ((face (list :background "#eee5ce"))
|
||||
(output (ebox-composite-slice (propertize "fiZ" 'face face) 7 24)))
|
||||
(should (equal (substring-no-properties output) "i Z"))
|
||||
(should (eq (get-text-property 1 'face output) face))
|
||||
(ebox-composite-test--assert-width output 17)))))
|
||||
|
||||
(ert-deftest ebox-composite-mounted-overlap-keeps-painted-edges-after-updates ()
|
||||
"Real publication preserves edge paint when moving, recoloring and hiding."
|
||||
(ebox-composite-test--with-metrics
|
||||
(with-temp-buffer
|
||||
(unwind-protect
|
||||
(progn
|
||||
(ebox-render-to-buffer
|
||||
(current-buffer)
|
||||
(ebox-build
|
||||
'(box :width (px 35) :height (lh 1) :background-color "blue"
|
||||
(box :id "lower" :width (px 35) :height (lh 1)
|
||||
:background-color "red" "ABCDE")
|
||||
(box :id "upper" :position absolute :left (px 3)
|
||||
:width (px 10) :height (lh 1)
|
||||
:background-color "cyan"))))
|
||||
(cl-labels
|
||||
((check (left color visible)
|
||||
(let* ((actual (buffer-string))
|
||||
(fresh (ebox-render
|
||||
(plist-get (ebox-surface-buffer-snapshot
|
||||
(current-buffer)) :input))))
|
||||
(ebox-composite-test--assert-width actual 35)
|
||||
(should (equal (substring-no-properties actual)
|
||||
(substring-no-properties fresh)))
|
||||
(dotimes (position (length actual))
|
||||
(let* ((x (ebox--substring-pixel-width actual 0 position))
|
||||
(end (ebox--substring-pixel-width
|
||||
actual 0 (1+ position))))
|
||||
(when (< x end)
|
||||
(should
|
||||
(equal (cadr (memq :background
|
||||
(flatten-tree
|
||||
(get-text-property
|
||||
position 'face actual))))
|
||||
(if (and visible (>= x left) (< x (+ left 10)))
|
||||
"cyan" color))))
|
||||
(should (equal (get-text-property position 'face actual)
|
||||
(get-text-property position 'face fresh))))))))
|
||||
(check 3 "red" t)
|
||||
(ebox-region-update (ebox-region-resolve (current-buffer) "upper")
|
||||
:left '(px 9))
|
||||
(check 9 "red" t)
|
||||
(ebox-region-update (ebox-region-resolve (current-buffer) "lower")
|
||||
:background-color "orange")
|
||||
(check 9 "orange" t)
|
||||
(ebox-region-update (ebox-region-resolve (current-buffer) "upper")
|
||||
:visibility 'hidden)
|
||||
(check 9 "orange" nil)))
|
||||
(when (ebox-surface-buffer-mounted-p (current-buffer))
|
||||
(ebox-unmount-buffer (current-buffer)))))))
|
||||
|
||||
(ert-deftest ebox-composite-mounted-layout-fill-tracks-child-paint ()
|
||||
"Clipped fill follows paint, content, border and mixed geometry updates."
|
||||
(ebox-composite-test--with-metrics
|
||||
(dolist (kind '(text box))
|
||||
(with-temp-buffer
|
||||
(unwind-protect
|
||||
(progn
|
||||
(ebox-render-to-buffer
|
||||
(current-buffer)
|
||||
(ebox-build
|
||||
`(box :id "parent" :width (px 17) :wrap-mode none :overflow hidden
|
||||
:background-color "blue"
|
||||
(,kind :id "child" :background-color "red"
|
||||
:help-echo "child" "ABCD"))))
|
||||
(cl-labels
|
||||
((check (color)
|
||||
(let* ((actual (buffer-string))
|
||||
(fill (1- (length actual)))
|
||||
(fresh (ebox-render
|
||||
(plist-get (ebox-surface-buffer-snapshot
|
||||
(current-buffer)) :input))))
|
||||
(should (equal (substring-no-properties actual)
|
||||
(substring-no-properties fresh)))
|
||||
(should (equal (cadr (memq :background
|
||||
(flatten-tree
|
||||
(get-text-property fill 'face actual))))
|
||||
color))
|
||||
(should-not (get-text-property fill 'help-echo actual))
|
||||
(should-not (get-text-property fill 'ebox-content actual))
|
||||
(should-not (get-text-property fill ebox--paint-origin-property actual))
|
||||
(dotimes (position (length actual))
|
||||
(should (equal (get-text-property position 'face actual)
|
||||
(get-text-property position 'face fresh)))))))
|
||||
(check "red")
|
||||
(ebox-region-update "child" :background-color "yellow")
|
||||
(check "yellow")
|
||||
(dolist (failure-step '(text client-state))
|
||||
(let ((before (buffer-string))
|
||||
(state (ebox--buffer-render-state (current-buffer)))
|
||||
(revision (ebox-surface-buffer-revision (current-buffer)))
|
||||
(tp--surface-publication-step-function
|
||||
(lambda (step _surface)
|
||||
(when (eq step failure-step) (error "Reject fill paint")))))
|
||||
(should-error (ebox-region-update "child" :background-color "orange"))
|
||||
(should (equal-including-properties before (buffer-string)))
|
||||
(should (eq state (ebox--buffer-render-state (current-buffer))))
|
||||
(should (= revision (ebox-surface-buffer-revision (current-buffer))))))
|
||||
(check "yellow")
|
||||
(ebox-region-update "parent" :background-color "green")
|
||||
(check "yellow")
|
||||
(when (eq kind 'box)
|
||||
(ebox-region-update "child" :border-top-width '(lh 1)
|
||||
:border-top-style 'solid :border-top-color "pink")
|
||||
(check "yellow")
|
||||
(ebox-region-update "child" :border-top-width '(lh 0))
|
||||
(check "yellow"))
|
||||
(ebox-region-update "child" :content "WXYZ")
|
||||
(check "yellow")
|
||||
(ebox-call-with-update-batch
|
||||
(lambda ()
|
||||
(ebox-region-update "parent" :width '(px 18))
|
||||
(ebox-region-update "child" :background-color "purple")))
|
||||
(check "purple")))
|
||||
(when (ebox-surface-buffer-mounted-p (current-buffer))
|
||||
(ebox-unmount-buffer (current-buffer))))))))
|
||||
|
||||
(ert-deftest ebox-composite-retained-slot-fill-keeps-paint-origin ()
|
||||
"Transported clipped content still tracks later paint and rolls back safely."
|
||||
(ebox-composite-test--with-metrics
|
||||
(with-temp-buffer
|
||||
(unwind-protect
|
||||
(cl-labels
|
||||
((paint (content color)
|
||||
(ebox-test-text content :key 'paint-text :source-identity 'paint-text
|
||||
:bgcolor color))
|
||||
(slot (content)
|
||||
(ebox-test-box :key 'left :source-identity 'left
|
||||
:wrap-mode 'none :overflow 'hidden
|
||||
(paint content "red")))
|
||||
(root (content)
|
||||
(ebox-test-box
|
||||
:bgcolor "blue"
|
||||
(ebox-test-grid :key 'grid :width '(60)
|
||||
:grid-template-columns '((17) (36)) :column-gap '(7)
|
||||
(slot content)
|
||||
(ebox-test-box :key 'right :source-identity 'right
|
||||
(ebox-test-text "R")))))
|
||||
(repaint (content)
|
||||
(let ((candidate (ebox-candidate-begin (current-buffer))))
|
||||
(ebox-candidate-patch-host-paint
|
||||
candidate 'paint-text (paint content "red") (paint content "yellow"))
|
||||
(ebox-commit (current-buffer) candidate))))
|
||||
(ebox-render-to-buffer (current-buffer) (root "ABCD"))
|
||||
(let ((content (concat "A" (string #x0301) "BCD"))
|
||||
(transport (symbol-function 'ebox-surface--retained-slot-owner-text))
|
||||
(transported 0))
|
||||
(cl-letf (((symbol-function 'ebox-surface--retained-slot-owner-text)
|
||||
(lambda (&rest args)
|
||||
(let ((result (apply transport args)))
|
||||
(when result (cl-incf transported))
|
||||
result))))
|
||||
(let ((candidate (ebox-candidate-begin (current-buffer))))
|
||||
(ebox-candidate-replace-host-ref candidate 'left (slot content))
|
||||
(ebox-commit (current-buffer) candidate)))
|
||||
(should (= transported 1))
|
||||
(dolist (failure-step '(text client-state))
|
||||
(let ((before (buffer-string))
|
||||
(revision (ebox-surface-buffer-revision (current-buffer)))
|
||||
(tp--surface-publication-step-function
|
||||
(lambda (step _surface)
|
||||
(when (eq step failure-step) (error "Reject transported paint")))))
|
||||
(should-error (repaint content))
|
||||
(should (equal-including-properties before (buffer-string)))
|
||||
(should (= revision (ebox-surface-buffer-revision (current-buffer))))))
|
||||
(repaint content)
|
||||
(let ((actual (buffer-string))
|
||||
(fresh (ebox-render (plist-get
|
||||
(ebox-surface-buffer-snapshot (current-buffer))
|
||||
:input))))
|
||||
(should (equal (substring-no-properties actual)
|
||||
(substring-no-properties fresh)))
|
||||
(dotimes (position (length actual))
|
||||
(should (equal (get-text-property position 'face actual)
|
||||
(get-text-property position 'face fresh)))))))
|
||||
(when (ebox-surface-buffer-mounted-p (current-buffer))
|
||||
(ebox-unmount-buffer (current-buffer)))))))
|
||||
|
||||
(ert-deftest ebox-composite-slice-keeps-supported-graphemes-whole ()
|
||||
"Both cut edges preserve combining, variation, modifier, ZWJ, and flags."
|
||||
(ebox-composite-test--with-metrics
|
||||
|
||||
@ -1101,6 +1101,35 @@ candidate cannot hide mutations by restoring the old hash-table pointer."
|
||||
(should (plist-get first :face-baseline-known-p))
|
||||
(should-not (get-text-property 0 ebox--paint-origin-property rendered)))))
|
||||
|
||||
(ert-deftest ebox-surface-clipped-paint-origin-stays-separate-from-ownership ()
|
||||
"Reclipping retains the paint producer without recovering omitted semantics."
|
||||
(let* ((ebox--paint-origin-capture-p t)
|
||||
(face '(:weight bold))
|
||||
(source (propertize "A" 'face face 'ebox-content 7
|
||||
'help-echo "child" 'keymap (make-sparse-keymap)))
|
||||
(fill (ebox-composite--fill source 0 5)))
|
||||
(put-text-property 0 (length fill) 'ebox-content-owner 2 fill)
|
||||
(let* ((reclipped (ebox-composite-slice fill 1 4))
|
||||
(origin (get-text-property 0 ebox--paint-origin-property reclipped))
|
||||
(other (ebox--paint-origin-create :baseline face
|
||||
:source-roles '((content . 8))))
|
||||
(fragment (car (ebox-surface--rendered-fragments reclipped))))
|
||||
(should (equal (plist-get fragment :role-ids) '((content-owner . 2))))
|
||||
(should (member '(content . 7) (plist-get fragment :paint-role-ids)))
|
||||
(should (equal (plist-get fragment :face-baseline) face))
|
||||
(should-not (ebox-surface--retained-property-value-equal-p origin other))
|
||||
(should (equal (plist-get (plist-get fragment :paint-address) :paint-source-roles)
|
||||
'((content . 7))))
|
||||
(dolist (property '(help-echo keymap ebox-content ebox--paint-origin))
|
||||
(should-not (get-text-property 0 property reclipped)))
|
||||
;; Border paint can follow the omitted child, but child semantic tags
|
||||
;; must not appear on the parent-owned replacement.
|
||||
(plist-put fragment :text reclipped)
|
||||
(ebox-surface--set-horizontal-border-role (list fragment) 7 'bt t)
|
||||
(should (member '(bt . 7) (plist-get fragment :paint-role-ids)))
|
||||
(should-not (member '(bt . 7) (plist-get fragment :role-ids)))
|
||||
(should-not (get-text-property 0 'ebox-bt (plist-get fragment :text))))))
|
||||
|
||||
(ert-deftest ebox-native-layout-ir-preserves-default-foreground-face ()
|
||||
"Serialize and optionally render the Ebox default foreground reset."
|
||||
(let* ((ebox-viewport-width 80)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user