mirror of
https://github.com/penpot/penpot.git
synced 2026-10-02 16:56:16 +00:00
fix comments
This commit is contained in:
parent
c6ce618bab
commit
116c82572f
@ -163,13 +163,9 @@
|
||||
|
||||
(def ^:private unicode-ranges
|
||||
{:japanese #"[\u3040-\u30FF\u31F0-\u31FF\uFF66-\uFF9F]"
|
||||
;; Han ideographs are shared by Japanese/Chinese/Korean (Han
|
||||
;; unification) and cannot identify a language by themselves; they
|
||||
;; are resolved to a concrete language by `resolve-ambiguous-cjk`.
|
||||
;; Han ideographs, shared by all CJK languages; see `resolve-ambiguous-cjk`.
|
||||
:han #"[\u4E00-\u9FFF\u3400-\u4DBF\uF900-\uFAFF]"
|
||||
;; CJK symbols/punctuation (U+3000-303F) and half/full-width forms
|
||||
;; (U+FF01-FF65, U+FFE0-FFEE) are likewise shared across
|
||||
;; Japanese/Chinese/Korean; resolved by `resolve-ambiguous-cjk`.
|
||||
;; CJK punctuation and half/full-width forms, shared; see `resolve-ambiguous-cjk`.
|
||||
:cjk-punctuation #"[\u3000-\u303F\uFF01-\uFF65\uFFE0-\uFFEE]"
|
||||
:korean #"[\uAC00-\uD7AF]"
|
||||
:arabic #"[\u0600-\u06FF\u0750-\u077F\u0870-\u089F\u08A0-\u08FF]"
|
||||
|
||||
@ -9,16 +9,14 @@
|
||||
[app.common.data.macros :as dm]
|
||||
[cuerdas.core :as str]))
|
||||
|
||||
;; Vertical writing (tategaki). Absent values behave as "horizontal-tb"
|
||||
;; and "mixed", so plain horizontal text never stores these attrs.
|
||||
;; Vertical writing (tategaki); absent means "horizontal-tb" and "mixed".
|
||||
(def text-writing-mode-attrs
|
||||
[:writing-mode])
|
||||
|
||||
(def text-orientation-attrs
|
||||
[:text-orientation])
|
||||
|
||||
;; Paragraph attrs that act as whole-shape properties: every paragraph must
|
||||
;; carry the first paragraph's value, since that one decides the flow.
|
||||
;; Whole-shape paragraph attrs: every paragraph carries the first one's value.
|
||||
(def whole-shape-paragraph-attrs
|
||||
(into text-writing-mode-attrs text-orientation-attrs))
|
||||
|
||||
@ -38,16 +36,14 @@
|
||||
:ruby-overhang
|
||||
:ruby-side])
|
||||
|
||||
;; Warichu (割注): the span renders as two half-size lines stacked inline
|
||||
;; within one column position. Values "warichu" / "none"; absent means off.
|
||||
;; Warichu (割注): two half-size lines in one line position; "warichu" or "none".
|
||||
(def text-warichu-attrs
|
||||
[:warichu])
|
||||
|
||||
(def text-font-features-attrs
|
||||
[:font-features])
|
||||
|
||||
;; Annotation collision policy. "none" preserves the explicit line height;
|
||||
;; "auto" reserves an additional half-em layer for ruby and emphasis.
|
||||
;; "auto" adds a half-em layer per ruby/emphasis; "none" keeps the line height.
|
||||
(def text-annotation-clearance-attrs
|
||||
[:annotation-clearance])
|
||||
|
||||
@ -90,8 +86,7 @@
|
||||
0.5))
|
||||
|
||||
(defn visible-ruby
|
||||
"Ruby annotation text of a text node, or nil when it has none or the
|
||||
annotation is hidden."
|
||||
"Ruby annotation text of a text node, or nil when absent or hidden."
|
||||
[node]
|
||||
(let [ruby (:ruby node)]
|
||||
(when (and (string? ruby)
|
||||
@ -100,8 +95,8 @@
|
||||
ruby)))
|
||||
|
||||
(defn warichu-text?
|
||||
"True when a text node renders as warichu: it needs two characters to fill
|
||||
its two sub-lines."
|
||||
"True when a text node renders as warichu, which needs at least two
|
||||
characters for its two sub-lines."
|
||||
[node]
|
||||
(let [text (:text node)]
|
||||
(and (= "warichu" (:warichu node))
|
||||
|
||||
@ -173,9 +173,7 @@
|
||||
height (dm/get-prop shape :height)
|
||||
content (get shape :content)
|
||||
|
||||
;; Vertical writing anchors columns to the box edges, so the oversized
|
||||
;; auto-grow box used to avoid horizontal wrapping/clipping would push
|
||||
;; the content off-position. Use the real selrect size instead.
|
||||
;; Vertical text anchors columns to the box edges, so it skips the oversized auto-grow box.
|
||||
vertical? (wm/vertical-text-content? content)
|
||||
|
||||
[colors _color-mapping color-mapping-inverse] (retrieve-colors shape)]
|
||||
|
||||
@ -82,9 +82,7 @@
|
||||
|
||||
content (if is-code (legacy.txt/index-content content) content)
|
||||
|
||||
;; Vertical writing anchors columns to the box edges; the oversized
|
||||
;; auto-grow box (used to avoid horizontal wrapping) would push content
|
||||
;; off-position, so use the real selrect size instead.
|
||||
;; Vertical text anchors columns to the box edges, so it skips the oversized auto-grow box.
|
||||
vertical? (wm/vertical-text-content? content)
|
||||
|
||||
style
|
||||
|
||||
@ -23,8 +23,7 @@
|
||||
(generate-root-styles props node false))
|
||||
([{:keys [width height]} node code?]
|
||||
(let [valign (:vertical-align node "top")
|
||||
;; Mirroring the shape's writing mode on the root makes paragraph
|
||||
;; blocks stack right-to-left.
|
||||
;; The root writing mode makes paragraph blocks stack right-to-left.
|
||||
writing-mode (wm/content-writing-mode node)
|
||||
base #js {:height (when-not code? (fmt/format-pixels height))
|
||||
:width (when-not code? (fmt/format-pixels width))
|
||||
@ -123,8 +122,8 @@
|
||||
(and (string? text-combine-upright) (pos? (alength text-combine-upright)))
|
||||
(obj/set! "textCombineUpright" (css-text-combine-upright text-combine-upright))
|
||||
|
||||
;; Emphasis marks map to CSS text-emphasis-style: our kebab values
|
||||
;; ("filled-dot") become the CSS "<fill> <shape>" pair ("filled dot").
|
||||
;; Stored kebab values ("filled-dot") become the CSS "<fill> <shape>"
|
||||
;; pair ("filled dot").
|
||||
(set-value? text-emphasis)
|
||||
(obj/set! "textEmphasis" (str/replace text-emphasis "-" " "))
|
||||
|
||||
@ -137,10 +136,8 @@
|
||||
(some? line-height)
|
||||
(obj/set! "lineHeight" line-height)
|
||||
|
||||
;; Warichu (割注) CSS emulation: an inline-block at half size whose
|
||||
;; inline-size fits half the characters, so the browser wraps it into
|
||||
;; two half-size sub-lines within one inline position in either writing
|
||||
;; mode.
|
||||
;; Warichu (割注): a half-size inline-block as wide as half the
|
||||
;; characters, so the browser wraps it into two sub-lines.
|
||||
(jl/warichu-text? data)
|
||||
(-> (obj/set! "display" "inline-block")
|
||||
(obj/set! "fontSize" (if (and (string? font-size) (pos? (alength font-size)))
|
||||
|
||||
@ -232,38 +232,30 @@
|
||||
(dm/str "mousetrap " (stl/css :text-editor-container)))
|
||||
|
||||
(def ^:private ime-surface-collapse
|
||||
"Scale applied along the inline axis of the capture surface, so its text
|
||||
(retained input or an in-flight composition) never moves the DOM caret."
|
||||
"Inline-axis scale of the capture surface, so its text never moves the DOM caret."
|
||||
0.001)
|
||||
|
||||
(defn- update-ime-caret!
|
||||
"Move the hidden contenteditable capture surface onto the WASM caret so the
|
||||
browser's IME candidate window opens next to the edited text. Returns the
|
||||
caret rectangle it anchored to, or nil when there is none yet.
|
||||
"Move the hidden capture surface onto the WASM caret so the IME candidate
|
||||
window opens next to the edited text. Returns the caret rect, or nil when
|
||||
there is none yet.
|
||||
|
||||
Must only be called while the editor is idle, never during or immediately
|
||||
before an IME composition: real IMEs (ibus-mozc) abort and commit the
|
||||
pending text on any mutation of the composing element, including style
|
||||
writes on the keydown-229 that precedes compositionstart. The surface is
|
||||
therefore kept on the caret at all times, updated after every
|
||||
caret-affecting operation (click, arrows, typing, commit). During a
|
||||
composition the window follows the WASM caret by moving the surface's
|
||||
wrapper instead (see `follow-ime-caret!`).
|
||||
Call only while idle, never during or right before a composition: real
|
||||
IMEs (ibus-mozc) abort and commit on any mutation of the composing
|
||||
element, including style writes on the keydown-229 before
|
||||
compositionstart. So the surface follows the caret after every
|
||||
caret-affecting operation, and during a composition only its wrapper
|
||||
moves (see `follow-ime-caret!`).
|
||||
|
||||
The browser anchors the candidate window to the DOM caret inside the
|
||||
surface. The surface is collapsed along the inline axis, so neither the text
|
||||
it retains between keystrokes (see `keep-input-alive`) nor the composition
|
||||
moves that caret. The block axis keeps the caret cell size (font-size = line
|
||||
height horizontally, column width vertically).
|
||||
The IME anchors its window to the DOM caret inside the surface. The
|
||||
surface is collapsed along the inline axis, so neither its retained text
|
||||
(see `keep-input-alive`) nor the composition moves that caret; the block
|
||||
axis keeps the caret cell size. The window opens below the caret, so in
|
||||
vertical text the surface (`vertical-rl`, `line-height: 1`) sits right of
|
||||
the caret column and the window opens beside the composition.
|
||||
|
||||
The IME opens the window below the caret. Horizontally that clears the
|
||||
current line. Vertically the text itself flows down, so the surface (with
|
||||
`writing-mode: vertical-rl` and `line-height: 1`, making its column exactly
|
||||
one caret column wide) sits just right of the caret column: the window opens
|
||||
beside the composition instead of over it.
|
||||
|
||||
The caret rectangle is in the same page space as the overlay foreignObject,
|
||||
whose top-left and width are passed as `origin`."
|
||||
`origin` holds the overlay foreignObject's top-left and width, in the
|
||||
same page space as the caret rect."
|
||||
[^js node ^js wrapper origin]
|
||||
(when (and (some? node) (some? origin))
|
||||
(when-let [{:keys [x y width height] :as rect} (text-editor/text-editor-get-cursor-rect)]
|
||||
@ -275,8 +267,8 @@
|
||||
(set! (.-lineHeight style) "1")
|
||||
(set! (.-transformOrigin style) "100% 0")
|
||||
(set! (.-transform style) (dm/str "scaleY(" ime-surface-collapse ")"))
|
||||
;; Surface column starts past the caret column's right edge, plus a
|
||||
;; small gap so the window border stays clear of the glyphs.
|
||||
;; Start past the caret column's right edge, plus a small gap so
|
||||
;; the window border clears the glyphs.
|
||||
(set! (.-left style) (dm/str (- (+ x (* 2.15 width)) (:x origin) (:width origin)) "px")))
|
||||
(do
|
||||
(set! (.-writingMode style) "")
|
||||
@ -324,9 +316,9 @@
|
||||
|
||||
(defn- follow-ime-caret!
|
||||
"Translate the surface wrapper by how far the WASM caret moved from `anchor`,
|
||||
the caret rectangle the surface was placed on. Only the wrapper is written,
|
||||
never the composing element, so it is safe while a composition is in flight.
|
||||
Returns nil when the caret rectangle is not available yet."
|
||||
the caret rect the surface was placed on. Writes only the wrapper, never the
|
||||
composing element, so it is safe mid-composition. Returns nil when the caret
|
||||
rect is not available yet."
|
||||
[^js wrapper anchor]
|
||||
(when (and (some? wrapper) (some? anchor))
|
||||
(when-let [{:keys [x y]} (text-editor/text-editor-get-cursor-rect)]
|
||||
@ -356,28 +348,23 @@
|
||||
|
||||
deferred-press-ref (mf/use-ref nil)
|
||||
|
||||
;; Overlay foreignObject top-left and width in page space, used to
|
||||
;; translate the WASM caret rectangle into an offset inside it.
|
||||
;; Overlay foreignObject top-left and width in page space (see `update-ime-caret!`).
|
||||
origin-ref (mf/use-ref nil)
|
||||
|
||||
;; True between compositionstart and compositionend; guards the surface
|
||||
;; against any repositioning while the IME owns it.
|
||||
;; True between compositionstart and compositionend; the surface must not move then.
|
||||
composing-ref (mf/use-ref false)
|
||||
|
||||
;; Positioned wrapper of the surface, moved during a composition so the
|
||||
;; IME window follows the WASM caret (see `follow-ime-caret!`).
|
||||
;; Surface wrapper, moved during a composition (see `follow-ime-caret!`).
|
||||
wrapper-ref (mf/use-ref nil)
|
||||
|
||||
;; Caret rectangle the surface was last placed on while idle.
|
||||
anchor-ref (mf/use-ref nil)
|
||||
|
||||
;; Surface text length (UTF-16) before the composition, and the current
|
||||
;; composition text: locate the IME cursor within the preview.
|
||||
;; Surface UTF-16 length before the composition, and the composition text.
|
||||
composition-base-ref (mf/use-ref 0)
|
||||
composition-text-ref (mf/use-ref "")
|
||||
|
||||
;; IME cursor offset last read from the DOM selection, nil right after a
|
||||
;; text change until the next read.
|
||||
;; IME cursor offset last read from the DOM selection; nil after a text change.
|
||||
synced-offset-ref (mf/use-ref nil)
|
||||
|
||||
fallback-fonts (wasm.api/fonts-from-text-content (:content shape) false)
|
||||
@ -409,10 +396,9 @@
|
||||
"center" (+ y (/ (- selrect-height height) 2))
|
||||
y)]
|
||||
;; Vertical text anchors the IME window right of the caret column,
|
||||
;; which for the rightmost column lies past the overlay. The
|
||||
;; foreignObject is widened (not the clip) so the anchor stays inside
|
||||
;; it; otherwise the browser scrolls it to reveal the caret and
|
||||
;; cancels the offset.
|
||||
;; past the overlay for the rightmost column. Widen the foreignObject
|
||||
;; (not the clip) to keep the anchor inside it, or the browser scrolls
|
||||
;; to reveal the caret and cancels the offset.
|
||||
[(assoc selrect
|
||||
:y y :width overlay-width :height max-height
|
||||
:ime-width (cond-> overlay-width vertical? (+ viewport-width)))
|
||||
@ -421,11 +407,9 @@
|
||||
schedule-ime-caret!
|
||||
(mf/use-fn
|
||||
(fn []
|
||||
;; Wait two frames so the pending WASM render rebuilds the text
|
||||
;; layout the caret rect reads, then retry for a while: right after
|
||||
;; mount the rect can stay unavailable until the first full layout.
|
||||
;; Skipped when a composition began in the meantime — the surface
|
||||
;; must not move while the IME owns it.
|
||||
;; Wait two frames for the pending WASM render to rebuild the layout
|
||||
;; the caret rect reads, then retry: after mount the rect can stay
|
||||
;; unavailable until the first full layout. Skip while composing.
|
||||
(letfn [(attempt [tries]
|
||||
(when-not (mf/ref-val composing-ref)
|
||||
(if-let [rect (update-ime-caret!
|
||||
@ -442,9 +426,9 @@
|
||||
schedule-ime-follow!
|
||||
(mf/use-fn
|
||||
(fn []
|
||||
;; Same frame wait and retries as `schedule-ime-caret!`: the preview
|
||||
;; update clears the text layout the caret rect reads until the
|
||||
;; pending render rebuilds it. Only applies mid-composition.
|
||||
;; Same wait and retries as `schedule-ime-caret!`: a preview update
|
||||
;; clears the layout the caret rect reads until the next render.
|
||||
;; Runs only mid-composition.
|
||||
(letfn [(attempt [tries]
|
||||
(when (and (mf/ref-val composing-ref)
|
||||
(nil? (follow-ime-caret!
|
||||
@ -456,10 +440,9 @@
|
||||
(fn []
|
||||
(js/requestAnimationFrame #(attempt 30)))))))
|
||||
|
||||
;; Moves the WASM caret inside the preview and the IME window with it.
|
||||
;; Synchronous: the browser reports the caret bounds to the IME right
|
||||
;; after the composition/selection update that triggered this, so a
|
||||
;; deferred move would only reach the IME on its next update.
|
||||
;; Moves the WASM caret inside the preview, and the IME window with it.
|
||||
;; Synchronous: the browser reports caret bounds to the IME right after
|
||||
;; the update that triggered this.
|
||||
place-composition-caret!
|
||||
(mf/use-fn
|
||||
(fn [offset]
|
||||
@ -467,10 +450,9 @@
|
||||
(when (nil? (follow-ime-caret! (mf/ref-val wrapper-ref) (mf/ref-val anchor-ref)))
|
||||
(schedule-ime-follow!))))
|
||||
|
||||
;; When only the IME cursor moves (switching clauses), the browser keeps
|
||||
;; the DOM selection on it: mirror it on the WASM caret. The first read
|
||||
;; after a text change is just the baseline; text changes place the
|
||||
;; caret themselves (see on-composition-update).
|
||||
;; Mirrors the IME cursor (the DOM selection) on the WASM caret when only
|
||||
;; the cursor moves, e.g. switching clauses. The first read after a text
|
||||
;; change only sets the baseline (see on-composition-update).
|
||||
sync-composition-cursor!
|
||||
(mf/use-fn
|
||||
(fn []
|
||||
@ -487,13 +469,9 @@
|
||||
(place-composition-caret! cursor)
|
||||
(wasm.api/render-text-editor-overlay!)))))))
|
||||
|
||||
;; Programmatic .focus() on the surface does not reliably fire
|
||||
;; focus/focusin in Chromium (contenteditable inside an SVG
|
||||
;; foreignObject): the node becomes document.activeElement but React's
|
||||
;; on-focus never dispatches, leaving the WASM editor unfocused and
|
||||
;; every caret-rect read null. So wherever we focus programmatically,
|
||||
;; the WASM focus is established explicitly instead of relying on the
|
||||
;; focus event.
|
||||
;; In Chromium, .focus() on a contenteditable inside an SVG foreignObject
|
||||
;; does not reliably fire focus/focusin, so React's on-focus never runs
|
||||
;; and every caret-rect read is null. This sets the WASM focus itself.
|
||||
focus-editor!
|
||||
(mf/use-fn
|
||||
(mf/deps shape-id)
|
||||
@ -526,22 +504,19 @@
|
||||
(mf/use-fn
|
||||
(mf/deps shape-id)
|
||||
(fn [event]
|
||||
;; IME cancel (e.g. Escape on Linux ibus-mozc) fires compositionupdate
|
||||
;; with an empty string; that must reach WASM to clear the preview text.
|
||||
;; IME cancel (e.g. Escape in ibus-mozc) fires compositionupdate with
|
||||
;; an empty string; WASM needs it to clear the preview.
|
||||
;;
|
||||
;; While the composition is in flight the browser owns the capture
|
||||
;; surface, so this handler must not touch it: no clearing, no style
|
||||
;; writes, and no store dispatch. A store sync re-renders this
|
||||
;; component (shape content/name/dimensions, foreignObject
|
||||
;; attributes, CSS vars), and any such churn makes a real IME commit
|
||||
;; the pending kana and restart (typing "ni" commits ん then い
|
||||
;; instead of composing に). The WASM editor keeps the preview in its
|
||||
;; own state and paints it via the render request; the store is
|
||||
;; synced once on compositionend. Only the surface wrapper moves, so
|
||||
;; the IME window follows the WASM caret (wrapping, candidates).
|
||||
;; The browser owns the capture surface mid-composition, so this
|
||||
;; handler must not touch it: no clearing, no style writes, no store
|
||||
;; dispatch. A store sync re-renders this component, and that makes
|
||||
;; a real IME commit the pending kana and restart (typing "ni"
|
||||
;; commits ん then い, not に). WASM keeps the preview in its own
|
||||
;; state; the store syncs on compositionend. Only the surface wrapper
|
||||
;; moves, so the IME window follows the WASM caret.
|
||||
;;
|
||||
;; The caret goes to the end of the part that changed: a typed kana,
|
||||
;; a converted word, a picked candidate (even in a middle clause).
|
||||
;; The caret goes to the end of the changed part: a typed kana, a
|
||||
;; converted word, or a candidate picked in any clause.
|
||||
(let [data (.-data event)]
|
||||
(when (some? data)
|
||||
(let [previous (mf/ref-val composition-text-ref)]
|
||||
@ -615,11 +590,10 @@
|
||||
on-key-down
|
||||
(mf/use-fn
|
||||
(fn [^js event]
|
||||
;; IME keydowns (keyCode 229) must not touch the surface at all: by
|
||||
;; the time the 229 reaches the DOM the IME already owns the surface,
|
||||
;; and even a pre-compositionstart style write makes ibus abort and
|
||||
;; commit. The surface is already sitting on the caret from the last
|
||||
;; idle repositioning (see `update-ime-caret!`).
|
||||
;; IME keydowns (keyCode 229) must not touch the surface: the IME
|
||||
;; already owns it, and even a style write before compositionstart
|
||||
;; makes ibus abort and commit. The surface already sits on the
|
||||
;; caret (see `update-ime-caret!`).
|
||||
(when (and (text-editor/text-editor-has-focus?)
|
||||
(not (composing-event? event)))
|
||||
(let [key (.-key event)
|
||||
@ -718,9 +692,9 @@
|
||||
|
||||
;; Let contenteditable handle text input via on-input
|
||||
:else nil)
|
||||
;; Any handled key may have moved the caret; keep the surface on
|
||||
;; it so the next IME sequence anchors correctly. Plain character
|
||||
;; keys reschedule again from on-input after the insert.
|
||||
;; A handled key may move the caret; keep the surface on it for
|
||||
;; the next IME sequence. Character keys also reschedule from
|
||||
;; on-input after the insert.
|
||||
(schedule-ime-caret!)))))
|
||||
|
||||
;; Native `beforeinput` listener (see the use-effect that registers it).
|
||||
@ -786,10 +760,9 @@
|
||||
on-pointer-down
|
||||
(mf/use-fn
|
||||
(fn [^js event]
|
||||
;; The capture surface is pointer-events:none (it sits on the caret,
|
||||
;; not under the pointer), so it must be focused programmatically.
|
||||
;; preventDefault stops the browser from moving focus to the body on
|
||||
;; mousedown, which would blur the surface right back.
|
||||
;; The capture surface is pointer-events:none, so focus it here.
|
||||
;; preventDefault stops mousedown from moving focus to the body,
|
||||
;; which would blur the surface.
|
||||
(dom/prevent-default event)
|
||||
(focus-editor!)
|
||||
(when-not (secondary-button? event)
|
||||
@ -1019,8 +992,7 @@
|
||||
:on-pointer-move on-pointer-move
|
||||
:on-pointer-up on-pointer-up
|
||||
:on-context-menu on-context-menu
|
||||
;; The hover cursor lives here: the capture surface below is
|
||||
;; pointer-events:none so it never receives hover itself.
|
||||
;; The hover cursor lives here: the capture surface is pointer-events:none.
|
||||
:class (dm/str (cur/get-text (:rotation shape) vertical?)
|
||||
" "
|
||||
(stl/css :text-editor))
|
||||
@ -1031,10 +1003,7 @@
|
||||
{:ref contenteditable-ref
|
||||
:contentEditable true
|
||||
:suppressContentEditableWarning true
|
||||
;; The surface retains typed text between keystrokes (see
|
||||
;; keep-input-alive), so disable text assistance that would otherwise
|
||||
;; rewrite that retained text and desync the WASM editor.
|
||||
;; NOTE: this was already not working in v1/v2
|
||||
;; Text assistance would rewrite the retained text (see keep-input-alive).
|
||||
:spellCheck false
|
||||
:autoCorrect "off"
|
||||
:autoCapitalize "off"
|
||||
|
||||
@ -50,8 +50,8 @@
|
||||
:dimmed true})))
|
||||
|
||||
(defn japanese-layout-config-enabled?
|
||||
"Japanese layout controls are available under the WASM renderer, when
|
||||
enabled for the current file or globally in the user's profile."
|
||||
"True under the WASM renderer when Japanese layout is enabled for the file
|
||||
or in the user's profile."
|
||||
[file-data profile]
|
||||
(and ^boolean (wm/vertical-layout-active?)
|
||||
(or (ctf/japanese-layout-enabled? file-data)
|
||||
@ -63,7 +63,7 @@
|
||||
(= "vertical-rl" (:writing-mode values)))
|
||||
|
||||
(defn proportional-metrics-feature
|
||||
"Return the proportional metric feature relevant to the writing mode."
|
||||
"Proportional metrics feature for the writing mode: `vpal` or `palt`."
|
||||
[writing-mode]
|
||||
(if (= writing-mode "vertical-rl") "vpal" "palt"))
|
||||
|
||||
@ -127,9 +127,8 @@
|
||||
:label (tr "workspace.options.text-options.text-orientation-upright")
|
||||
:icon i/text-orientation-upright}])
|
||||
|
||||
;; Warichu (割注): a span-scoped toggle that renders the selection as two
|
||||
;; half-size lines within one inline position (top/bottom in horizontal
|
||||
;; flow, right/left in vertical flow).
|
||||
;; Warichu (割注): renders the selection as two half-size lines in one inline
|
||||
;; position (top/bottom horizontally, right/left vertically).
|
||||
(defn- warichu-options
|
||||
[]
|
||||
[{:value "none"
|
||||
@ -142,9 +141,8 @@
|
||||
:icon i/warichu}])
|
||||
|
||||
(mf/defc text-combine-upright-options*
|
||||
;; Digit TCY can be applied across a shape because it discovers eligible
|
||||
;; runs automatically. The unrestricted `all` value is only offered for an
|
||||
;; explicit text range.
|
||||
;; Digit TCY finds eligible runs itself, so it applies to a whole shape;
|
||||
;; `all` is offered only for a text selection.
|
||||
[{:keys [values on-change on-blur text-selection-active]}]
|
||||
(let [text-combine-upright (radio-selected (:text-combine-upright values) "none")
|
||||
digits? (case text-combine-upright
|
||||
@ -176,8 +174,7 @@
|
||||
false)
|
||||
options
|
||||
(mf/with-memo []
|
||||
;; select* matches and reports options by :id, so the id is the
|
||||
;; persisted attr value.
|
||||
;; select* matches options by :id, so the id is the stored value.
|
||||
[{:id "digits2"
|
||||
:label (tr "workspace.options.text-options.text-combine-upright-digits-2")}
|
||||
{:id "digits3"
|
||||
@ -366,9 +363,8 @@
|
||||
[:> ruby-customization-options* common-props]]]))
|
||||
|
||||
(mf/defc font-features-options*
|
||||
;; Japanese proportional metric alternates. `palt` is typically used for
|
||||
;; horizontal composition and `vpal` for vertical composition; the value is
|
||||
;; span-scoped and passed through browser, export, and render-wasm paths.
|
||||
;; Japanese proportional metric alternates, per span: `palt` for
|
||||
;; horizontal text, `vpal` for vertical.
|
||||
[{:keys [values on-change on-blur]}]
|
||||
(let [writing-mode (:writing-mode values)
|
||||
feature (proportional-metrics-feature writing-mode)
|
||||
@ -450,8 +446,8 @@
|
||||
:options (:writing-mode options)})]
|
||||
(when ^boolean vertical?
|
||||
[:*
|
||||
;; The v2 editor can read the paragraph orientation back as an empty
|
||||
;; string when it is unset; that (and nil) selects "mixed".
|
||||
;; An unset orientation reads as nil, or "" in the v2 editor; both
|
||||
;; select "mixed".
|
||||
[:> attr-radio-options* (mf/spread-props common-props
|
||||
{:attr :text-orientation
|
||||
:default-value "mixed"
|
||||
|
||||
@ -573,9 +573,8 @@
|
||||
horizontal-resize?)
|
||||
resize-direction (if horizontal-resize? :horizontal :vertical)]
|
||||
(cond
|
||||
;; Resizing auto-width on the wrap axis switches to auto-height.
|
||||
;; The physical wrap axis is horizontal normally and vertical
|
||||
;; under vertical writing.
|
||||
;; Resizing auto-width along the wrap axis (vertical under
|
||||
;; vertical writing) switches to auto-height.
|
||||
(and (= shape-type :text)
|
||||
(= grow-type :auto-width)
|
||||
wrap-axis-resize?)
|
||||
|
||||
@ -1407,14 +1407,13 @@
|
||||
|
||||
(if fallback-fonts-only? updated-fonts fallback-fonts))))))
|
||||
|
||||
;; Fallback faces a composition is already waiting on, so each preview update
|
||||
;; does not queue another relayout for them.
|
||||
;; Fallback faces a composition waits on, so preview updates do not queue more relayouts.
|
||||
(defonce ^:private composition-pending-faces (atom #{}))
|
||||
|
||||
(defn load-composition-fonts!
|
||||
"Fetch the fallback faces (emoji, Noto, ...) that `text` needs and relayout
|
||||
`shape-id` once they are stored. For text that only lives in WASM, such as an
|
||||
IME composition preview; committed content loads its faces through
|
||||
`shape-id` once they are stored. For text that lives only in WASM, such as
|
||||
an IME composition preview; committed content loads its faces through
|
||||
`set-shape-text-content`."
|
||||
[shape-id text]
|
||||
(let [emoji? (cfnt/contains-emoji? text)
|
||||
@ -3025,8 +3024,7 @@
|
||||
features))
|
||||
;; Each warichu sub-line has its own entry, drawn at half size.
|
||||
:warichu (when (= "warichu" (get element :warichu)) "warichu")
|
||||
;; Horizontal position data has no separate ruby strip, so the base
|
||||
;; entry carries the annotation for static SVG export.
|
||||
;; Horizontal data has no ruby strip; the base entry carries ruby for SVG export.
|
||||
:ruby (when (and (not vertical?) (seq element-text)) ruby)
|
||||
:ruby-size (get element :ruby-size)
|
||||
:ruby-align (get element :ruby-align)
|
||||
|
||||
@ -699,8 +699,8 @@
|
||||
|
||||
(defn- span-japanese-styles
|
||||
"Japanese span attributes with their defaults filled in. Every selection
|
||||
snapshot carries all of them, so moving onto an unstyled span clears the
|
||||
previous span's controls instead of leaving stale values behind."
|
||||
snapshot carries all of them, so moving onto an unstyled span resets the
|
||||
controls."
|
||||
[span]
|
||||
(reduce-kv
|
||||
(fn [styles attr default]
|
||||
@ -717,9 +717,8 @@
|
||||
styles))
|
||||
|
||||
(defn- span-at-offset
|
||||
"Return the span used by the WASM editor for a collapsed caret. At a span
|
||||
boundary the preceding span wins, matching find_text_span_at_offset in
|
||||
render-wasm."
|
||||
"Span the WASM editor uses for a collapsed caret. At a span boundary the
|
||||
preceding span wins, as in find_text_span_at_offset in render-wasm."
|
||||
[paragraph offset]
|
||||
(let [spans (:children paragraph)]
|
||||
(loop [remaining-spans spans
|
||||
@ -747,9 +746,9 @@
|
||||
selected)))
|
||||
|
||||
(defn selection-japanese-styles
|
||||
"Read Japanese span styles for a normalized WASM selection from Penpot's
|
||||
cached content tree. A range spanning different values reports :multiple;
|
||||
a caret reports the style of its current span."
|
||||
"Japanese span styles of a WASM selection, read from the cached content.
|
||||
A range with differing values reports :multiple; a caret reports its
|
||||
span's style."
|
||||
[content selection]
|
||||
(when (and content selection)
|
||||
(let [{:keys [start-para start-offset end-para end-offset]}
|
||||
@ -782,7 +781,7 @@
|
||||
selected-spans)))))
|
||||
|
||||
(defn text-editor-get-current-japanese-styles
|
||||
"Return Japanese span styles for the active WASM editor selection."
|
||||
"Japanese span styles of the active WASM editor selection."
|
||||
[]
|
||||
(when (wasm/ready?)
|
||||
(let [shape-id (text-editor-get-active-shape-id)
|
||||
@ -942,9 +941,9 @@
|
||||
:content new-content})))))
|
||||
|
||||
(defn apply-paragraph-attrs-to-range
|
||||
"Apply paragraph level attrs to the paragraphs `selection` touches; a
|
||||
collapsed caret means just the one it sits in. Whole-shape attrs (writing
|
||||
mode, orientation) go to every paragraph instead, a nil value removing them."
|
||||
"Apply paragraph attrs to the paragraphs `selection` touches (a caret
|
||||
touches one). Whole-shape attrs (writing mode, orientation) go to every
|
||||
paragraph; a nil value removes them."
|
||||
[content selection attrs]
|
||||
(let [{:keys [start-para end-para]} (normalize-selection selection)
|
||||
whole-attrs (select-keys attrs jl/whole-shape-paragraph-attrs)
|
||||
|
||||
@ -6,8 +6,8 @@
|
||||
|
||||
(ns app.util.text.writing-mode
|
||||
"Writing mode of text content as the active renderer lays it out. Only the
|
||||
WASM renderer supports vertical writing; under the SVG renderer every text
|
||||
flows horizontally, while the stored attrs are kept untouched."
|
||||
WASM renderer supports vertical writing; the SVG renderer lays every text
|
||||
out horizontally and leaves the stored attrs as they are."
|
||||
(:require
|
||||
[app.common.types.shape :as cts]
|
||||
[app.common.types.text.japanese-layout :as jl]))
|
||||
@ -31,8 +31,8 @@
|
||||
|
||||
(defn stale-vertical-layout?
|
||||
"True when the content is stored as vertical but the active renderer lays
|
||||
it out horizontally, so a layout computed by the WASM renderer (such as
|
||||
`:position-data`) no longer matches."
|
||||
it out horizontally, so a WASM-computed layout (such as `:position-data`)
|
||||
does not match."
|
||||
[content]
|
||||
(and (not ^boolean (vertical-layout-active?))
|
||||
(jl/vertical-text-content? content)))
|
||||
|
||||
@ -124,8 +124,8 @@
|
||||
content (selection 0 2 0 3)))))))
|
||||
|
||||
(t/deftest composition-caret-goes-to-the-end-of-the-changed-part
|
||||
;; Typing appends, converting replaces everything, and picking a candidate
|
||||
;; for a middle clause changes only that clause.
|
||||
;; Typing appends, converting replaces all, and a candidate for a middle
|
||||
;; clause changes only that clause.
|
||||
(t/is (= 1 (v3-editor/changed-span-end "" "に")))
|
||||
(t/is (= 2 (v3-editor/changed-span-end "に" "にほ")))
|
||||
(t/is (= 3 (v3-editor/changed-span-end "にほんご" "日本語")))
|
||||
@ -227,13 +227,11 @@
|
||||
(t/is (= #{:japanese} (langs "デザイン"))))
|
||||
|
||||
(t/deftest classification-han-is-ambiguous
|
||||
;; Kanji-only text (han-unification fixture) must NOT classify as a
|
||||
;; concrete language; it is ambiguous Han.
|
||||
;; Kanji-only text is ambiguous Han, not a concrete language.
|
||||
(t/is (= #{:han} (langs "東京都渋谷区神南一丁目"))))
|
||||
|
||||
(t/deftest classification-cjk-punctuation
|
||||
;; CJK punctuation and full-width forms match the shared class
|
||||
;; (previously they matched no range at all).
|
||||
;; CJK punctuation and full-width forms match the shared class.
|
||||
(t/is (= #{:cjk-punctuation} (langs "、。「」『』()")))
|
||||
(t/is (= #{:cjk-punctuation} (langs "!?:;123ABC"))))
|
||||
|
||||
@ -262,8 +260,7 @@
|
||||
(t/is (= #{:chinese} (cfnt/resolve-ambiguous-cjk #{:han} "zh_cn"))))
|
||||
|
||||
(t/deftest resolve-han-only-defaults-to-chinese
|
||||
;; Without kana/hangul and without a CJK locale, keep the previous
|
||||
;; behavior (Noto Sans SC).
|
||||
;; Without kana, hangul or a CJK locale, Han resolves to Chinese (Noto Sans SC).
|
||||
(t/is (= #{:chinese} (cfnt/resolve-ambiguous-cjk #{:han} "en")))
|
||||
(t/is (= #{:chinese} (cfnt/resolve-ambiguous-cjk #{:han} nil))))
|
||||
|
||||
|
||||
62
plugins/libs/plugin-types/index.d.ts
vendored
62
plugins/libs/plugin-types/index.d.ts
vendored
@ -4327,32 +4327,31 @@ export interface Text extends ShapeBase {
|
||||
verticalAlign: 'top' | 'center' | 'bottom' | null;
|
||||
|
||||
/**
|
||||
* The writing mode of the text shape. `horizontal-tb` lays text out in
|
||||
* horizontal lines; `vertical-rl` in vertical columns advancing right-to-left.
|
||||
* Returns 'mixed' if paragraphs use different modes.
|
||||
* The writing mode of the text shape: `horizontal-tb` (horizontal lines) or
|
||||
* `vertical-rl` (vertical columns, right to left). `null` when unset, which
|
||||
* means `horizontal-tb`. Returns 'mixed' if paragraphs use different modes.
|
||||
*/
|
||||
writingMode: 'horizontal-tb' | 'vertical-rl' | 'mixed' | null;
|
||||
|
||||
/**
|
||||
* The orientation of characters in vertical writing. `mixed` rotates
|
||||
* non-CJK runs sideways; `upright` keeps every character upright.
|
||||
* Returns 'mixed' if paragraphs use different orientations.
|
||||
* Character orientation in vertical writing: `mixed` turns non-CJK runs
|
||||
* sideways; `upright` keeps every character upright. `null` when unset,
|
||||
* which means `mixed`. Returns 'mixed' if paragraphs use different values.
|
||||
*/
|
||||
textOrientation: 'mixed' | 'upright' | null;
|
||||
|
||||
/**
|
||||
* Combines the text shape upright in vertical writing. `all` draws the text
|
||||
* as one upright composite; `digits` combines runs of 2-4 consecutive
|
||||
* digits (`digits2`/`digits3` cap the run length at 2/3); `none` uses the
|
||||
* normal vertical layout.
|
||||
* Returns 'mixed' if text spans use different values.
|
||||
* Tate-chu-yoko: sets text upright as one block in vertical writing. `all`
|
||||
* combines the whole span; `digits` combines runs of 2-4 digits
|
||||
* (`digits2`/`digits3` cap the run at 2/3); `none` (the default) turns it
|
||||
* off. Returns 'mixed' if text spans use different values.
|
||||
*/
|
||||
textCombineUpright:
|
||||
'none' | 'all' | 'digits' | 'digits2' | 'digits3' | 'mixed' | null;
|
||||
|
||||
/**
|
||||
* Emphasis marks (圏点 / bouten) drawn beside each base character, mirroring
|
||||
* CSS `text-emphasis-style`. `none` removes them.
|
||||
* Emphasis marks (圏点 / bouten) beside each base character, as in CSS
|
||||
* `text-emphasis-style`. `none` (the default) removes them.
|
||||
* Returns 'mixed' if text spans use different values.
|
||||
*/
|
||||
textEmphasis:
|
||||
@ -4367,45 +4366,44 @@ export interface Text extends ShapeBase {
|
||||
| null;
|
||||
|
||||
/**
|
||||
* Warichu (割注): renders the span as two half-size lines stacked inline
|
||||
* within one column position of the vertical flow. `none` disables it.
|
||||
* Warichu (割注): sets the text as two half-size lines in one line position.
|
||||
* `none` (the default) turns it off.
|
||||
* Returns 'mixed' if text spans use different values.
|
||||
*/
|
||||
warichu: 'none' | 'warichu' | 'mixed' | null;
|
||||
|
||||
/**
|
||||
* OpenType proportional alternate metrics for Japanese text. `palt` applies
|
||||
* proportional alternate widths in horizontal writing; `vpal` applies
|
||||
* proportional alternate widths in vertical writing. `none` disables them.
|
||||
* OpenType proportional metrics for Japanese text: `palt` for horizontal
|
||||
* writing, `vpal` for vertical. `none` (the default) turns them off.
|
||||
* Returns 'mixed' if text spans use different values.
|
||||
*/
|
||||
fontFeatures: 'none' | 'palt' | 'vpal' | 'mixed' | null;
|
||||
|
||||
/**
|
||||
* Controls annotation collision handling. `none` preserves the explicit
|
||||
* line gap; `auto` reserves separate half-em layers for ruby and emphasis.
|
||||
* Room for annotations: `auto` adds a half-em to the line height for each
|
||||
* ruby or emphasis layer; `none` (the default) keeps the set line height.
|
||||
* Returns 'mixed' if text spans use different values.
|
||||
*/
|
||||
annotationClearance: 'none' | 'auto' | 'mixed' | null;
|
||||
|
||||
/**
|
||||
* Ruby (furigana) annotation shown over the base text in vertical writing.
|
||||
* Set a string to annotate the selected span(s), or `null` to remove it.
|
||||
* Ruby (furigana) annotation text for the base text. Set a string to
|
||||
* annotate the whole text, or `null` to remove it.
|
||||
* Returns 'mixed' if text spans carry different ruby values.
|
||||
*/
|
||||
ruby: string | null;
|
||||
|
||||
/** Ruby annotation size relative to its base text. */
|
||||
/** Ruby size relative to the base text: `half` (default), `third` or `quarter`. */
|
||||
rubySize: 'half' | 'third' | 'quarter' | 'mixed' | null;
|
||||
|
||||
/** Distribution of ruby glyphs across the corresponding base text. */
|
||||
/** How ruby glyphs spread over the base text; defaults to `space-around`. */
|
||||
rubyAlign:
|
||||
'space-around' | 'center' | 'start' | 'space-between' | 'mixed' | null;
|
||||
|
||||
/** Whether ruby may extend beyond the corresponding base text. */
|
||||
/** Whether ruby may extend past its base text: `auto` (default) or `none`. */
|
||||
rubyOverhang: 'auto' | 'none' | 'mixed' | null;
|
||||
|
||||
/** Annotation side: above/right (`over`) or below/left (`under`). */
|
||||
/** Ruby side: `over` (above or right, the default) or `under` (below or left). */
|
||||
rubySide: 'over' | 'under' | 'mixed' | null;
|
||||
|
||||
/**
|
||||
@ -4534,8 +4532,8 @@ export interface TextRange {
|
||||
warichu: 'none' | 'warichu' | 'mixed' | null;
|
||||
|
||||
/**
|
||||
* OpenType proportional alternate metrics for Japanese text. It can be a
|
||||
* specific feature or 'mixed' if multiple text spans are used.
|
||||
* OpenType proportional metrics for the range: `palt` (horizontal), `vpal`
|
||||
* (vertical) or `none`. Returns 'mixed' for different span values.
|
||||
*/
|
||||
fontFeatures: 'none' | 'palt' | 'vpal' | 'mixed' | null;
|
||||
|
||||
@ -4551,17 +4549,17 @@ export interface TextRange {
|
||||
*/
|
||||
ruby: string | 'mixed' | null;
|
||||
|
||||
/** Ruby annotation size relative to its base text. */
|
||||
/** Ruby size relative to the base text: `half` (default), `third` or `quarter`. */
|
||||
rubySize: 'half' | 'third' | 'quarter' | 'mixed' | null;
|
||||
|
||||
/** Distribution of ruby glyphs across the corresponding base text. */
|
||||
/** How ruby glyphs spread over the base text; defaults to `space-around`. */
|
||||
rubyAlign:
|
||||
'space-around' | 'center' | 'start' | 'space-between' | 'mixed' | null;
|
||||
|
||||
/** Whether ruby may extend beyond the corresponding base text. */
|
||||
/** Whether ruby may extend past its base text: `auto` (default) or `none`. */
|
||||
rubyOverhang: 'auto' | 'none' | 'mixed' | null;
|
||||
|
||||
/** Annotation side: above/right (`over`) or below/left (`under`). */
|
||||
/** Ruby side: `over` (above or right, the default) or `under` (below or left). */
|
||||
rubySide: 'over' | 'under' | 'mixed' | null;
|
||||
|
||||
/**
|
||||
|
||||
@ -1728,7 +1728,7 @@ impl RenderState {
|
||||
|
||||
// Plain fill (no strokes / parent shadows): reuse cached layout
|
||||
// paragraphs when valid. Skip builder rebuild + Skia layout.
|
||||
// The developer text grid is painted by the full text pass.
|
||||
// The full text pass paints the developer text grid.
|
||||
let can_use_layout_cache = !self.options.is_text_grid_visible()
|
||||
&& !shape.has_visible_strokes()
|
||||
&& parent_shadows.is_none()
|
||||
|
||||
@ -121,10 +121,9 @@ impl FontStore {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Upgrade an already-uploaded family to participate in character
|
||||
/// fallback. Font bytes are cached independently from the role in which a
|
||||
/// face is used, so a family may first arrive as a document font and only
|
||||
/// later be requested as a fallback.
|
||||
/// Upgrade an already-uploaded family to take part in character fallback.
|
||||
/// Font bytes are cached apart from a face's role, so a family may arrive
|
||||
/// as a document font and later be requested as a fallback.
|
||||
pub fn mark_as_fallback(&mut self, family: &FontFamily) {
|
||||
if self.has_family(family, false) {
|
||||
self.fallback_fonts.insert(format!("{}", family));
|
||||
|
||||
@ -124,7 +124,7 @@ impl RenderOptions {
|
||||
}
|
||||
|
||||
/// jlreq-style character-frame grid overlay for Japanese vertical
|
||||
/// text: a developer aid, never part of exported output.
|
||||
/// text: a developer aid, never exported.
|
||||
pub fn is_text_grid_visible(&self) -> bool {
|
||||
self.flags & TEXT_GRID_VISIBLE == TEXT_GRID_VISIBLE
|
||||
}
|
||||
|
||||
@ -17,9 +17,9 @@ use skia_safe::{
|
||||
};
|
||||
|
||||
/// Vertical text: shadows, fill, strokes and the debug grid, all painted from
|
||||
/// one layout. Like every vertical pass, the content is rebound to the
|
||||
/// selrect: stored text bounds describe the measured content and can be
|
||||
/// taller than a fixed shape, whose height is the column-wrap budget.
|
||||
/// one layout. Rebinds the content to the selrect: stored text bounds
|
||||
/// describe the measured content and can be taller than a fixed shape, whose
|
||||
/// height is the column-wrap budget.
|
||||
pub fn render_vertical_text(
|
||||
state: &mut RenderState,
|
||||
shape: &Shape,
|
||||
@ -82,10 +82,10 @@ pub fn render_vertical_text(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Paint a viewport-only grid over SkParagraph horizontal text. Blue outlines
|
||||
/// show line boxes, green boxes show the per-scalar tight rectangles returned
|
||||
/// by SkParagraph, and amber rules show baselines. This mirrors the vertical
|
||||
/// text grid while making horizontal annotation anchors inspectable.
|
||||
/// Paint a viewport-only debug grid over SkParagraph horizontal text: blue
|
||||
/// line boxes, green per-scalar tight rects from SkParagraph, amber
|
||||
/// baselines. Matches the vertical text grid and shows horizontal annotation
|
||||
/// anchors.
|
||||
pub fn paint_horizontal_grid(canvas: &Canvas, shape: &Shape, text_content: &TextContent) {
|
||||
let mut builders = text_content.paragraph_builder_group_from_text(None);
|
||||
let layout = calculate_text_layout_data(shape, text_content, &mut builders, true);
|
||||
@ -682,10 +682,9 @@ fn paint_text_with_emoji_overlay(
|
||||
) {
|
||||
let text_content = shape.get_text_content();
|
||||
|
||||
// Vertical writing renders through the custom vertical pass.
|
||||
// Stored text bounds describe the measured content and can be taller than
|
||||
// a fixed shape. Rebind to the selrect so the shape height remains the
|
||||
// column-wrap budget used by the vertical painter.
|
||||
// Vertical writing paints through the vertical pass. Stored text bounds
|
||||
// describe the measured content and can be taller than a fixed shape, so
|
||||
// rebind to the selrect to keep the shape height as the column-wrap budget.
|
||||
let vertical_text_content = text_content
|
||||
.is_vertical()
|
||||
.then(|| text_content.new_bounds(shape.selrect()));
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
//! Minimal `GPOS` parser for the `vpal` (proportional vertical alternate
|
||||
//! metrics) feature. Vertical layout applies these deltas itself: cells
|
||||
//! flow by `vmtx` advances, so HarfBuzz never gets a chance to apply
|
||||
//! vertical GPOS positioning (SkShaper shapes on a horizontal line).
|
||||
//! metrics) feature. Vertical layout places cells by `vmtx` advances and
|
||||
//! applies these deltas itself: SkShaper shapes on a horizontal line, so
|
||||
//! HarfBuzz never applies vertical GPOS positioning.
|
||||
//!
|
||||
//! Only single-adjustment lookups are read (SinglePos, directly or behind
|
||||
//! an Extension lookup) — `vpal` is metrics-only by design and real fonts
|
||||
//! (Noto CJK, Source Han) encode it exactly this way.
|
||||
//! Reads only single-adjustment lookups (SinglePos, direct or behind an
|
||||
//! Extension lookup): `vpal` is metrics-only, and real fonts (Noto CJK,
|
||||
//! Source Han) encode it this way.
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
@ -67,7 +67,7 @@ fn parse_coverage(data: &[u8], offset: usize) -> Option<Vec<u16>> {
|
||||
|
||||
/// A ValueRecord holds one i16 per set low bit of `value_format`, in bit
|
||||
/// order (xPlacement, yPlacement, xAdvance, yAdvance, then four device
|
||||
/// offsets). Returns the y deltas and consumes nothing else.
|
||||
/// offsets). Returns only the y deltas.
|
||||
fn parse_value_record(data: &[u8], offset: usize, value_format: u16) -> Option<VpalDelta> {
|
||||
let mut delta = VpalDelta::default();
|
||||
let mut o = offset;
|
||||
|
||||
@ -1,10 +1,9 @@
|
||||
//! Shared JLREQ character classes and pair-rule tables.
|
||||
//!
|
||||
//! JLREQ defines thirty layout classes. Classes 20–24 and 28–30 are
|
||||
//! contextual/virtual classes produced by higher-level inline composites;
|
||||
//! [`classify`] handles scalar characters and callers assign those virtual
|
||||
//! classes when constructing reference marks, ruby, grouped numerals,
|
||||
//! warichu, or tate-chu-yoko.
|
||||
//! virtual classes for inline composites: [`classify`] handles single
|
||||
//! characters, and callers assign the virtual classes when building
|
||||
//! reference marks, ruby, grouped numerals, warichu, or tate-chu-yoko.
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
#[repr(u8)]
|
||||
@ -124,9 +123,9 @@ impl JapaneseClass {
|
||||
)
|
||||
}
|
||||
|
||||
/// Half-width punctuation whose normal character frame is completed by
|
||||
/// half an em after the glyph. Consecutive punctuation may suppress that
|
||||
/// appended spacing, but the glyph body itself remains half-width.
|
||||
/// Half-width punctuation followed by a half-em of aki that completes
|
||||
/// its character frame. Consecutive punctuation may drop that aki; the
|
||||
/// glyph body stays half-width.
|
||||
pub const fn is_trailing_aki_punctuation(self) -> bool {
|
||||
matches!(self, Self::ClosingBracket | Self::FullStop | Self::Comma)
|
||||
}
|
||||
@ -141,9 +140,9 @@ pub struct PairRule {
|
||||
/// Largest spacing allowed during oidashi/justification, in em.
|
||||
pub maximum_em: f32,
|
||||
pub break_allowed: bool,
|
||||
/// Whether horizontal SkParagraph needs an inserted WORD JOINER for this
|
||||
/// pair. Atomic Western/numeral runs are already protected by its Unicode
|
||||
/// breaker, so they remain non-breakable without synthetic characters.
|
||||
/// Whether horizontal SkParagraph needs a WORD JOINER to block a break
|
||||
/// in this pair. Its Unicode breaker already keeps Western/numeral runs
|
||||
/// whole.
|
||||
pub suppress_break_with_joiner: bool,
|
||||
/// Lower values are adjusted first; zero means not adjustable.
|
||||
pub shrink_priority: u8,
|
||||
@ -215,15 +214,15 @@ const fn generated_pair_rules() -> [[PairRule; JapaneseClass::COUNT]; JapaneseCl
|
||||
(before, after),
|
||||
(JapaneseClass::TateChuYoko, JapaneseClass::TateChuYoko)
|
||||
) {
|
||||
// Two adjacent cl-30 entries are necessarily separate TCY
|
||||
// composites (characters inside one composite are atomic).
|
||||
// Adjacent cl-30 entries are always separate TCY composites
|
||||
// (characters inside one composite are atomic).
|
||||
rule.maximum_em = 0.25;
|
||||
rule.expand_priority = 3;
|
||||
} else if matches!(before, JapaneseClass::DividingPunctuation)
|
||||
&& !matches!(after, JapaneseClass::ClosingBracket)
|
||||
{
|
||||
// A sentence-ending question/exclamation mark carries one em
|
||||
// after it. Line planning may discard this at the line edge.
|
||||
// after it. Line planning may drop it at the line edge.
|
||||
rule.preferred_em = 1.0;
|
||||
rule.minimum_em = 0.0;
|
||||
rule.maximum_em = 1.0;
|
||||
@ -231,8 +230,8 @@ const fn generated_pair_rules() -> [[PairRule; JapaneseClass::COUNT]; JapaneseCl
|
||||
} else if before.is_trailing_aki_punctuation()
|
||||
&& matches!(after, JapaneseClass::OpeningBracket)
|
||||
{
|
||||
// Only one half-em is retained between the two half-width
|
||||
// glyph bodies, not the sum of both characters' normal aki.
|
||||
// Keep one half-em between the two half-width glyph bodies,
|
||||
// not the sum of both characters' aki.
|
||||
rule.preferred_em = 0.5;
|
||||
rule.minimum_em = 0.0;
|
||||
rule.maximum_em = 0.5;
|
||||
@ -281,8 +280,8 @@ const fn generated_pair_rules() -> [[PairRule; JapaneseClass::COUNT]; JapaneseCl
|
||||
rule.shrink_priority = 3;
|
||||
} else if before.is_japanese_letter() && after.is_japanese_letter() {
|
||||
// Solid Japanese text is the general third-stage expansion
|
||||
// opportunity. The planner may continue past this quarter-em
|
||||
// cap only in JLREQ's final equal-expansion fallback.
|
||||
// point. The planner passes this quarter-em cap only in
|
||||
// JLREQ's final equal-expansion fallback.
|
||||
rule.maximum_em = 0.25;
|
||||
rule.expand_priority = 3;
|
||||
}
|
||||
|
||||
@ -1,26 +1,22 @@
|
||||
//! Japanese line-breaking rules (kinsoku shori).
|
||||
//!
|
||||
//! Skia's default break iterator allows closing punctuation, the
|
||||
//! prolonged sound mark or small kana at a line start, and opening
|
||||
//! brackets at a line end. skia-safe exposes no ICU BreakIterator, so
|
||||
//! the forbidden break opportunities are suppressed by inserting U+2060 WORD
|
||||
//! JOINER into the text handed to skparagraph. The same lossless layout-text
|
||||
//! transform inserts JLREQ quarter-em Japanese/Western boundary space and
|
||||
//! normalizes Western word spaces to one third em.
|
||||
//! Skia's default break iterator allows closing punctuation, the prolonged
|
||||
//! sound mark or small kana at a line start, and opening brackets at a line
|
||||
//! end. skia-safe exposes no ICU BreakIterator, so this module suppresses
|
||||
//! those breaks by inserting U+2060 WORD JOINER into the text handed to
|
||||
//! skparagraph. The same transform inserts the JLREQ quarter-em space at
|
||||
//! Japanese/Western boundaries and sets Western word spaces to one third em.
|
||||
//!
|
||||
//! The inserted joiners shift every UTF-16 offset reported by the laid
|
||||
//! out paragraph (position-data, caret mapping, selection rects). The
|
||||
//! [`OffsetMap`] returned along with the modified texts translates
|
||||
//! between original and joiner-shifted offsets. It is a pure function
|
||||
//! of the span texts, so any consumer can recompute it and stay
|
||||
//! consistent with the builders by construction.
|
||||
//! Inserted characters shift every UTF-16 offset the laid-out paragraph
|
||||
//! reports (position-data, carets, selection rects). [`OffsetMap`]
|
||||
//! translates between original and shifted offsets. It depends only on the
|
||||
//! span texts, so any consumer can recompute it and match the builders.
|
||||
|
||||
/// Zero-width character whose UAX #14 class forbids breaking on either
|
||||
/// side of it.
|
||||
/// Zero-width character; its UAX #14 class forbids a break on either side.
|
||||
pub const WORD_JOINER: char = '\u{2060}';
|
||||
/// Unicode FOUR-PER-EM SPACE, used for the preferred Japanese/Western gap.
|
||||
pub const JAPANESE_WESTERN_SPACE: char = '\u{2005}';
|
||||
/// Unicode THREE-PER-EM SPACE, used for Western word spacing in Japanese text.
|
||||
/// Unicode THREE-PER-EM SPACE, used for Western word spaces.
|
||||
pub const WESTERN_WORD_SPACE: char = '\u{2004}';
|
||||
|
||||
use super::japanese::{classify, pair_rule};
|
||||
@ -37,9 +33,7 @@ pub fn forbidden_at_line_end(c: char) -> bool {
|
||||
/// and layout-text UTF-16 offsets (the text handed to skparagraph).
|
||||
#[derive(Debug, Clone, Default, PartialEq)]
|
||||
pub struct OffsetMap {
|
||||
/// UTF-16 indices of inserted joiners or boundary spaces, ascending and
|
||||
/// expressed in shifted coordinates. Same-length substitutions need no
|
||||
/// entry.
|
||||
/// Ascending shifted UTF-16 indices of inserted (not substituted) chars.
|
||||
inserted: Vec<usize>,
|
||||
}
|
||||
|
||||
@ -49,14 +43,14 @@ impl OffsetMap {
|
||||
self.inserted.is_empty()
|
||||
}
|
||||
|
||||
/// Original offset for a shifted offset. An offset pointing at an inserted
|
||||
/// character resolves to the source boundary where it was inserted.
|
||||
/// Original offset for a shifted offset. An offset on an inserted
|
||||
/// character resolves to the boundary where it was inserted.
|
||||
pub fn to_original(&self, shifted: usize) -> usize {
|
||||
shifted - self.inserted.iter().take_while(|&&p| p < shifted).count()
|
||||
}
|
||||
|
||||
/// Shifted offset for an original offset. A boundary that received a
|
||||
/// layout character resolves after it, so carets avoid synthetic spacing.
|
||||
/// Shifted offset for an original offset. A boundary that received an
|
||||
/// inserted character resolves after it, so carets skip synthetic spacing.
|
||||
pub fn to_shifted(&self, original: usize) -> usize {
|
||||
let mut shifted = original;
|
||||
for &p in &self.inserted {
|
||||
@ -70,14 +64,12 @@ impl OffsetMap {
|
||||
}
|
||||
}
|
||||
|
||||
/// Applies the horizontal Japanese layout-text transform. It inserts WORD
|
||||
/// JOINER wherever a break would violate kinsoku, inserts a quarter-em space at
|
||||
/// Japanese↔Western boundaries, and normalizes breakable ASCII word spaces to
|
||||
/// one third em. Span boundaries are transparent. Returns `None` when the
|
||||
/// paragraph needs no transformation.
|
||||
/// Apply the normal Japanese layout transform while additionally protecting
|
||||
/// annotated base units. `ruby_breaks[span] == Some(boundaries)` means that
|
||||
/// every internal scalar boundary except those UTF-16 offsets is atomic.
|
||||
/// Applies the horizontal Japanese layout-text transform: inserts WORD
|
||||
/// JOINER wherever a break would violate kinsoku, inserts a quarter-em space
|
||||
/// at Japanese↔Western boundaries, and sets breakable ASCII spaces to one
|
||||
/// third em. Span boundaries are transparent. `ruby_breaks[span] ==
|
||||
/// Some(boundaries)` forbids breaks at every internal scalar boundary of that
|
||||
/// span except those UTF-16 offsets. Returns `None` when nothing changes.
|
||||
pub fn apply_to_span_texts_with_ruby_breaks(
|
||||
span_texts: &[String],
|
||||
ruby_breaks: &[Option<Vec<usize>>],
|
||||
@ -218,8 +210,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn no_insertion_at_paragraph_start() {
|
||||
// A leading forbidden-at-start char has no break opportunity
|
||||
// before it; nothing to suppress.
|
||||
// A leading start-forbidden char has no break before it to suppress.
|
||||
let (texts, _) = apply(&["。あ。"]);
|
||||
assert_eq!(texts, vec!["。あ\u{2060}。".to_string()]);
|
||||
}
|
||||
@ -458,10 +449,9 @@ mod tests {
|
||||
// long-paragraph-wrap
|
||||
"国境の長いトンネルを抜けると雪国であった。夜の底が白くなった。信号所に汽車が止まった。向側の座席から娘が立って来て、島村の前のガラス窓を落した。雪の冷気が流れこんだ。",
|
||||
];
|
||||
// Several widths to move the break positions around. Widths
|
||||
// must exceed the longest unbreakable (joined) run, otherwise
|
||||
// skparagraph rightfully falls back to an emergency mid-run
|
||||
// break.
|
||||
// Vary the width to move the breaks. Each width must exceed the
|
||||
// longest joined run, or skparagraph falls back to an emergency
|
||||
// mid-run break.
|
||||
let char_width = measure_width("国");
|
||||
for width in [9.0, 12.0, 16.5, 24.0].map(|n: f32| n * char_width) {
|
||||
for original in fixtures {
|
||||
@ -488,9 +478,9 @@ mod tests {
|
||||
fn joiner_becomes_visible_under_letter_spacing() {
|
||||
// skparagraph applies letter-spacing per cluster, INCLUDING the
|
||||
// zero-width joiner, which would double the tracking at every
|
||||
// suppressed break. This is why callers disable kinsoku for
|
||||
// paragraphs with a non-zero letter-spacing. If this test ever
|
||||
// fails (Skia stops spacing ignorables), that gate can go.
|
||||
// suppressed break, so callers disable kinsoku when letter-spacing
|
||||
// is non-zero. If this test fails (Skia stops spacing ignorables),
|
||||
// drop that gate.
|
||||
let collection = font_collection();
|
||||
let measure = |text: &str| {
|
||||
let mut builder = ParagraphBuilder::new(&ParagraphStyle::default(), collection.clone());
|
||||
|
||||
@ -271,7 +271,7 @@ fn propagate_transform(
|
||||
let width_before = text_content.size.width;
|
||||
let height_before = text_content.size.height;
|
||||
let (new_width, new_height) = if text_content.is_vertical() {
|
||||
// Vertical auto-height fixes the physical height (the
|
||||
// Vertical auto-height keeps the physical height (the
|
||||
// column wrap axis) and grows width as columns are added.
|
||||
if height_changed {
|
||||
let mut clone = text_content.clone();
|
||||
@ -288,8 +288,8 @@ fn propagate_transform(
|
||||
(shape_bounds_after.width(), height_before)
|
||||
};
|
||||
// Reflow only when the grow axis (the WASM-computed
|
||||
// dimension) changes; the wrap axis is driven by the
|
||||
// resize itself.
|
||||
// dimension) changes; the resize itself sets the wrap
|
||||
// axis.
|
||||
let grow_axis_changed = if text_content.is_vertical() {
|
||||
!is_close_to(width_before, new_width)
|
||||
} else {
|
||||
|
||||
@ -558,16 +558,15 @@ impl TextContent {
|
||||
self.size.normalized_line_height
|
||||
}
|
||||
|
||||
/// Writing mode is a whole-shape property: the first paragraph
|
||||
/// decides the flow for all of them.
|
||||
/// Writing mode applies to the whole shape: the first paragraph sets it.
|
||||
pub fn is_vertical(&self) -> bool {
|
||||
self.paragraphs
|
||||
.first()
|
||||
.is_some_and(|p| p.writing_mode() == WritingMode::VerticalRl)
|
||||
}
|
||||
|
||||
/// Vertical writing and horizontal ruby, warichu and emphasis marks are
|
||||
/// painted by the full text pass, not from the cached paint layout.
|
||||
/// Vertical writing and horizontal ruby, warichu and emphasis marks need
|
||||
/// the full text pass, not the cached paint layout.
|
||||
pub fn can_paint_from_layout_cache(&self) -> bool {
|
||||
!self.is_vertical()
|
||||
&& !self
|
||||
@ -659,9 +658,8 @@ impl TextContent {
|
||||
// AutoWidth paragraphs are laid out with f32::MAX, so line metrics
|
||||
// (line.left) reflect alignment within that huge width and are
|
||||
// unusable for tight bounds. Fall back to content_rect.
|
||||
// Vertical writing bounds come from the vertical pass through
|
||||
// content_rect; the skparagraph line metrics below describe the
|
||||
// unused horizontal layout.
|
||||
// Vertical writing takes its bounds from content_rect; the
|
||||
// skparagraph line metrics below describe an unused horizontal layout.
|
||||
if self.grow_type() == GrowType::AutoWidth || self.is_vertical() {
|
||||
return self.content_rect(selrect, valign);
|
||||
}
|
||||
@ -746,7 +744,7 @@ impl TextContent {
|
||||
|
||||
pub fn content_rect(&self, selrect: &Rect, valign: VerticalAlign) -> Rect {
|
||||
// Vertical content anchors to the shape's right edge and always
|
||||
// aligns to the top (vertical-align along columns is deferred).
|
||||
// aligns to the top; vertical-align does not apply.
|
||||
if self.is_vertical() {
|
||||
let (width, height) = if self.grow_type() == GrowType::AutoWidth {
|
||||
(self.size.width, self.size.height)
|
||||
@ -792,8 +790,8 @@ impl TextContent {
|
||||
point: &Point,
|
||||
vertical_align: VerticalAlign,
|
||||
) -> Option<TextPositionWithAffinity> {
|
||||
// Vertical writing: resolve through the vertical pass. The point
|
||||
// arrives selrect-local; the content block is right-anchored.
|
||||
// Vertical writing: resolve through the vertical pass. The point is
|
||||
// selrect-local; the content block is right-anchored.
|
||||
if self.is_vertical() {
|
||||
let bounds = self.bounds();
|
||||
let layout = super::text_vertical::layout_for_box(self, bounds.height());
|
||||
@ -1274,10 +1272,10 @@ impl TextContent {
|
||||
}
|
||||
}
|
||||
|
||||
// Vertical writing sizes come from the vertical pass. Auto-width
|
||||
// fits both axes without wrapping. Auto-height keeps the shape height
|
||||
// as its wrap budget and grows width as columns advance right-to-left.
|
||||
// Fixed keeps both shape dimensions.
|
||||
// Vertical writing takes sizes from the vertical pass. Auto-width
|
||||
// fits both axes without wrapping. Auto-height wraps at the shape
|
||||
// height and grows width as columns advance right-to-left. Fixed
|
||||
// keeps both dimensions.
|
||||
if self.is_vertical() {
|
||||
match self.grow_type() {
|
||||
GrowType::AutoWidth => {
|
||||
@ -1407,8 +1405,8 @@ impl TextContent {
|
||||
|
||||
let result = matrix.map_point((x_pos, y_pos));
|
||||
|
||||
// Vertical writing: hit-test against the laid-out cells directly
|
||||
// (absolute coordinates, right-anchored to the selrect).
|
||||
// Vertical writing: hit-test against the laid-out cells (absolute
|
||||
// coordinates, right-anchored to the selrect).
|
||||
if self.is_vertical() {
|
||||
let layout = super::text_vertical::layout_for_box(self, shape.selrect.height());
|
||||
return super::text_vertical::intersects(
|
||||
@ -1600,12 +1598,12 @@ impl Paragraph {
|
||||
self.text_transform
|
||||
}
|
||||
|
||||
/// Span texts as fed to the paragraph builders: text-transform applied,
|
||||
/// Japanese spacing normalized, and kinsoku break suppressions inserted,
|
||||
/// plus the map between original and builder-text UTF-16 offsets. Every
|
||||
/// consumer of laid-out offsets must translate through the map. The layout
|
||||
/// transform is skipped under letter-spacing, where skparagraph would add
|
||||
/// letter spacing to synthetic layout characters.
|
||||
/// Span texts as fed to the paragraph builders (text-transform applied,
|
||||
/// Japanese spacing normalized, kinsoku break suppressions inserted),
|
||||
/// plus the map from original to builder-text UTF-16 offsets. Consumers
|
||||
/// of laid-out offsets must translate through the map. Skips the layout
|
||||
/// transform under letter-spacing, where skparagraph would also space
|
||||
/// the synthetic characters.
|
||||
pub fn layout_span_texts(&self) -> (Vec<String>, kinsoku::OffsetMap) {
|
||||
layout_span_texts(self)
|
||||
}
|
||||
@ -1648,11 +1646,10 @@ pub fn add_text_with_tabs(builder: &mut ParagraphBuilder, text: &str, font_size:
|
||||
}
|
||||
}
|
||||
|
||||
/// Text after browser filtering and CSS text transformation, plus the source
|
||||
/// UTF-16 range that produced each transformed Unicode scalar. A single source
|
||||
/// scalar can produce several output scalars (for example `ß` uppercases to
|
||||
/// `SS`); keeping that ownership lets vertical layout wrap and export the
|
||||
/// transformed glyphs as one source-text unit.
|
||||
/// Text after browser filtering and CSS text-transform, plus the source
|
||||
/// UTF-16 range behind each output scalar. One source scalar can yield
|
||||
/// several (`ß` uppercases to `SS`); the ranges let vertical layout wrap and
|
||||
/// export those glyphs as one source unit.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct AppliedTextTransform {
|
||||
pub text: String,
|
||||
@ -1757,8 +1754,7 @@ pub struct TextSpan {
|
||||
pub ruby_align: RubyAlign,
|
||||
pub ruby_overhang: RubyOverhang,
|
||||
pub ruby_side: RubySide,
|
||||
/// Warichu (割注): render the span as two half-size lines stacked inline
|
||||
/// within one column position of the vertical flow.
|
||||
/// Warichu (割注): two half-size lines stacked in one column position.
|
||||
pub warichu: bool,
|
||||
pub font_features: FontFeatures,
|
||||
pub annotation_clearance: AnnotationClearance,
|
||||
@ -2165,9 +2161,8 @@ pub fn calculate_text_layout_data(
|
||||
let current_y = para_layout.y;
|
||||
let text_paragraph = text_paragraphs.get(paragraph_index);
|
||||
if let Some(text_para) = text_paragraph {
|
||||
// Ranges are in the builder-text (kinsoku-shifted)
|
||||
// space; exported positions are translated back to
|
||||
// original span-relative offsets through the map.
|
||||
// Ranges are in builder-text (kinsoku-shifted) space; the
|
||||
// map translates exported positions back to span offsets.
|
||||
let offsets = HorizontalOffsets::new(text_para);
|
||||
let offset_map = &offsets.offset_map;
|
||||
let entry =
|
||||
|
||||
@ -47,9 +47,9 @@ pub(crate) fn layout_span_texts(paragraph: &Paragraph) -> (Vec<String>, kinsoku:
|
||||
(texts, kinsoku::OffsetMap::default())
|
||||
}
|
||||
|
||||
/// Add a span to a horizontal paragraph builder. Warichu is represented by a
|
||||
/// single inline placeholder so the two annotation lines wrap as one unit.
|
||||
/// The actual glyphs are painted after SkParagraph has positioned the box.
|
||||
/// Add a span to a horizontal paragraph builder. A warichu span becomes one
|
||||
/// inline placeholder so its two lines wrap as a unit; its glyphs are painted
|
||||
/// after layout.
|
||||
pub(crate) fn add_horizontal_span(
|
||||
builder: &mut ParagraphBuilder,
|
||||
span: &TextSpan,
|
||||
@ -83,10 +83,9 @@ pub(crate) fn add_horizontal_span(
|
||||
height,
|
||||
));
|
||||
// SkParagraph does not expose a placeholder's TextStyle through line
|
||||
// metrics. A near-zero, inkless NBSP preserves the exact
|
||||
// fill/stroke/shadow style for the custom paint pass; the following
|
||||
// zero-width space restores a legal wrapping boundary after the
|
||||
// atomic placeholder.
|
||||
// metrics. A near-zero, inkless NBSP carries the span style for the
|
||||
// paint pass; the zero-width space after it allows a line break after
|
||||
// the atomic placeholder.
|
||||
let mut anchor_style = text_style.clone();
|
||||
anchor_style.set_font_size(0.01);
|
||||
anchor_style.set_height(0.01);
|
||||
@ -112,10 +111,9 @@ pub(crate) struct HorizontalSpanRange {
|
||||
pub style_anchor_start: usize,
|
||||
}
|
||||
|
||||
/// Offset mapping of one horizontally laid-out paragraph between source
|
||||
/// characters, the kinsoku-adjusted layout text and the paragraph builder
|
||||
/// text. Building it runs the kinsoku pass once; reuse it for every lookup
|
||||
/// into the same paragraph.
|
||||
/// Offset maps of one horizontal paragraph between source characters, the
|
||||
/// kinsoku-adjusted layout text and the builder text. Building it runs the
|
||||
/// kinsoku pass; reuse it for every lookup in the paragraph.
|
||||
pub(crate) struct HorizontalOffsets {
|
||||
/// Map between the transformed text and the kinsoku-adjusted text.
|
||||
pub(crate) offset_map: kinsoku::OffsetMap,
|
||||
@ -232,9 +230,9 @@ pub(crate) fn horizontal_builder_to_source(paragraph: &Paragraph, builder_offset
|
||||
HorizontalOffsets::new(paragraph).builder_to_source(builder_offset)
|
||||
}
|
||||
|
||||
/// Ranges shared by layout, position-data and editor mapping. `shifted_*`
|
||||
/// addresses the normal kinsoku-adjusted paragraph text, while `builder_*`
|
||||
/// addresses the paragraph where a whole warichu span occupies one U+FFFC.
|
||||
/// Ranges shared by layout, position data and editor mapping. `shifted_*`
|
||||
/// addresses the kinsoku-adjusted text; `builder_*` addresses the builder
|
||||
/// text, where each warichu span collapses to one placeholder.
|
||||
fn span_ranges(
|
||||
paragraph: &Paragraph,
|
||||
span_texts: Vec<String>,
|
||||
@ -446,11 +444,9 @@ pub(crate) fn horizontal_warichu_range_rects(
|
||||
}
|
||||
|
||||
/// Char index where a warichu run splits into its two sub-lines: the
|
||||
/// balanced midpoint (first line longer), nudged so the second sub-line
|
||||
/// does not start with a line-start-prohibited character and the first
|
||||
/// does not end with a line-end-prohibited one. Nudging forward pulls the
|
||||
/// offending mark up into the first sub-line (jlreq); backward is the
|
||||
/// fallback, and the midpoint stands when no split satisfies kinsoku.
|
||||
/// midpoint (first line longer), moved to the nearest split that keeps
|
||||
/// kinsoku. At equal distance a forward move wins, pulling the mark up into
|
||||
/// the first sub-line (jlreq). Falls back to the midpoint.
|
||||
pub(crate) fn warichu_split_chars(text: &str) -> usize {
|
||||
let chars: Vec<char> = text.chars().collect();
|
||||
let n = chars.len();
|
||||
@ -551,9 +547,8 @@ pub(crate) struct HorizontalEmphasisPlacement {
|
||||
pub(crate) rect: skia::Rect,
|
||||
}
|
||||
|
||||
/// Locate one horizontal emphasis mark above each eligible source character.
|
||||
/// SkParagraph owns wrapping and bidi placement; querying each transformed
|
||||
/// character range keeps the marks attached to the actual laid-out glyphs.
|
||||
/// Place one emphasis mark above each eligible character, from the laid-out
|
||||
/// rect of each character so marks follow SkParagraph's wrapping and bidi.
|
||||
pub(crate) fn horizontal_emphasis_placements(
|
||||
paragraph: &Paragraph,
|
||||
offsets: &HorizontalOffsets,
|
||||
@ -622,13 +617,10 @@ pub(crate) fn horizontal_span_style(
|
||||
})
|
||||
}
|
||||
|
||||
/// Top of the horizontal base em inside SkParagraph's typographic rectangle.
|
||||
/// Its tight rect can include substantial ascender-side padding; anchoring an
|
||||
/// over annotation to that rect's top therefore leaves a visible gap above CJK
|
||||
/// ink. The em is bottom-aligned to the rect, matching the baseline model used
|
||||
/// by the horizontal painter. The ruby showcase needs a 12 px clearance at
|
||||
/// 56 px: the original 6 px adjustment still left it 6 px too close to the
|
||||
/// kanji. Keep the 3/14-em value proportional at other text sizes.
|
||||
/// Top of the horizontal base em inside SkParagraph's typographic rect, less
|
||||
/// a 3/14-em clearance (12 px at 56 px). The tight rect includes ascender-side
|
||||
/// padding, so anchoring an over annotation to its top leaves a gap above CJK
|
||||
/// ink; the em is bottom-aligned to the rect.
|
||||
pub(crate) fn horizontal_annotation_over_top(rect: skia::Rect, font_size: f32) -> f32 {
|
||||
let font_size = font_size.max(0.0);
|
||||
rect.top.max(rect.bottom - font_size) - font_size * (3.0 / 14.0)
|
||||
@ -652,8 +644,7 @@ pub(crate) fn horizontal_emphasis_mark_box(
|
||||
}
|
||||
|
||||
/// Paint horizontal emphasis marks (圏点 / bouten) above their base glyphs.
|
||||
/// The base paragraph retains its normal metrics; interlinear collision and
|
||||
/// automatic line-gap expansion remain a separate layout policy.
|
||||
/// Draw-only: the base paragraph keeps its own metrics.
|
||||
pub(crate) fn paint_horizontal_emphasis(
|
||||
canvas: &skia::Canvas,
|
||||
paragraph: &Paragraph,
|
||||
@ -747,9 +738,9 @@ fn next_horizontal_ruby_range(
|
||||
offset_map.to_shifted(start)..offset_map.to_shifted(*utf16_cursor)
|
||||
}
|
||||
|
||||
/// Baseline adjustment that attaches a glyph's visible ink edge to its base
|
||||
/// strip. Font-wide ascender/descender metrics include leading which makes
|
||||
/// horizontal ruby visibly detached for many Japanese faces.
|
||||
/// Ink edge of `glyph` (bottom when `over`, else top), used to attach ruby
|
||||
/// ink to its base. Font-wide ascent/descent include leading, which leaves
|
||||
/// ruby detached in many Japanese faces.
|
||||
fn horizontal_ruby_ink_edge(font: &Font, glyph: GlyphId, fallback: f32, over: bool) -> f32 {
|
||||
let mut bounds = [skia::Rect::default()];
|
||||
font.get_bounds(&[glyph], &mut bounds, None);
|
||||
@ -765,13 +756,11 @@ fn horizontal_ruby_ink_edge(font: &Font, glyph: GlyphId, fallback: f32, over: bo
|
||||
}
|
||||
}
|
||||
|
||||
/// Paint ruby annotations for one horizontally laid-out paragraph. Draw-only:
|
||||
/// base rects come from the already laid-out skparagraph
|
||||
/// (`get_rects_for_range`), the annotation is shaped at half the span size
|
||||
/// and distributed over each line's base rect with the same jlreq
|
||||
/// distribution the vertical path uses (`distribute_ruby_tops` along the
|
||||
/// horizontal flow). Lines are not reflowed to reserve an annotation band;
|
||||
/// the ruby draws in the natural leading above the base line.
|
||||
/// Paint ruby for one laid-out horizontal paragraph. Draw-only: base rects
|
||||
/// come from `get_rects_for_range`, and the ruby is shaped at
|
||||
/// `ruby_font_size` and spread over each line's rect with the vertical path's
|
||||
/// jlreq distribution (`distribute_ruby_tops`). Lines are not reflowed; ruby
|
||||
/// draws in the line's leading.
|
||||
pub(crate) fn paint_horizontal_ruby(
|
||||
canvas: &Canvas,
|
||||
text_content: &TextContent,
|
||||
@ -844,9 +833,8 @@ pub(crate) fn paint_horizontal_ruby(
|
||||
.map(|(_, _, advance)| *advance)
|
||||
.fold(0.0f32, f32::max)
|
||||
.max(1.0);
|
||||
// Horizontal SkParagraph has already fixed the base geometry.
|
||||
// When overhang is prohibited, fit the annotation strip to
|
||||
// that geometry instead of allowing either end to escape it.
|
||||
// With overhang prohibited, squeeze the ruby to fit the base
|
||||
// rect that SkParagraph has fixed.
|
||||
let glyph_scale = if span.ruby_overhang == RubyOverhang::None {
|
||||
(rect_box.rect.width() / (advance * count as f32)).min(1.0)
|
||||
} else {
|
||||
@ -901,9 +889,9 @@ pub(crate) fn paint_horizontal_ruby(
|
||||
}
|
||||
}
|
||||
|
||||
/// Block flow direction of a paragraph. Horizontal is the skparagraph
|
||||
/// path; vertical-rl lays out columns top->bottom advancing right->left
|
||||
/// through the custom vertical pass.
|
||||
/// Block flow direction of a paragraph. Horizontal uses skparagraph;
|
||||
/// vertical-rl uses the custom vertical pass: columns top to bottom,
|
||||
/// advancing right to left.
|
||||
#[derive(Debug, PartialEq, Clone, Copy, Default)]
|
||||
pub enum WritingMode {
|
||||
#[default]
|
||||
@ -926,13 +914,11 @@ pub enum TextCombineUpright {
|
||||
#[default]
|
||||
None,
|
||||
All,
|
||||
/// Combine runs of 2-4 consecutive ASCII or full-width digits into one upright
|
||||
/// composite; other characters keep the normal vertical layout.
|
||||
/// Runs of 2-4 ASCII or full-width digits combine into one upright cell.
|
||||
Digits,
|
||||
/// Like `Digits` but only runs of exactly 2 digits combine
|
||||
/// (CSS `text-combine-upright: digits 2`).
|
||||
/// Like `Digits`, for runs of exactly 2 (CSS `digits 2`).
|
||||
Digits2,
|
||||
/// Like `Digits` but runs of 2-3 digits combine.
|
||||
/// Like `Digits`, for runs of 2-3.
|
||||
Digits3,
|
||||
}
|
||||
|
||||
@ -991,9 +977,9 @@ pub enum FontFeatures {
|
||||
Vpal,
|
||||
}
|
||||
|
||||
/// Controls whether annotation layers participate in line/column spacing.
|
||||
/// The default preserves legacy documents; `Auto` reserves one half-em for
|
||||
/// each active ruby or emphasis layer.
|
||||
/// Whether ruby and emphasis on the same side stack. `Auto` places emphasis
|
||||
/// outside over-side ruby and reserves room for both; `None` lets them share
|
||||
/// one layer.
|
||||
#[derive(Debug, PartialEq, Clone, Copy, Default)]
|
||||
pub enum AnnotationClearance {
|
||||
#[default]
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
// Ruby (furigana) and emphasis marks (圏点) in vertical flow. Annotations
|
||||
// stay out of `cells`, so base metrics, caret geometry and position data of
|
||||
// the base text never see them; they are placed from the base cells' final
|
||||
// columns and flow extents.
|
||||
// stay out of `cells`, so base metrics, caret geometry and position data
|
||||
// ignore them; they are placed from the base cells' final columns and flow
|
||||
// extents.
|
||||
|
||||
use skia_safe::{self as skia, Font};
|
||||
|
||||
@ -24,23 +24,18 @@ pub struct RubyGlyph {
|
||||
pub utf16_end: usize,
|
||||
}
|
||||
|
||||
/// A ruby annotation placed alongside one column of base characters.
|
||||
/// Painted from `ruby_runs`; kept out of `cells` so base metrics and caret
|
||||
/// geometry are unaffected.
|
||||
/// A ruby annotation beside one column of base characters, painted from
|
||||
/// `ruby_runs`.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RubyCell {
|
||||
/// Ordered glyphs for this base column. Each retains the shaped run that
|
||||
/// supplied its font so fallback boundaries do not drop ruby content.
|
||||
/// Glyphs in order; each keeps its shaped run so fallback fonts survive.
|
||||
pub glyphs: Vec<RubyGlyph>,
|
||||
pub paragraph: usize,
|
||||
pub span: usize,
|
||||
pub column: usize,
|
||||
/// Flow-axis (top, extent) of each base character this ruby annotates,
|
||||
/// in flow order, restricted to the annotated column. Group ruby spreads
|
||||
/// the annotation over the union; mono ruby maps ruby glyphs onto the
|
||||
/// individual segments.
|
||||
/// Flow-axis (top, extent) of each annotated base character in the column.
|
||||
pub base_segments: Vec<(f32, f32)>,
|
||||
/// Final flow-axis positions computed from the explicit ruby mapping.
|
||||
/// Flow-axis top of each glyph.
|
||||
pub glyph_tops: Vec<f32>,
|
||||
pub font_size: f32,
|
||||
pub base_font_size: f32,
|
||||
@ -48,10 +43,9 @@ pub struct RubyCell {
|
||||
pub paint: usize,
|
||||
}
|
||||
|
||||
/// One emphasis mark (圏点 / bouten) drawn beside a base character. Kept out of
|
||||
/// `cells` like ruby, so base metrics and caret geometry never see it. The
|
||||
/// mark glyph is the single-glyph `run`; it is centred on its base cell's flow
|
||||
/// extent and drawn in the column's right-side gutter with the cell's paint.
|
||||
/// One emphasis mark (圏点 / bouten) beside a base character: the
|
||||
/// single-glyph `run`, centred on the base cell's flow extent in the column's
|
||||
/// right-side gutter.
|
||||
pub struct EmphasisMark {
|
||||
pub run: usize,
|
||||
/// Index of the annotated base cell in `VerticalLayout::cells`.
|
||||
@ -61,9 +55,9 @@ pub struct EmphasisMark {
|
||||
pub outside_offset: f32,
|
||||
}
|
||||
|
||||
/// Return the proportional item range belonging to a contiguous slice of the
|
||||
/// base text. This keeps a ruby reading monotonic when its base wraps across
|
||||
/// columns while ensuring the final column receives any rounding remainder.
|
||||
/// Item range proportional to a contiguous slice of the base text. Keeps a
|
||||
/// reading monotonic when its base wraps across columns; the final slice gets
|
||||
/// the rounding remainder.
|
||||
fn proportional_range(
|
||||
item_count: usize,
|
||||
base_start: usize,
|
||||
@ -83,18 +77,14 @@ fn proportional_range(
|
||||
start.min(item_count)..end.min(item_count)
|
||||
}
|
||||
|
||||
/// Distribute `count` ruby glyphs of the given `advance` along a single base
|
||||
/// segment `[seg_top, seg_top + seg_extent)`, returning each glyph's flow-axis
|
||||
/// top. Two jlreq regimes:
|
||||
/// Flow-axis top of each of `count` ruby glyphs of `advance` along the base
|
||||
/// segment `[seg_top, seg_top + seg_extent)`. Per jlreq:
|
||||
///
|
||||
/// - Ruby no longer than the base (`Lr <= seg_extent`): even distribution
|
||||
/// (均等割り付け). Each glyph gets an equal slot `seg_extent / count` and is
|
||||
/// centred in its slot, which yields equal inter-glyph gaps and half gaps at
|
||||
/// both ends.
|
||||
/// - Ruby longer than the base (`Lr > seg_extent`): the glyphs are packed at
|
||||
/// their own advance and the block is centred on the base, overhanging both
|
||||
/// ends symmetrically (オーバーハング). The per-end overhang is capped at one
|
||||
/// ruby em so a long annotation cannot swallow its neighbours' cells.
|
||||
/// - Ruby that fits the base is placed by `align`; `SpaceAround` is even
|
||||
/// distribution (均等割り付け): equal slots, each glyph centred in its slot.
|
||||
/// - Longer ruby packs at its own advance and overhangs the base start
|
||||
/// (オーバーハング) by half the overflow, capped at one ruby em so it cannot
|
||||
/// cover its neighbours. `RubyOverhang::None` starts it at the base top.
|
||||
pub(crate) fn distribute_ruby_tops(
|
||||
seg_top: f32,
|
||||
seg_extent: f32,
|
||||
@ -140,10 +130,9 @@ pub(crate) fn distribute_ruby_tops(
|
||||
}
|
||||
}
|
||||
|
||||
/// Cross-axis start of a vertical ruby strip. Line height controls the column
|
||||
/// advance (`base_width`), but must not become spacing between an annotation
|
||||
/// and its base glyph. Centre the base em in that advance and attach ruby to
|
||||
/// the em edge; any extra leading remains outside the base+ruby group.
|
||||
/// Cross-axis start of a vertical ruby strip. Ruby attaches to the edge of the
|
||||
/// base em, centred in the column advance (`base_width`), so line height adds
|
||||
/// no gap between ruby and base.
|
||||
pub(super) fn ruby_strip_x(
|
||||
column: &VerticalColumn,
|
||||
ruby_font_size: f32,
|
||||
@ -219,11 +208,10 @@ pub(super) fn ruby_base_units(
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Long ruby with no slack: grow the base span's flow extent *before*
|
||||
/// column planning so the wrap itself makes room (forced spreading). The
|
||||
/// growth becomes inter-character gaps, so only the cells before the last
|
||||
/// one grow. Single-character bases keep the capped-overhang behaviour of
|
||||
/// `spread_ruby_base_cells`, unless overhang is prohibited.
|
||||
/// Grow the flow extent of base cells under long ruby before column planning,
|
||||
/// so wrapping makes room (forced spreading). The growth goes into gaps
|
||||
/// between characters, so the last cell keeps its extent. A single-character
|
||||
/// base grows only when overhang is prohibited; otherwise the ruby overhangs.
|
||||
pub(super) fn grow_ruby_bases(flow: &mut [FlowCell], ruby_units: &[RubyBaseUnit]) {
|
||||
for unit in ruby_units {
|
||||
let indices: Vec<usize> = (0..flow.len())
|
||||
@ -250,12 +238,9 @@ pub(super) fn grow_ruby_bases(flow: &mut [FlowCell], ruby_units: &[RubyBaseUnit]
|
||||
}
|
||||
}
|
||||
|
||||
/// Expand gaps between already-placed base cells for long ruby annotations.
|
||||
///
|
||||
/// This is deliberately post-placement and bounded: it only shifts later base
|
||||
/// cells in the same span/column, and only into slack before the next cell (or
|
||||
/// the column bottom). It avoids re-wrapping columns while making the base span
|
||||
/// long enough for common long compound-word readings when there is room.
|
||||
/// Widen gaps between placed base cells under long ruby. Shifts only later
|
||||
/// cells of the same span and column, and only into slack before the next
|
||||
/// cell or the column bottom, so columns never re-wrap.
|
||||
pub(super) fn spread_ruby_base_cells(
|
||||
cells: &mut [VerticalCell],
|
||||
ruby_units: &[RubyBaseUnit],
|
||||
@ -391,10 +376,9 @@ fn ruby_glyphs(
|
||||
glyphs
|
||||
}
|
||||
|
||||
/// Ruby (furigana) placement. Runs after column placement because a ruby
|
||||
/// annotation's strip is positioned from its base characters' final column
|
||||
/// and flow extent. A reading whose base wraps is partitioned across the
|
||||
/// columns in proportion to their base characters.
|
||||
/// Ruby (furigana) placement. Runs after column placement, since ruby follows
|
||||
/// its base's final column and flow extent. A reading whose base wraps is
|
||||
/// split across columns in proportion to their base characters.
|
||||
pub(super) fn layout_ruby(
|
||||
text_content: &TextContent,
|
||||
cells: &[VerticalCell],
|
||||
@ -478,10 +462,9 @@ pub(super) fn layout_ruby(
|
||||
(ruby_runs, ruby_cells)
|
||||
}
|
||||
|
||||
/// Emphasis marks (圏点 / bouten): one mark glyph per upright base cell of
|
||||
/// every span that carries `text_emphasis`, drawn beside the cell in the
|
||||
/// right-side gutter. The mark is shaped once per span. Whitespace and
|
||||
/// Japanese punctuation cells get no mark (CSS `text-emphasis` behaviour).
|
||||
/// Emphasis marks (圏点 / bouten): one mark per upright base cell of each
|
||||
/// span with `text_emphasis`, shaped once per span. Whitespace and Japanese
|
||||
/// punctuation get no mark, as in CSS `text-emphasis`.
|
||||
pub(super) fn layout_emphasis(
|
||||
text_content: &TextContent,
|
||||
cells: &[VerticalCell],
|
||||
@ -822,9 +805,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn ruby_shorter_than_base_distributes_evenly() {
|
||||
// One base char (extent 100) with 2 ruby glyphs at advance 50: the
|
||||
// combined ruby line (100) equals the base, so glyphs sit in equal
|
||||
// slots of 50, each centred (slot/2 - advance/2 = 0 offset).
|
||||
// A ruby line of 2 x 50 equals the base extent (100): one glyph per
|
||||
// slot, with no offset.
|
||||
let tops = distribute_ruby_tops(
|
||||
0.0,
|
||||
100.0,
|
||||
@ -843,8 +825,8 @@ mod tests {
|
||||
"second ruby glyph one slot down"
|
||||
);
|
||||
|
||||
// A wide base (extent 200) with 2 glyphs of advance 50 must spread out
|
||||
// (slot 100) rather than pack tight at advance 50.
|
||||
// A wide base (extent 200) spreads 2 glyphs of advance 50 into slots
|
||||
// of 100.
|
||||
let spread = distribute_ruby_tops(
|
||||
0.0,
|
||||
200.0,
|
||||
@ -865,10 +847,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn ruby_longer_than_base_overhangs_symmetrically() {
|
||||
// Base extent 40, four ruby glyphs of advance 20 (line 80 > 40): the
|
||||
// block centres on the base and overhangs both ends. Overflow is 40,
|
||||
// per-end overhang 20 (capped at one ruby em = 20), so it starts 20
|
||||
// above the base top.
|
||||
// A ruby line of 80 over a 40 base centres on the base, overhanging
|
||||
// each end by 20 (the one-em cap).
|
||||
let tops = distribute_ruby_tops(
|
||||
0.0,
|
||||
40.0,
|
||||
@ -993,10 +973,9 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn ruby_base_spreading_forces_room_when_no_slack() {
|
||||
// Base 日本 (2 em = 40) with a 6-glyph half-em reading (60) followed
|
||||
// by 語 in a 60 budget: there is no post-placement slack, so the base
|
||||
// extents must grow before planning and push the follower to the next
|
||||
// column instead of falling back to overhang.
|
||||
// Base 日本 (40) with a 60-long reading, then 語, in a 60 column: with
|
||||
// no slack after placement, the base grows before planning and pushes
|
||||
// 語 to the next column.
|
||||
let mut content = make_content_with_spans(&["日本", "語"], 60.0);
|
||||
content.paragraphs_mut()[0].children_mut()[0].ruby = "にほんごです".to_string();
|
||||
let layout = layout_content(&content, 60.0);
|
||||
|
||||
@ -16,8 +16,7 @@ use super::orientation::{
|
||||
};
|
||||
use super::shaping::{shape_segment_with_fallbacks, span_font_families, ShapedRun};
|
||||
|
||||
/// Minimum scale for unconstrained `all` tate-chu-yoko. Counted digit modes
|
||||
/// derive their limit from the eligible run length instead.
|
||||
/// Smallest scale for `all` tate-chu-yoko; digit modes use 1 / run length.
|
||||
const MIN_TCY_SCALE: f32 = 0.5;
|
||||
|
||||
/// Fonts available to the layout: the provider's registered faces and the
|
||||
@ -28,15 +27,13 @@ pub(super) struct Fonts<'a> {
|
||||
pub fallback_families: &'a [String],
|
||||
}
|
||||
|
||||
/// Centered punctuation (jlreq class cl-05 中点類): the middle dot, the
|
||||
/// full-width colon and the full-width semicolon are placed at the centre of
|
||||
/// the em box in vertical writing rather than on the horizontal baseline.
|
||||
/// Centered punctuation (jlreq cl-05 中点類: middle dot, full-width colon and
|
||||
/// semicolon), set at the centre of the em box in vertical writing.
|
||||
fn is_centered_punctuation(c: char) -> bool {
|
||||
classify(c) == JapaneseClass::MiddleDot
|
||||
}
|
||||
|
||||
/// Flow-axis shift that centres a glyph's ink band within its em body: moves
|
||||
/// the ink midpoint (`(ink_top + ink_bottom) / 2`) to the body midpoint.
|
||||
/// Flow-axis shift that centres a glyph's ink band in its em body.
|
||||
fn centered_flow_shift(ink_top: f32, ink_bottom: f32, em_body: f32) -> f32 {
|
||||
em_body / 2.0 - (ink_top + ink_bottom) / 2.0
|
||||
}
|
||||
@ -53,10 +50,9 @@ fn is_tcy_digit(c: char) -> bool {
|
||||
c.is_ascii_digit() || ('0'..='9').contains(&c)
|
||||
}
|
||||
|
||||
/// Split a TCY `digits` span into pieces: maximal ASCII or full-width digit
|
||||
/// runs of 2..=max characters become upright composites, everything else
|
||||
/// keeps the normal vertical layout. Returns (piece text, span-relative
|
||||
/// UTF-16 start, tcy).
|
||||
/// Split a TCY `digits` span into pieces. Maximal ASCII or full-width digit
|
||||
/// runs of 2..=max chars are marked tcy; the rest keep normal vertical
|
||||
/// layout. Returns (piece text, span-relative UTF-16 start, tcy).
|
||||
fn split_digit_runs(text: &str, max: usize) -> Vec<(String, usize, bool)> {
|
||||
let mut pieces: Vec<(String, usize, bool)> = Vec::new();
|
||||
let mut utf16 = 0usize;
|
||||
@ -92,9 +88,7 @@ pub(super) struct SpanCells<'a> {
|
||||
paint: usize,
|
||||
/// UTF-16 offset of the span in its paragraph's layout text.
|
||||
start: usize,
|
||||
/// The span's own font first, then emoji and the registered fallback
|
||||
/// fonts. Shaping resolves glyph coverage explicitly and splits runs at
|
||||
/// typeface boundaries.
|
||||
/// The span's own font, then emoji and the registered fallback fonts.
|
||||
families: Vec<String>,
|
||||
}
|
||||
|
||||
@ -165,10 +159,7 @@ impl<'a> SpanCells<'a> {
|
||||
if self.span.is_warichu() && self.push_warichu(text, runs, cells) {
|
||||
return;
|
||||
}
|
||||
// `digits` combines runs of 2..=max consecutive ASCII or full-width
|
||||
// digits (max 4, or 2/3 for the counted variants) into one upright
|
||||
// composite; the rest of the span flows through the normal
|
||||
// orientation segmentation.
|
||||
// `digits` merges each run of 2..=max digits into one upright cell.
|
||||
let pieces = match combine.digits_max() {
|
||||
Some(max) => split_digit_runs(text, max),
|
||||
None => vec![(text.to_string(), 0, false)],
|
||||
@ -193,11 +184,10 @@ impl<'a> SpanCells<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Compose `piece` (a whole TCY span or a digit run inside one) into one
|
||||
/// upright composite cell, shaping with the first family that covers the
|
||||
/// piece and composing any fallback runs side by side. Returns false when
|
||||
/// the composite would compress below `min_scale`; the caller then falls
|
||||
/// back to the normal vertical segmentation.
|
||||
/// Compose `piece` (a whole TCY span or a digit run in one) into one
|
||||
/// upright composite cell, with fallback runs side by side. Returns false
|
||||
/// when the composite would scale below `min_scale`; the caller then uses
|
||||
/// normal vertical layout.
|
||||
fn push_tate_chu_yoko(
|
||||
&self,
|
||||
piece: &str,
|
||||
@ -206,12 +196,8 @@ impl<'a> SpanCells<'a> {
|
||||
runs: &mut Vec<ShapedRun>,
|
||||
cells: &mut Vec<FlowCell>,
|
||||
) -> bool {
|
||||
// Ruby and other span-level formatting can split an otherwise
|
||||
// continuous vertical run into a one-character span while preserving
|
||||
// an inherited `text-combine-upright: all`. A single CJK character is
|
||||
// already upright; routing it through the horizontal TCY path can
|
||||
// scale it down to fit the font's line metrics and make it smaller
|
||||
// than adjacent base characters.
|
||||
// A lone upright CJK char (e.g. a ruby base span that inherits `all`)
|
||||
// skips TCY, which would shrink it below its neighbours.
|
||||
let mut chars = piece.chars();
|
||||
if matches!((chars.next(), chars.next()), (Some(ch), None) if is_upright_char(ch)) {
|
||||
return false;
|
||||
@ -264,9 +250,9 @@ impl<'a> SpanCells<'a> {
|
||||
}
|
||||
|
||||
/// Warichu (割注): the span becomes one composite cell holding two
|
||||
/// half-size sub-lines laid side by side within the column (the first
|
||||
/// sub-line on the right, jlreq reading order), split by
|
||||
/// `warichu_split_chars`. Returns false when a sub-line shapes empty.
|
||||
/// half-size sub-lines side by side in the column (the first on the
|
||||
/// right, jlreq reading order), split by `warichu_split_chars`. Returns
|
||||
/// false when a sub-line shapes empty.
|
||||
fn push_warichu(
|
||||
&self,
|
||||
text: &str,
|
||||
@ -291,8 +277,7 @@ impl<'a> SpanCells<'a> {
|
||||
};
|
||||
runs.extend(first_runs);
|
||||
runs.extend(second_runs);
|
||||
// Two half-em sub-columns side by side fill the em, the default
|
||||
// `h_advance`.
|
||||
// Two half-em sub-columns fill the em, the default `h_advance`.
|
||||
let end = self.start + text.encode_utf16().count();
|
||||
let cell = self.cell(kind, self.start, end, extent);
|
||||
cells.push(FlowCell::new(
|
||||
@ -317,9 +302,7 @@ impl<'a> SpanCells<'a> {
|
||||
let font_size = self.span.font_size;
|
||||
let letter_spacing = self.span.letter_spacing;
|
||||
let vmetrics = vertical_metrics(&run.font);
|
||||
// vpal deltas apply here in layout, not in the shaper: SkShaper
|
||||
// shapes on a horizontal line, where vertical GPOS positioning never
|
||||
// fires.
|
||||
// vpal applies here: SkShaper's horizontal shaping skips vertical GPOS.
|
||||
let vpal = (self.span.font_features == FontFeatures::Vpal)
|
||||
.then(|| vpal_table(&run.font))
|
||||
.flatten();
|
||||
@ -336,13 +319,7 @@ impl<'a> SpanCells<'a> {
|
||||
advance if advance > 0.0 => advance,
|
||||
_ => font_size,
|
||||
};
|
||||
// Flow down the column by the true vertical advance when the font
|
||||
// has `vmtx`. Without it, fall back to the horizontal advance,
|
||||
// which is exact for full-width CJK. A character that is upright
|
||||
// only because `text-orientation: upright` forced it (e.g. a Latin
|
||||
// letter) is far narrower than an em and would collide with the
|
||||
// following character, so floor it to a full em, matching the
|
||||
// browser's upright behavior.
|
||||
// No `vmtx`: h-advance, with forced-upright Latin floored to an em.
|
||||
let horizontal_fallback = if ch.is_some_and(|c| !is_upright_char(c)) {
|
||||
h_advance.max(font_size)
|
||||
} else {
|
||||
@ -358,10 +335,7 @@ impl<'a> SpanCells<'a> {
|
||||
})
|
||||
.filter(|advance| *advance > 0.0)
|
||||
.unwrap_or(horizontal_fallback);
|
||||
// vpal: the font's advance delta tightens the cell and its
|
||||
// placement delta lifts the drawn ink to keep it inside. The
|
||||
// tightened extent flows into caret, position-data and the aki
|
||||
// sheds (which skip already-half-width cells).
|
||||
// vpal tightens the cell and lifts the ink by the font's deltas.
|
||||
let vpal_delta = vpal
|
||||
.as_ref()
|
||||
.and_then(|table| table.cluster_delta(glyphs, font_size));
|
||||
@ -381,9 +355,7 @@ impl<'a> SpanCells<'a> {
|
||||
let (ink_top, ink_bottom) = ink.unwrap_or((0.0, extent - letter_spacing));
|
||||
let (mut ink_top, mut ink_bottom) =
|
||||
(ink_top + vpal_flow_shift, ink_bottom + vpal_flow_shift);
|
||||
// Centre middle-dot / colon / semicolon ink in the em body; the
|
||||
// shift moves the drawn glyph only. A vpal-covered glyph keeps the
|
||||
// font's own centring instead.
|
||||
// Centre cl-05 ink in the em body, unless vpal already centres it.
|
||||
let glyph_flow_shift = if !synthetic_rotation
|
||||
&& vpal_delta.is_none()
|
||||
&& ch.is_some_and(is_centered_punctuation)
|
||||
@ -423,8 +395,8 @@ impl<'a> SpanCells<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// One cell for a whole sideways Western run. Its glyphs spread down the
|
||||
/// column by letter-spacing (post-rotation +x runs down the column).
|
||||
/// One cell for a whole sideways Western run. Letter-spacing spreads its
|
||||
/// glyphs down the column (post-rotation +x).
|
||||
fn rotated_cell(
|
||||
&self,
|
||||
segment: &Segment,
|
||||
@ -449,8 +421,7 @@ impl<'a> SpanCells<'a> {
|
||||
.ink_bounds()
|
||||
.map_or((0.0, extent - letter_spacing), |ink| (ink.left, ink.right));
|
||||
let cell = VerticalCell {
|
||||
// Rotated runs draw from `run.positions`; centring doesn't read
|
||||
// `h_advance`.
|
||||
// Unused for centring: rotated runs draw from `run.positions`.
|
||||
h_advance: run.advance,
|
||||
ink_top,
|
||||
ink_bottom,
|
||||
@ -493,11 +464,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn upright_narrow_latin_reserves_a_full_em_without_vmtx() {
|
||||
// Under text-orientation: upright, Latin letters are set upright. The
|
||||
// Latin test face carries no `vmtx`, so the flow advance must fall back
|
||||
// to a synthesized em instead of the (much narrower) horizontal advance;
|
||||
// otherwise the letters pack together and collide with the following
|
||||
// character.
|
||||
// Under text-orientation: upright, Latin letters stand upright. The
|
||||
// test face has no `vmtx`, so each letter advances a full em.
|
||||
let em = 20.0;
|
||||
let mut content = make_content(&["ab"], 1000.0);
|
||||
content.paragraphs_mut()[0].children_mut()[0].text_orientation = TextOrientation::Upright;
|
||||
@ -537,8 +505,8 @@ mod tests {
|
||||
panic!("expected a warichu cell");
|
||||
};
|
||||
assert!(first_count >= 1 && run_count > first_count);
|
||||
// Two chars per sub-line at half size: the block's flow extent is
|
||||
// roughly one em, far below the four em of normal layout.
|
||||
// Two half-size chars per sub-line: about one em, not the four em of
|
||||
// normal layout.
|
||||
assert!(
|
||||
cell.extent < 2.0 * 20.0,
|
||||
"two half-size sub-lines take about one em, got {}",
|
||||
@ -627,8 +595,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn tate_chu_yoko_wide_run_falls_back_to_normal_layout() {
|
||||
// A run far wider than the em would compress below MIN_TCY_SCALE; it is
|
||||
// not combined into a squished composite but laid out normally.
|
||||
// A run far wider than the em would scale below MIN_TCY_SCALE, so it
|
||||
// gets normal layout.
|
||||
let mut content = make_content_with_spans(&["123456789"], 400.0);
|
||||
content.paragraphs_mut()[0].children_mut()[0]
|
||||
.set_text_combine_upright(TextCombineUpright::All);
|
||||
@ -749,9 +717,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn letter_spacing_extends_upright_cells() {
|
||||
// Each upright cluster gains `letter_spacing` of flow advance, so
|
||||
// cells stack further apart down the column; the centring width
|
||||
// (`h_advance`) is unaffected.
|
||||
// Each upright cluster gains `letter_spacing` of flow advance; the
|
||||
// centring width (`h_advance`) stays the same.
|
||||
let plain = layout_content(&spaced_content("あい", 0.0), 1000.0);
|
||||
let spaced = layout_content(&spaced_content("あい", 5.0), 1000.0);
|
||||
assert_eq!(plain.cells.len(), spaced.cells.len());
|
||||
@ -807,9 +774,9 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
// A tiny Noto Sans JP subset carrying `vmtx`/`vhea`: U+3031 (〱, the
|
||||
// vertical kana repeat mark) has a 2em vertical advance vs a 1em
|
||||
// horizontal advance; U+3042/U+304F are symmetric controls.
|
||||
// A tiny Noto Sans JP subset with `vmtx`/`vhea`: U+3031 (〱, vertical kana
|
||||
// repeat mark) advances 2em vertically and 1em horizontally;
|
||||
// U+3042/U+304F are symmetric controls.
|
||||
|
||||
#[test]
|
||||
fn vertical_advance_uses_vmtx() {
|
||||
@ -900,9 +867,9 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn vpal_opening_bracket_uses_font_placement() {
|
||||
// In the middle of a line a normal opening bracket keeps its leading
|
||||
// aki. Under vpal its placement still comes from the font's own
|
||||
// YPlacement (481 units), not a synthetic sequence shed.
|
||||
// Mid-line, an opening bracket keeps its leading aki. Under vpal its
|
||||
// placement comes from the font's YPlacement (481 units), not a
|
||||
// synthetic sequence shed.
|
||||
let provider = provider(VPAL_TEST_FONT);
|
||||
let shed = layout_with(&provider, &vpal_content("あ「あ", FontFeatures::None));
|
||||
let vpal = layout_with(&provider, &vpal_content("あ「あ", FontFeatures::Vpal));
|
||||
@ -930,8 +897,7 @@ mod tests {
|
||||
assert!(is_centered_punctuation(':'));
|
||||
assert!(is_centered_punctuation(';'));
|
||||
assert!(!is_centered_punctuation('あ'));
|
||||
// Ink hugging the bottom of the body (14..18 in a 20 body) shifts up so
|
||||
// its midpoint (16) lands on the body midpoint (10): shift == -6.
|
||||
// Ink at 14..18 in a 20 body: midpoint 16 moves to 10, so shift == -6.
|
||||
let shift = centered_flow_shift(14.0, 18.0, 20.0);
|
||||
assert!((shift + 6.0).abs() < 1e-4, "expected -6, got {shift}");
|
||||
// Already-centred ink needs no shift.
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
// Column planning and JLREQ spacing along the vertical flow. The passes run
|
||||
// on a paragraph's `FlowCell`s before they are placed: they adjust cell
|
||||
// extents (aki, oikomi, inter-script spacing) and pick the column breaks
|
||||
// (kinsoku, burasage, oidashi).
|
||||
// Column planning and JLREQ spacing along the vertical flow. These passes run
|
||||
// on a paragraph's `FlowCell`s before placement: they adjust cell extents
|
||||
// (aki, oikomi, inter-script spacing) and pick column breaks (kinsoku,
|
||||
// burasage, oidashi).
|
||||
|
||||
use crate::shapes::japanese::{classify, pair_rule, JapaneseClass};
|
||||
use crate::shapes::kinsoku::{forbidden_at_line_end, forbidden_at_line_start};
|
||||
@ -14,16 +14,14 @@ const INTER_SCRIPT_SPACING_EM: f32 = 0.25;
|
||||
/// Amounts below this are treated as zero by the spacing passes.
|
||||
const EPSILON: f32 = 0.0001;
|
||||
|
||||
/// A placeable item in the vertical flow: its extent along the column and,
|
||||
/// for single-character cells, the character (used for kinsoku decisions
|
||||
/// at column breaks). Rotated runs carry no character and are unsplittable.
|
||||
/// An item placed along the column. `ch` is set for single-character cells
|
||||
/// and drives kinsoku at column breaks; rotated runs have none and never
|
||||
/// split.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub(super) struct FlowItem {
|
||||
pub extent: f32,
|
||||
pub ch: Option<char>,
|
||||
/// This item and its predecessor came from the same source character
|
||||
/// after a length-expanding text transform, so a column break must not
|
||||
/// split them.
|
||||
/// Same source char as the previous item (text transform); keep together.
|
||||
pub keep_with_previous: bool,
|
||||
}
|
||||
|
||||
@ -40,8 +38,7 @@ pub(super) enum FlowScript {
|
||||
/// spacing passes and the column planner need.
|
||||
pub(super) struct FlowCell {
|
||||
pub cell: VerticalCell,
|
||||
/// The character of single-character cells; classifies the cell for aki
|
||||
/// and kinsoku.
|
||||
/// Character of a single-character cell; classifies it for aki and kinsoku.
|
||||
pub ch: Option<char>,
|
||||
pub keep_with_previous: bool,
|
||||
pub script: FlowScript,
|
||||
@ -73,10 +70,10 @@ impl FlowCell {
|
||||
}
|
||||
}
|
||||
|
||||
/// Reduce the cell to a half-em frame (plus its letter-spacing). Opening
|
||||
/// punctuation keeps its ink in the trailing half of the em, so
|
||||
/// `pull_glyph` lifts it into the compressed cell, against the preceding
|
||||
/// character. Returns false when the cell is already that narrow.
|
||||
/// Shrink the cell to a half-em frame plus its letter-spacing. Opening
|
||||
/// punctuation has its ink in the trailing half of the em, so
|
||||
/// `pull_glyph` lifts it into the shrunk cell. Returns false when the
|
||||
/// cell is already that narrow.
|
||||
fn shed_to_half_em(&mut self, pull_glyph: bool) -> bool {
|
||||
let target = 0.5 * self.cell.font_size + self.trailing_spacing;
|
||||
if target >= self.cell.extent {
|
||||
@ -94,10 +91,9 @@ pub(super) fn flow_items(cells: &[FlowCell]) -> Vec<FlowItem> {
|
||||
cells.iter().map(FlowCell::item).collect()
|
||||
}
|
||||
|
||||
/// Punctuation allowed to hang past the column bottom (ぶら下げ / burasage): the
|
||||
/// ideographic and full-width comma and period. When such a mark is the item
|
||||
/// that would overflow the column, it protrudes into the margin instead of
|
||||
/// wrapping (itself and its predecessor) to the next column.
|
||||
/// Punctuation that may hang past the column bottom (ぶら下げ / burasage): the
|
||||
/// ideographic and full-width comma and period. A mark that would overflow
|
||||
/// the column hangs into the margin.
|
||||
fn can_hang(c: char) -> bool {
|
||||
matches!(c, '、' | '。' | ',' | '.')
|
||||
}
|
||||
@ -107,14 +103,13 @@ fn overflows(cursor: f32, item: &FlowItem, max_height: f32) -> bool {
|
||||
cursor > 0.0 && cursor + item.extent > max_height && !item.ch.is_some_and(can_hang)
|
||||
}
|
||||
|
||||
/// First item of the next column when `items[i]` overflows the column that
|
||||
/// starts at `column_start`. Kinsoku: a break may not leave a
|
||||
/// forbidden-at-line-end character at the column bottom nor put a
|
||||
/// forbidden-at-line-start character at the next column top; offending
|
||||
/// predecessors move to the new column (oidashi), bounded so a pathological
|
||||
/// run cannot empty its column. `None` keeps `items[i]` in the column: it
|
||||
/// First item of the next column when `items[i]` overflows the column
|
||||
/// starting at `column_start`. Kinsoku: no forbidden-at-line-end char at the
|
||||
/// column bottom and no forbidden-at-line-start char at the next column top;
|
||||
/// offending predecessors move to the new column (oidashi), at most
|
||||
/// `MAX_KINSOKU_SHIFT` of them. `None` keeps `items[i]` in the column when it
|
||||
/// closes an atomic composite (group ruby or an expanded source scalar) that
|
||||
/// began at the column head, which has no legal internal break.
|
||||
/// began at the column head.
|
||||
fn column_break(
|
||||
items: &[FlowItem],
|
||||
column_start: usize,
|
||||
@ -143,9 +138,8 @@ fn column_break(
|
||||
break_at = skip_kept(break_at - 1);
|
||||
shifted += 1;
|
||||
}
|
||||
// Give up on the shift when the moved items plus the current one would
|
||||
// overflow the new column too: overflowing the wrap budget is worse than
|
||||
// the kinsoku violation.
|
||||
// Drop the shift if it would overflow the new column too: a kinsoku
|
||||
// violation is better than overflowing the wrap budget.
|
||||
let shifted_extent: f32 = items[break_at..i].iter().map(|it| it.extent).sum();
|
||||
if break_at < i && shifted_extent + items[i].extent > max_height {
|
||||
break_at = i;
|
||||
@ -153,12 +147,12 @@ fn column_break(
|
||||
Some(break_at)
|
||||
}
|
||||
|
||||
/// Assign items to columns: returns (column-index, offset-from-top) per
|
||||
/// item. An item that overflows the column height starts a new column (see
|
||||
/// `column_break`); items taller than the column occupy one on their own.
|
||||
/// Burasage marks never overflow: the next non-hanging item still sees the
|
||||
/// overflowed cursor and wraps normally, and oidashi never pulls the hung
|
||||
/// marks because they are forbidden-at-line-start, not -end.
|
||||
/// Assign items to columns, returning (column index, offset from top) per
|
||||
/// item. An item that overflows starts a new column (see `column_break`); an
|
||||
/// item taller than the column gets one to itself. Burasage marks never
|
||||
/// overflow: the next non-hanging item sees the overflowed cursor and wraps,
|
||||
/// and oidashi never pulls a hung mark since it is forbidden-at-line-start,
|
||||
/// not -end.
|
||||
pub(super) fn plan_columns(items: &[FlowItem], max_height: f32) -> Vec<(usize, f32)> {
|
||||
let mut placements: Vec<(usize, f32)> = Vec::with_capacity(items.len());
|
||||
let mut column = 0usize;
|
||||
@ -189,13 +183,11 @@ pub(super) fn is_bounded(max_height: f32) -> bool {
|
||||
max_height.is_finite() && max_height > 0.0 && max_height < f32::MAX
|
||||
}
|
||||
|
||||
/// Offset of a column's content along the column (vertical/inline) axis for
|
||||
/// a given `text-align`. In `vertical-rl` the inline axis runs top->bottom, so
|
||||
/// Left/Start anchor to the top, Right/End to the bottom and Center to the
|
||||
/// middle of the wrap budget. `budget` is the column wrap height (the box
|
||||
/// height); an unbounded budget (auto-width, columns are snug) yields no shift.
|
||||
/// Justify yields no uniform shift here — its space is distributed between
|
||||
/// cells by `ordered_expansion_offsets`.
|
||||
/// Offset of a column's content along the inline axis for `text-align`. In
|
||||
/// `vertical-rl` that axis runs top to bottom: Left/Start anchor to the top,
|
||||
/// Right/End to the bottom, Center to the middle of `budget` (the column wrap
|
||||
/// height). An unbounded budget (auto-width) and Justify yield no shift;
|
||||
/// `ordered_expansion_offsets` spreads justify space between cells.
|
||||
pub(super) fn align_offset_along_column(align: TextAlign, budget: f32, used: f32) -> f32 {
|
||||
if !is_bounded(budget) {
|
||||
return 0.0;
|
||||
@ -208,10 +200,9 @@ pub(super) fn align_offset_along_column(align: TextAlign, budget: f32, used: f32
|
||||
}
|
||||
}
|
||||
|
||||
/// Japanese inter-script spacing at an upright CJK <-> rotated alphabetic or
|
||||
/// numeric boundary. Punctuation and explicit whitespace do not create an
|
||||
/// automatic gap. Use the smaller adjacent font size so a large neighboring
|
||||
/// run cannot create a disproportionate gap.
|
||||
/// Japanese inter-script spacing at an upright CJK <-> rotated alphanumeric
|
||||
/// boundary; punctuation and whitespace get none. Scales by the smaller
|
||||
/// adjacent font size so a large neighbour cannot widen the gap.
|
||||
fn inter_script_spacing(
|
||||
previous: FlowScript,
|
||||
previous_font_size: f32,
|
||||
@ -241,11 +232,10 @@ fn inter_script_spacing(
|
||||
}
|
||||
}
|
||||
|
||||
/// Give every upright <-> Western boundary its inter-script gap. The
|
||||
/// preceding cell's flow advance is adjusted so the *visible ink* edges have
|
||||
/// the target gap: logical advances alone are asymmetric around mixed
|
||||
/// fonts/glyphs (e.g. `うpenあ`) because their side bearings differ. The
|
||||
/// preceding cell's explicit trailing letter-spacing is preserved.
|
||||
/// Give every upright <-> Western boundary its inter-script gap, measured
|
||||
/// between ink edges: side bearings differ across fonts, so advances alone
|
||||
/// would space `うpenあ` unevenly. Adjusts the preceding cell's extent and
|
||||
/// keeps its trailing letter-spacing.
|
||||
pub(super) fn apply_inter_script_spacing(cells: &mut [FlowCell]) {
|
||||
for i in 1..cells.len() {
|
||||
let target_gap = inter_script_spacing(
|
||||
@ -299,12 +289,11 @@ fn embedded_trailing_aki(class: JapaneseClass) -> f32 {
|
||||
}
|
||||
}
|
||||
|
||||
/// JLREQ punctuation and cl-30 adjacency. Full-width fonts bake the half-em
|
||||
/// aki into punctuation advances. That preferred aki stays in ordinary text,
|
||||
/// but one half-em goes at the internal boundaries defined by §3.1.4:
|
||||
/// closing punctuation sequences set solid, closing→opening retains a single
|
||||
/// half-em, opening sequences set solid after the first bracket, and
|
||||
/// middle-dot adjacency retains its own quarter-em side spacing.
|
||||
/// JLREQ punctuation and cl-30 adjacency. Full-width fonts include a half-em
|
||||
/// aki in punctuation advances. Ordinary text keeps it; at the internal
|
||||
/// boundaries of §3.1.4 one half-em goes: closing sequences set solid,
|
||||
/// closing→opening keeps one half-em, opening sequences set solid after the
|
||||
/// first bracket, and middle dots keep their quarter-em sides.
|
||||
pub(super) fn shed_punctuation_aki(cells: &mut [FlowCell], classes: &[Option<JapaneseClass>]) {
|
||||
for (i, flow) in cells.iter_mut().enumerate() {
|
||||
let Some(ch) = flow.ch else {
|
||||
@ -337,10 +326,9 @@ pub(super) fn shed_punctuation_aki(cells: &mut [FlowCell], classes: &[Option<Jap
|
||||
}
|
||||
}
|
||||
|
||||
/// Smallest legal flow extent after oikomi. Full-width Japanese punctuation
|
||||
/// normally carries removable aki inside its one-em advance, but `vpal` may
|
||||
/// have removed that aki already. Clamp the floor to the actual shaped extent
|
||||
/// so a proportional half-em glyph frame can never be compressed again.
|
||||
/// Smallest legal flow extent after oikomi. Full-width punctuation floors at
|
||||
/// a half-em frame, or at its shaped extent when `vpal` has already removed
|
||||
/// the aki.
|
||||
pub(super) fn minimum_oikomi_extent(
|
||||
ch: Option<char>,
|
||||
extent: f32,
|
||||
@ -429,10 +417,9 @@ pub(super) fn preferred_pair_spacing(classes: &[Option<JapaneseClass>]) -> Vec<f
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// cl-04 question/exclamation marks carry an explicit one-em space after
|
||||
/// their character frame. Unlike bracket/comma/full-stop aki, this space is
|
||||
/// not embedded in the font's full-width advance, so it must exist in the
|
||||
/// flow extent before oikomi is allowed to reduce it.
|
||||
/// cl-04 question/exclamation marks take a one-em space after their frame.
|
||||
/// Fonts leave it out of the advance (unlike bracket aki), so this adds it
|
||||
/// to the flow extent, where oikomi can reduce it.
|
||||
pub(super) fn materialize_explicit_pair_spacing(
|
||||
cells: &mut [FlowCell],
|
||||
classes: &[Option<JapaneseClass>],
|
||||
@ -457,8 +444,8 @@ fn spacing_owner(before: JapaneseClass, after: JapaneseClass, boundary: usize) -
|
||||
after,
|
||||
JapaneseClass::OpeningBracket | JapaneseClass::MiddleDot
|
||||
) {
|
||||
// Sequence layout removes any redundant preceding trailing half; the
|
||||
// remaining aki is the next glyph's embedded leading space.
|
||||
// After sequence shedding, the remaining aki is the next glyph's
|
||||
// embedded leading space.
|
||||
(boundary + 1, true)
|
||||
} else {
|
||||
(boundary, false)
|
||||
@ -478,8 +465,8 @@ fn reduce_cell_spacing(cells: &mut [FlowCell], index: usize, amount: f32, leadin
|
||||
amount
|
||||
}
|
||||
|
||||
/// Explicit spacing at a column edge is discarded (no trailing `!` space at
|
||||
/// the column bottom). Returns true when any spacing was removed.
|
||||
/// Discard explicit spacing at column edges (no trailing `!` space at a
|
||||
/// column bottom). Returns true when it removed any.
|
||||
fn discard_explicit_spacing_at_column_edges(
|
||||
cells: &mut [FlowCell],
|
||||
classes: &[Option<JapaneseClass>],
|
||||
@ -509,9 +496,9 @@ fn discard_explicit_spacing_at_column_edges(
|
||||
}
|
||||
|
||||
/// JLREQ oikomi: before wrapping a non-hanging item, try to keep it in the
|
||||
/// current column by reducing only legal aki, in table priority order. If the
|
||||
/// complete deficit cannot be recovered, leave the line untouched for the
|
||||
/// subsequent oidashi/kinsoku planner.
|
||||
/// column by reducing legal aki in table priority order. When the whole
|
||||
/// deficit cannot be recovered, leave the line to the oidashi/kinsoku
|
||||
/// planner.
|
||||
pub(super) fn apply_ordered_oikomi(
|
||||
cells: &mut [FlowCell],
|
||||
classes: &[Option<JapaneseClass>],
|
||||
@ -604,13 +591,12 @@ fn compress_line(
|
||||
removed_from_line
|
||||
}
|
||||
|
||||
/// Plan the columns, then trim spacing that a break left at a column edge
|
||||
/// and re-plan, since a shorter line may pull one more cell into the
|
||||
/// preceding column. Each round only shrinks a previously untrimmed cell, so
|
||||
/// the loops are bounded by the cell count.
|
||||
/// Plan the columns, then trim spacing a break left at a column edge and
|
||||
/// re-plan, since a shorter line may pull another cell into the column. Each
|
||||
/// round shrinks a not-yet-trimmed cell, so the cell count bounds the loops.
|
||||
///
|
||||
/// Wrapped opening brackets use the JIS X 4051 tentsuki policy: their leading
|
||||
/// half-em is discarded at a column head.
|
||||
/// Opening brackets at a column head follow the JIS X 4051 tentsuki policy
|
||||
/// and drop their leading half-em.
|
||||
pub(super) fn plan_with_edge_trimming(
|
||||
cells: &mut [FlowCell],
|
||||
classes: &[Option<JapaneseClass>],
|
||||
@ -643,9 +629,9 @@ pub(super) fn plan_with_edge_trimming(
|
||||
}
|
||||
|
||||
/// Ordered oidashi expansion for justified columns. Stages 1–3 respect the
|
||||
/// table caps; if slack remains, stage 4 distributes it equally across every
|
||||
/// otherwise-expandable boundary, as required by JLREQ §3.8.4. Returns the
|
||||
/// extra flow offset of every cell. The last column is not justified.
|
||||
/// table caps; stage 4 spreads any remaining slack evenly over every
|
||||
/// expandable boundary (JLREQ §3.8.4). Returns each cell's extra flow offset.
|
||||
/// Skips the last column.
|
||||
pub(super) fn ordered_expansion_offsets(
|
||||
cells: &[FlowCell],
|
||||
classes: &[Option<JapaneseClass>],
|
||||
@ -760,10 +746,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn plan_columns_burasage_hangs_comma_period() {
|
||||
// 。 overflows the two-cell column; instead of oidashi (pushing い down
|
||||
// with it), burasage lets it hang past the column bottom. The invariant
|
||||
// "no forbidden-at-line-start char at a column top" still holds — 。
|
||||
// never reaches the next column's top.
|
||||
// 。 overflows the two-cell column; burasage hangs it past the column
|
||||
// bottom, so no oidashi pushes い down and 。 never starts a column.
|
||||
let items = vec![
|
||||
item(10.0, 'あ'),
|
||||
item(10.0, 'い'),
|
||||
@ -800,10 +784,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn plan_columns_kinsoku_shift_never_overflows_budget() {
|
||||
// A two-item budget: moving the trailing 「「 down with the
|
||||
// overflowing い would put three items (30.0) in a 20.0 column;
|
||||
// the shift is dropped instead, keeping every column within the
|
||||
// wrap budget.
|
||||
// A two-item budget: moving 「「 down with the overflowing い would put
|
||||
// three items (30.0) in a 20.0 column, so the planner drops the shift.
|
||||
let items = vec![
|
||||
item(10.0, 'あ'),
|
||||
item(10.0, '「'),
|
||||
@ -914,8 +896,7 @@ mod tests {
|
||||
fn closing_then_opening_keeps_half_em_aki() {
|
||||
// く」「く: the closing bracket sheds its trailing half, but the
|
||||
// opening bracket after it keeps its full em (leading half blank), so
|
||||
// the pair keeps the half-em aki JIS X 4051 asks for instead of
|
||||
// setting solid.
|
||||
// the pair keeps the half-em aki of JIS X 4051.
|
||||
let content = make_content(&["く」「く"], 1000.0);
|
||||
let layout = layout_with(&provider(VMTX_TEST_FONT), &content);
|
||||
assert_eq!(layout.cells.len(), 4, "one cell per character");
|
||||
@ -1122,9 +1103,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn vpal_oikomi_preserves_half_em_punctuation_frame() {
|
||||
// vpal has already removed the opening bracket's leading aki. A tight
|
||||
// fixed-height column must wrap instead of offering that same half-em
|
||||
// to oikomi and collapsing the glyph frame a second time.
|
||||
// vpal has removed the opening bracket's leading aki, so in a tight
|
||||
// fixed-height column oikomi cannot shrink the frame and text wraps.
|
||||
let provider = provider(VPAL_TEST_FONT);
|
||||
let content = vpal_content("あ「あ");
|
||||
let natural = layout_with(&provider, &content);
|
||||
|
||||
@ -12,8 +12,7 @@ use crate::shapes::gpos_vpal::{parse_vpal, VpalDelta};
|
||||
type TableCache<T> = RefCell<HashMap<u32, Option<Rc<T>>>>;
|
||||
|
||||
thread_local! {
|
||||
// Layouts are recomputed on every paint but a face's tables never
|
||||
// change, so each table (whose parse copies it) is read once per face.
|
||||
// Layout runs on every paint; each face's tables are parsed once.
|
||||
static VERTICAL_METRICS: TableCache<VerticalMetrics> = RefCell::default();
|
||||
static VPAL_TABLES: TableCache<VpalTable> = RefCell::default();
|
||||
static TYPO_METRICS: TableCache<TypoMetrics> = RefCell::default();
|
||||
@ -44,10 +43,9 @@ fn table_data(font: &Font, tag: &[u8; 4]) -> Option<skia_safe::Data> {
|
||||
}
|
||||
|
||||
/// Vertical advances from a font's `vhea`/`vmtx` tables. Upright cells
|
||||
/// advance down the column by the glyph's true vertical advance rather
|
||||
/// than its shaped horizontal advance: identical for full-width CJK, but
|
||||
/// correct for vertical alternates and proportional glyphs whose `vmtx`
|
||||
/// differs from `hmtx` (e.g. the vertical kana repeat marks).
|
||||
/// advance by the glyph's vertical advance, not its shaped horizontal one;
|
||||
/// the two differ for vertical alternates and proportional glyphs (e.g. the
|
||||
/// vertical kana repeat marks).
|
||||
pub(super) struct VerticalMetrics {
|
||||
units_per_em: f32,
|
||||
/// Advance heights of the first `advances.len()` glyph ids.
|
||||
@ -57,9 +55,8 @@ pub(super) struct VerticalMetrics {
|
||||
}
|
||||
|
||||
impl VerticalMetrics {
|
||||
/// Parse `vhea`/`vmtx` off the font's typeface. Returns `None` when the
|
||||
/// font carries no vertical metrics (the caller then keeps horizontal
|
||||
/// advances).
|
||||
/// Parses `vhea`/`vmtx`. `None` when the font has no vertical metrics
|
||||
/// (callers keep horizontal advances).
|
||||
pub(super) fn from_font(font: &Font) -> Option<Self> {
|
||||
let units_per_em = units_per_em(font)?;
|
||||
let vhea = table_data(font, b"vhea")?;
|
||||
@ -88,8 +85,8 @@ impl VerticalMetrics {
|
||||
})
|
||||
}
|
||||
|
||||
/// Vertical advance of `glyph` at `font_size`, in pixels at the shaped
|
||||
/// size (same units as the horizontal advances).
|
||||
/// Vertical advance of `glyph` at `font_size`, in pixels (same units as
|
||||
/// the horizontal advances).
|
||||
pub(super) fn advance(&self, glyph: GlyphId, font_size: f32) -> f32 {
|
||||
let raw = self
|
||||
.advances
|
||||
@ -122,10 +119,9 @@ impl VpalTable {
|
||||
}
|
||||
|
||||
/// Pixel deltas for a cluster at `font_size`: summed advance delta
|
||||
/// (negative when the cell tightens) and the flow-axis shift of the
|
||||
/// drawn ink (positive down the column). GPOS `yPlacement` is y-up,
|
||||
/// so its sign flips into flow space. `None` when no glyph of the
|
||||
/// cluster is covered.
|
||||
/// (negative when the cell tightens) and flow-axis ink shift (positive
|
||||
/// down the column; GPOS `yPlacement` is y-up, so its sign flips).
|
||||
/// `None` when no glyph of the cluster is covered.
|
||||
pub(super) fn cluster_delta(&self, glyphs: &[GlyphId], font_size: f32) -> Option<(f32, f32)> {
|
||||
let scale = font_size / self.units_per_em;
|
||||
let mut advance = 0.0f32;
|
||||
@ -144,11 +140,10 @@ pub(super) fn vpal_table(font: &Font) -> Option<Rc<VpalTable>> {
|
||||
cached_table(&VPAL_TABLES, font, VpalTable::from_font)
|
||||
}
|
||||
|
||||
/// OS/2 typographic ascender/descender, normalised to the em (design units /
|
||||
/// unitsPerEm). These bound the ideographic em box and are what the browser
|
||||
/// uses for the vertical central baseline; `hhea`/`Font::metrics` are oversized
|
||||
/// for CJK faces (their ascent exceeds the em) and would push an upright glyph
|
||||
/// off the ideographic centre of its cell.
|
||||
/// OS/2 typographic ascender/descender over unitsPerEm. They bound the
|
||||
/// ideographic em box, which the browser uses for the vertical central
|
||||
/// baseline. `hhea`/`Font::metrics` ascent exceeds the em in CJK faces and
|
||||
/// would push an upright glyph off the centre of its cell.
|
||||
struct TypoMetrics {
|
||||
/// sTypoAscender / unitsPerEm (positive, above the baseline).
|
||||
ascender: f32,
|
||||
@ -161,8 +156,7 @@ impl TypoMetrics {
|
||||
let units_per_em = units_per_em(font)?;
|
||||
let os2 = table_data(font, b"OS/2")?;
|
||||
let os2 = os2.as_bytes();
|
||||
// sTypoAscender @ 68 (i16), sTypoDescender @ 70 (i16); present in every
|
||||
// OS/2 table version.
|
||||
// sTypoAscender @ 68, sTypoDescender @ 70 (i16), in every OS/2 version.
|
||||
let ascender = i16::from_be_bytes([*os2.get(68)?, *os2.get(69)?]) as f32;
|
||||
let descender = i16::from_be_bytes([*os2.get(70)?, *os2.get(71)?]) as f32;
|
||||
Some(Self {
|
||||
@ -173,10 +167,9 @@ impl TypoMetrics {
|
||||
}
|
||||
|
||||
/// Ascent/descent for centring an upright cell, in Skia's sign convention
|
||||
/// (ascent negative, descent positive) at the font's current size. Prefers the
|
||||
/// OS/2 typographic metrics (the ideographic em box, matching the browser's
|
||||
/// vertical central baseline used by the SVG/foreignObject export); falls back
|
||||
/// to `Font::metrics` when the face carries no OS/2 table.
|
||||
/// (ascent negative, descent positive) at the font's size. Uses the OS/2
|
||||
/// typographic metrics, which match the browser's vertical central baseline
|
||||
/// in the SVG/foreignObject export, or `Font::metrics` without an OS/2 table.
|
||||
pub(super) fn upright_centre_metrics(font: &Font) -> (f32, f32) {
|
||||
if let Some(typo) = cached_table(&TYPO_METRICS, font, TypoMetrics::from_font) {
|
||||
let size = font.size();
|
||||
@ -186,11 +179,10 @@ pub(super) fn upright_centre_metrics(font: &Font) -> (f32, f32) {
|
||||
(metrics.ascent, metrics.descent)
|
||||
}
|
||||
|
||||
/// Vertical offset from a cell's top edge to the glyph baseline for an upright
|
||||
/// cell. Centres the glyph's line box within the em cell (as the Tate-chu-yoko
|
||||
/// path does) instead of hanging it from the horizontal ascent: CJK faces have
|
||||
/// an ascent larger than the em, so hanging from it pushes every glyph below
|
||||
/// its cell and the whole column overflows its bounds.
|
||||
/// Offset from an upright cell's top edge to the glyph baseline. Centres the
|
||||
/// glyph's line box in the em cell, as Tate-chu-yoko does. CJK faces have an
|
||||
/// ascent larger than the em, so hanging glyphs from the ascent would push
|
||||
/// them below their cells and overflow the column.
|
||||
pub(super) fn upright_baseline_offset(ascent: f32, descent: f32, font_size: f32) -> f32 {
|
||||
font_size / 2.0 - (ascent + descent) / 2.0
|
||||
}
|
||||
|
||||
@ -33,10 +33,9 @@ pub enum CellKind {
|
||||
glyph: usize,
|
||||
count: usize,
|
||||
},
|
||||
/// A character that participates in upright Japanese flow and kinsoku,
|
||||
/// but whose font has no `vert`/`vrt2` alternate. It is rotated per cell
|
||||
/// instead of becoming a sideways run so wrapping and editor offsets stay
|
||||
/// character-granular.
|
||||
/// An upright-flow character whose font lacks a `vert`/`vrt2` alternate,
|
||||
/// rotated in its own cell so wrapping, kinsoku and editor offsets stay
|
||||
/// per character.
|
||||
SyntheticRotated {
|
||||
run: usize,
|
||||
glyph: usize,
|
||||
@ -46,21 +45,17 @@ pub enum CellKind {
|
||||
run: usize,
|
||||
},
|
||||
TateChuYoko {
|
||||
/// Composite of one or more shaped runs (fallback fonts each add a
|
||||
/// run) laid side by side; `[run_start, run_start + run_count)`.
|
||||
/// Runs `[run_start, run_start + run_count)`, laid side by side.
|
||||
run_start: usize,
|
||||
run_count: usize,
|
||||
scale: f32,
|
||||
},
|
||||
Warichu {
|
||||
/// Two half-size sub-lines stacked side by side within the column:
|
||||
/// runs `[run_start, run_start + first_count)` are the first (right)
|
||||
/// sub-line, the rest up to `run_start + run_count` the second (left).
|
||||
/// First `first_count` runs are the right sub-line, the rest the left.
|
||||
run_start: usize,
|
||||
run_count: usize,
|
||||
first_count: usize,
|
||||
/// UTF-16 length of the first sub-line's text (the split point,
|
||||
/// relative to the cell's `start`).
|
||||
/// UTF-16 length of the first sub-line (split point from `start`).
|
||||
first_chars: usize,
|
||||
},
|
||||
}
|
||||
@ -75,14 +70,11 @@ pub struct VerticalCell {
|
||||
pub end: usize,
|
||||
pub column: usize,
|
||||
pub top: f32,
|
||||
/// Advance along the column (vertical/flow axis) — from `vmtx` when
|
||||
/// available, else the shaped horizontal advance.
|
||||
/// Flow-axis advance, from `vmtx` when present, else the horizontal one.
|
||||
pub extent: f32,
|
||||
/// Lower bound for oikomi reductions. This is the glyph frame after font
|
||||
/// features such as `vpal`, before any removable pair spacing is added.
|
||||
/// Oikomi lower bound: the glyph frame before removable pair spacing.
|
||||
pub minimum_oikomi_extent: f32,
|
||||
/// Shaped horizontal glyph advance, used to centre the glyph on the
|
||||
/// column axis (independent of the vertical flow `extent`).
|
||||
/// Shaped horizontal advance, used to centre the glyph in the column.
|
||||
pub h_advance: f32,
|
||||
/// Visible glyph-ink edges along the flow axis, relative to `top`.
|
||||
pub ink_top: f32,
|
||||
@ -92,10 +84,7 @@ pub struct VerticalCell {
|
||||
pub font_size: f32,
|
||||
/// Span text decoration, painted as vertical bars along the column.
|
||||
pub decoration: Option<TextDecoration>,
|
||||
/// Extra flow-axis (vertical) shift applied to the drawn glyph only, used to
|
||||
/// pull half-width opening punctuation up into its compressed cell so its
|
||||
/// ink hugs the preceding character (jlreq leading aki removal). Zero for
|
||||
/// every other cell; never affects extent, caret, or position-data.
|
||||
/// Draw-only flow shift for half-width opening punctuation (jlreq aki).
|
||||
pub glyph_flow_shift: f32,
|
||||
}
|
||||
|
||||
@ -107,9 +96,7 @@ pub struct VerticalColumn {
|
||||
pub width: f32,
|
||||
/// Reserved annotation gutter before the base band (left / `under`).
|
||||
pub base_offset: f32,
|
||||
/// Line-height-controlled column advance. Base glyphs centre on this band.
|
||||
/// Ruby reserves additional column width but attaches to the centred base
|
||||
/// em, so extra leading does not become base-to-ruby spacing.
|
||||
/// Line-height column advance that base glyphs centre on.
|
||||
pub base_width: f32,
|
||||
}
|
||||
|
||||
@ -125,8 +112,7 @@ pub struct VerticalLayout {
|
||||
/// Shaped ruby annotation runs, indexed by `RubyCell::run`.
|
||||
pub ruby_runs: Vec<ShapedRun>,
|
||||
pub ruby_cells: Vec<RubyCell>,
|
||||
/// Shaped emphasis-mark runs (single glyph each), indexed by
|
||||
/// `EmphasisMark::run`.
|
||||
/// Single-glyph emphasis runs, indexed by `EmphasisMark::run`.
|
||||
pub emphasis_runs: Vec<ShapedRun>,
|
||||
pub emphasis_marks: Vec<EmphasisMark>,
|
||||
/// Per paragraph: [start, end) range into `columns`.
|
||||
@ -137,19 +123,15 @@ pub struct VerticalLayout {
|
||||
pub span_source_utf16_starts: Vec<Vec<usize>>,
|
||||
/// Per paragraph and span: transformed scalar ownership in source text.
|
||||
pub span_transforms: Vec<Vec<AppliedTextTransform>>,
|
||||
/// Per paragraph: UTF-16 offset of every Unicode scalar boundary. Editor
|
||||
/// positions use indices into this table, while cells and position data
|
||||
/// keep their browser-facing UTF-16 offsets.
|
||||
/// Per paragraph: UTF-16 offset of each scalar boundary, for editor positions.
|
||||
pub paragraph_utf16_boundaries: Vec<Vec<usize>>,
|
||||
pub width: f32,
|
||||
pub height: f32,
|
||||
}
|
||||
|
||||
impl VerticalLayout {
|
||||
/// Content origin (top-left of the laid-out block) in the same
|
||||
/// coordinate space as `bounds`. In vertical-rl, block-start is the
|
||||
/// right edge, block-center is the horizontal center and block-end is
|
||||
/// the left edge.
|
||||
/// Content origin (top-left of the laid-out block) in the coordinate
|
||||
/// space of `bounds`.
|
||||
pub fn origin(&self, bounds: &Rect, align: VerticalAlign) -> (f32, f32) {
|
||||
(
|
||||
bounds.left + block_axis_offset(bounds.width(), self.width, align),
|
||||
@ -158,9 +140,9 @@ impl VerticalLayout {
|
||||
}
|
||||
}
|
||||
|
||||
/// Horizontal offset of vertical content within its shape. The existing
|
||||
/// top/center/bottom values describe block-start/center/end; for vertical-rl
|
||||
/// those positions map to right/center/left respectively.
|
||||
/// Horizontal offset of vertical content within its shape. `VerticalAlign`
|
||||
/// top/center/bottom mean block start/center/end: right/center/left in
|
||||
/// vertical-rl.
|
||||
pub fn block_axis_offset(container_width: f32, content_width: f32, align: VerticalAlign) -> f32 {
|
||||
let slack = (container_width - content_width).max(0.0);
|
||||
match align {
|
||||
@ -170,10 +152,8 @@ pub fn block_axis_offset(container_width: f32, content_width: f32, align: Vertic
|
||||
}
|
||||
}
|
||||
|
||||
/// The column-wrap limit for a vertical text content: auto-width shapes
|
||||
/// grow to fit (columns never wrap), everything else wraps at the shape
|
||||
/// height. This is the phase's explicit auto-size decision: auto-height
|
||||
/// behaves like fixed under vertical writing for now.
|
||||
/// Column-wrap limit: auto-width shapes grow to fit (columns never wrap);
|
||||
/// all others, auto-height included, wrap at the shape height.
|
||||
pub fn wrap_height(text_content: &TextContent, height: f32) -> f32 {
|
||||
match text_content.grow_type() {
|
||||
GrowType::AutoWidth => f32::MAX,
|
||||
@ -211,8 +191,8 @@ impl ColumnGeometry {
|
||||
.fold(0.0, f32::max)
|
||||
};
|
||||
let ruby_over_gutter = ruby_gutter(RubySide::Over);
|
||||
// Emphasis occupies the over/right side. Auto-clearance spans carrying
|
||||
// both annotation types stack there; opposite-side ruby stays separate.
|
||||
// Emphasis takes the over (right) side. Auto-clearance spans with both
|
||||
// annotations stack there; under-side ruby stays separate.
|
||||
let emphasis_gutter = if spans.iter().any(|s| !s.text_emphasis.is_none()) {
|
||||
max_font_size * EMPHASIS_FONT_SCALE
|
||||
} else {
|
||||
@ -279,9 +259,9 @@ fn build_paragraph_flow(
|
||||
(flow, span_starts)
|
||||
}
|
||||
|
||||
/// A CSS transform may expand one source character into several shaped
|
||||
/// cells. Keep those cells in one column so a source slice is rendered
|
||||
/// exactly once by the SVG fallback (for example `ß` -> `SS`).
|
||||
/// A CSS transform may expand one source character into several cells
|
||||
/// (`ß` -> `SS`). Keep them in one column so the SVG fallback renders each
|
||||
/// source slice once.
|
||||
fn keep_transform_expansions_together(
|
||||
flow: &mut [FlowCell],
|
||||
transforms: &[AppliedTextTransform],
|
||||
@ -298,10 +278,9 @@ fn keep_transform_expansions_together(
|
||||
}
|
||||
}
|
||||
|
||||
/// Final flow-axis top of every cell: its planned offset, shifted by the
|
||||
/// paragraph's text-align. Justify stretches every column but the last (the
|
||||
/// last "line") to fill the wrap budget; a snug auto-width budget has no
|
||||
/// slack.
|
||||
/// Final flow-axis top of each cell: its planned offset plus the text-align
|
||||
/// shift. Justify stretches every column but the last to fill a bounded wrap
|
||||
/// height.
|
||||
fn aligned_tops(
|
||||
flow: &[FlowCell],
|
||||
classes: &[Option<JapaneseClass>],
|
||||
|
||||
@ -10,25 +10,23 @@
|
||||
// paint / outline export (`paint`) and position data / hit-testing
|
||||
// (`positions`) from the cells.
|
||||
//
|
||||
// Offset discipline: cells use UTF-16 offsets in transformed layout text.
|
||||
// Position data maps those ranges back to the original span text before it
|
||||
// crosses the WASM boundary; the WORD JOINER OffsetMap remains exclusive to
|
||||
// Offsets: cells use UTF-16 offsets in transformed layout text. Position
|
||||
// data maps those ranges back to the original span text before it crosses
|
||||
// the WASM boundary; the WORD JOINER OffsetMap applies only to
|
||||
// skparagraph-driven horizontal breaks.
|
||||
//
|
||||
// Layouts are computed on demand (like the horizontal path, which rebuilds
|
||||
// its skparagraph objects per paint); only per-typeface font tables are
|
||||
// cached.
|
||||
// Layouts are computed on demand, as the horizontal path rebuilds its
|
||||
// skparagraph objects per paint; only per-typeface font tables are cached.
|
||||
//
|
||||
// Text-align aligns each column's glyphs along the vertical (inline) axis:
|
||||
// Left/Start->top, Center->middle, Right/End->bottom of the wrap budget.
|
||||
// Justify stretches every column but the last to fill the wrap budget.
|
||||
//
|
||||
// Letter-spacing adds inter-glyph advance along the column — once per
|
||||
// upright cluster and once per glyph inside a rotated run — mirroring the
|
||||
// horizontal `letter-spacing` that Skia applies to each glyph advance.
|
||||
// Letter-spacing adds advance along the column once per upright cluster
|
||||
// and once per glyph inside a rotated run, matching the horizontal
|
||||
// `letter-spacing` that Skia applies to each glyph advance.
|
||||
//
|
||||
// Deferred to a later phase: PDF/vector emoji overlays, inner shadows and
|
||||
// block-axis vertical-align.
|
||||
// Not supported yet: PDF/vector emoji overlays and inner shadows.
|
||||
|
||||
mod annotations;
|
||||
mod cells;
|
||||
|
||||
@ -1,8 +1,7 @@
|
||||
use crate::shapes::TextOrientation;
|
||||
|
||||
/// Emoji ranges recognized by the frontend font-loader. Emoji use their
|
||||
/// intrinsic upright presentation in vertical flow instead of rotating like
|
||||
/// Latin text under `text-orientation: mixed`.
|
||||
/// Emoji ranges recognized by the frontend font-loader. Emoji stay upright
|
||||
/// in vertical flow; Latin text rotates under `text-orientation: mixed`.
|
||||
pub(super) fn is_emoji_char(c: char) -> bool {
|
||||
matches!(u32::from(c),
|
||||
0x2300..=0x23FF
|
||||
@ -35,12 +34,11 @@ pub(super) fn is_upright_char(c: char) -> bool {
|
||||
)
|
||||
}
|
||||
|
||||
/// Characters whose horizontal glyph needs a vertical alternate. `vert` /
|
||||
/// `vrt2` normally supplies that alternate; when the selected face has no
|
||||
/// such substitution, rotate the horizontal glyph clockwise as a legible
|
||||
/// fallback. The set covers UAX #50 `Tr`, plus comma/full-stop punctuation
|
||||
/// whose untransformed glyph otherwise occupies the wrong half of the
|
||||
/// vertical em box.
|
||||
/// Characters whose horizontal glyph needs a vertical alternate, normally
|
||||
/// from `vert` / `vrt2`. When the face has no such substitution, the
|
||||
/// horizontal glyph rotates clockwise as a fallback. Covers UAX #50 `Tr`,
|
||||
/// plus comma/full-stop punctuation whose plain glyph sits in the wrong
|
||||
/// half of the vertical em box.
|
||||
pub(super) fn uses_rotated_vertical_fallback(c: char) -> bool {
|
||||
matches!(u32::from(c),
|
||||
0x2018..=0x2019 // single quotation marks
|
||||
@ -83,10 +81,10 @@ pub(super) fn segment_by_orientation(text: &str, orientation: TextOrientation) -
|
||||
for c in text.chars() {
|
||||
let upright = orientation == TextOrientation::Upright || is_upright_char(c);
|
||||
let emoji = is_emoji_char(c);
|
||||
// CJK and emoji are both upright, but keep a shaping boundary between
|
||||
// them so the segment probe can explicitly select the emoji family.
|
||||
// The provider's generic fallback iterator does not reliably switch
|
||||
// from a registered CJK face to a registered color-emoji face.
|
||||
// CJK and emoji are both upright, but split them so the segment
|
||||
// probe can select the emoji family: the provider's generic fallback
|
||||
// iterator does not reliably switch from a CJK face to a color-emoji
|
||||
// face.
|
||||
match segments.last_mut() {
|
||||
Some(last)
|
||||
if last.upright == upright
|
||||
|
||||
@ -12,10 +12,9 @@ use super::font_tables::upright_baseline;
|
||||
use super::layout::{column_base_center, layout_for_box, CellKind, VerticalCell, VerticalLayout};
|
||||
use super::shaping::{single_glyph_blob, ShapedRun};
|
||||
|
||||
/// One text blob placed in document space: drawn at `offset` in the local
|
||||
/// space that `transform` maps to the document (identity when `None`).
|
||||
/// Canvas painting and outline export consume the same draws, so both stay
|
||||
/// in sync for every cell kind.
|
||||
/// One text blob drawn at `offset` in the local space that `transform` maps
|
||||
/// to the document (identity when `None`). Canvas painting and outline export
|
||||
/// share these draws, so they match for every cell kind.
|
||||
struct GlyphDraw {
|
||||
blob: TextBlob,
|
||||
offset: SkPoint,
|
||||
@ -155,9 +154,8 @@ fn cell_draws(layout: &VerticalLayout, cell: &VerticalCell, origin: (f32, f32))
|
||||
}
|
||||
}
|
||||
|
||||
/// One warichu sub-line: upright half-size glyphs stacked down the
|
||||
/// sub-column centred on `x_center`, clusters kept together like the normal
|
||||
/// upright path.
|
||||
/// One warichu sub-line: half-size upright glyphs stacked down the
|
||||
/// sub-column centred on `x_center`, one cluster per cell.
|
||||
fn warichu_line_draws(runs: &[ShapedRun], x_center: f32, y_top: f32) -> Vec<GlyphDraw> {
|
||||
let mut draws = Vec::new();
|
||||
let mut cursor = y_top;
|
||||
@ -181,10 +179,9 @@ fn warichu_line_draws(runs: &[ShapedRun], x_center: f32, y_top: f32) -> Vec<Glyp
|
||||
draws
|
||||
}
|
||||
|
||||
/// One column's slice of a ruby annotation stacked upright in its column's
|
||||
/// ruby gutter. Each glyph's flow-axis position comes from the whole base
|
||||
/// span; each glyph is centred across the gutter using its own
|
||||
/// fallback-font run.
|
||||
/// One column's slice of a ruby annotation, stacked upright in the column's
|
||||
/// ruby gutter. Flow positions come from the whole base span; each glyph is
|
||||
/// centred across the gutter by its own fallback-font run.
|
||||
fn ruby_draws(layout: &VerticalLayout, ruby: &RubyCell, origin: (f32, f32)) -> Vec<GlyphDraw> {
|
||||
let column = &layout.columns[ruby.column];
|
||||
let gutter_center = origin.0
|
||||
@ -361,8 +358,7 @@ fn paths_from_layout(
|
||||
paths
|
||||
}
|
||||
|
||||
/// Convert the custom vertical layout to glyph-outline paths using the same
|
||||
/// cells, composite transforms, annotations and alignment as the canvas pass.
|
||||
/// Glyph-outline paths of the vertical layout, matching the canvas pass.
|
||||
pub fn vertical_text_paths(
|
||||
text_content: &TextContent,
|
||||
vertical_align: VerticalAlign,
|
||||
@ -376,14 +372,11 @@ pub fn vertical_text_paths(
|
||||
paths_from_layout(&layout, &bounds, vertical_align, antialias)
|
||||
}
|
||||
|
||||
/// Developer overlay: draw a jlreq-style character-frame grid over the
|
||||
/// laid-out vertical cells. For every column it outlines the column band;
|
||||
/// for every cell it draws the advance box (the real layout cell), the
|
||||
/// virtual body / em square centred on the column axis, and the glyph-ink
|
||||
/// band. This exposes solid-setting, aki, letter-spacing and column
|
||||
/// planning visually, mirroring the grids in the jlreq figures. It is never
|
||||
/// emitted to exported or persisted output — only the on-screen fills pass
|
||||
/// calls it, gated by the `TEXT_GRID_VISIBLE` render flag.
|
||||
/// Developer overlay: a jlreq-style character-frame grid. Outlines each
|
||||
/// column band and, per cell, the advance box, the virtual body / em square
|
||||
/// centred on the column axis, and the glyph-ink band, so aki and
|
||||
/// letter-spacing are visible. Only the on-screen fills pass calls it, behind
|
||||
/// the `TEXT_GRID_VISIBLE` render flag; it never reaches exported output.
|
||||
pub fn paint_grid(canvas: &Canvas, layout: &VerticalLayout, bounds: &Rect, align: VerticalAlign) {
|
||||
let (origin_x, origin_y) = layout.origin(bounds, align);
|
||||
|
||||
@ -462,9 +455,9 @@ pub fn paint_grid(canvas: &Canvas, layout: &VerticalLayout, bounds: &Rect, align
|
||||
}
|
||||
|
||||
/// Paint the vertical glyph shadows. The shadow paint carries the
|
||||
/// blur/offset image filter; drawing the glyphs directly through it renders
|
||||
/// each glyph's shadow without allocating a full-content offscreen layer
|
||||
/// (a `save_layer` with the filter can exceed GPU limits on tall columns).
|
||||
/// blur/offset image filter, so glyphs draw through it with no offscreen
|
||||
/// layer (a `save_layer` with the filter can exceed GPU limits on tall
|
||||
/// columns).
|
||||
pub fn paint_drop_shadow(
|
||||
canvas: &Canvas,
|
||||
layout: &VerticalLayout,
|
||||
@ -478,10 +471,9 @@ pub fn paint_drop_shadow(
|
||||
paint_glyphs(canvas, layout, bounds, vertical_align, &paint);
|
||||
}
|
||||
|
||||
/// Paint a stroke masked to the vertical glyph silhouettes. Center strokes
|
||||
/// draw the stroked outline directly; inner/outer strokes are masked with
|
||||
/// `SrcIn` / `SrcOut` against the glyph silhouette (mirrors the horizontal
|
||||
/// masked-stroke path).
|
||||
/// Paint a stroke on the vertical glyphs. Center strokes draw directly;
|
||||
/// inner/outer strokes mask with `SrcIn` / `SrcOut` against the glyph
|
||||
/// silhouette, as the horizontal path does.
|
||||
pub fn paint_stroke(
|
||||
canvas: &Canvas,
|
||||
layout: &VerticalLayout,
|
||||
@ -562,9 +554,7 @@ fn paint_masked_stroke(
|
||||
}
|
||||
|
||||
fn text_blob_path(mut blob: TextBlob, offset: impl Into<SkPoint>) -> skia::Path {
|
||||
// SkParagraph normalizes extracted glyph outlines against the blob's ink
|
||||
// bounds. Restore that origin before applying the draw offset so the path
|
||||
// occupies the same document coordinates as Canvas::draw_text_blob.
|
||||
// get_path is relative to the blob's ink bounds; add them back to match.
|
||||
let bounds = *blob.bounds();
|
||||
let offset = offset.into();
|
||||
SkiaParagraph::get_path(&mut blob).with_offset((offset.x + bounds.left, offset.y + bounds.top))
|
||||
@ -584,11 +574,10 @@ fn push_text_path(
|
||||
paths.push((path, paint));
|
||||
}
|
||||
|
||||
/// Decoration bar geometry for a cell in absolute coordinates. Underline
|
||||
/// runs along the *left* side of the column (the under side in
|
||||
/// `vertical-rl`); line-through runs down the column centre. Both span the
|
||||
/// cell's vertical extent so consecutive decorated cells tile a continuous
|
||||
/// bar.
|
||||
/// Decoration bar of a cell in absolute coordinates. Underline runs along
|
||||
/// the *left* side of the column (the under side in `vertical-rl`);
|
||||
/// line-through runs down the centre. Both span the cell's extent, so
|
||||
/// adjacent decorated cells form one bar.
|
||||
fn decoration_bar(
|
||||
layout: &VerticalLayout,
|
||||
cell: &VerticalCell,
|
||||
@ -682,12 +671,9 @@ mod tests {
|
||||
let underline = decoration_bar(&layout, cell, ox, oy, false);
|
||||
let strike = decoration_bar(&layout, cell, ox, oy, true);
|
||||
|
||||
// Underline sits left of the column axis; line-through is centered.
|
||||
assert!(underline.center_x() < x_center);
|
||||
assert!((strike.center_x() - x_center).abs() < 0.01);
|
||||
// Both bars span the cell's vertical extent.
|
||||
assert!((underline.height() - cell.extent).abs() < 0.01);
|
||||
// Bar thickness is at least 1px.
|
||||
assert!(underline.width() >= 1.0 - 0.01);
|
||||
}
|
||||
|
||||
@ -733,9 +719,8 @@ mod tests {
|
||||
let mut surface = skia::surfaces::raster_n32_premul((256, 256)).unwrap();
|
||||
let canvas = surface.canvas();
|
||||
|
||||
// Shape transforms are applied on the caller's canvas. Exercise a
|
||||
// non-trivial transform with mixed upright/rotated cells so every
|
||||
// vertical paint pass remains transform-safe.
|
||||
// Callers apply shape transforms to the canvas; run every pass under
|
||||
// a non-trivial transform with upright and rotated cells.
|
||||
canvas.translate((12.0, 8.0));
|
||||
canvas.rotate(7.0, Some((64.0, 64.0).into()));
|
||||
|
||||
@ -748,7 +733,7 @@ mod tests {
|
||||
&Paint::default(),
|
||||
);
|
||||
|
||||
// With a real drop-shadow image filter (as `drop_shadow_paints` builds).
|
||||
// A real drop-shadow image filter, as `drop_shadow_paints` builds.
|
||||
let mut shadow_paint = Paint::default();
|
||||
shadow_paint.set_image_filter(skia::image_filters::drop_shadow(
|
||||
(12.0, 12.0),
|
||||
|
||||
@ -8,20 +8,13 @@ use crate::shapes::{PositionData, VerticalAlign};
|
||||
use super::annotations::{emphasis_mark_center, ruby_strip_x};
|
||||
use super::layout::{column_base_center, CellKind, VerticalCell, VerticalLayout};
|
||||
|
||||
/// Position-data `direction` value marking a vertical (vertical-rl)
|
||||
/// strip; 0/1 are the horizontal rtl/ltr values. The CLJS deserializer
|
||||
/// turns it into `:writing-mode "vertical-rl"` on the entry so the
|
||||
/// legacy SVG renderer can draw the strip vertically.
|
||||
/// Position-data `direction` of a vertical-rl strip; 0/1 are rtl/ltr.
|
||||
pub const DIRECTION_VERTICAL_RL: u32 = 2;
|
||||
|
||||
/// Position-data `direction` value marking a ruby annotation strip: the
|
||||
/// entry's offsets index the span's *ruby* string and the geometry is the
|
||||
/// exact gutter placement the canvas paints.
|
||||
/// Position-data `direction` of a ruby strip; offsets index the *ruby* string.
|
||||
pub const DIRECTION_VERTICAL_RUBY: u32 = 3;
|
||||
|
||||
/// Position-data `direction` value marking one emphasis mark (圏点): the
|
||||
/// entry's offsets are its base character's and the geometry is the mark's
|
||||
/// em box, centred where the canvas paints it. Horizontal text uses it too.
|
||||
/// Position-data `direction` of one emphasis mark (圏点) box, in any mode.
|
||||
pub const DIRECTION_EMPHASIS_MARK: u32 = 4;
|
||||
|
||||
/// Paragraph source UTF-16 range of a `transformed` span-text range.
|
||||
@ -134,8 +127,7 @@ pub fn position_data(
|
||||
}
|
||||
}
|
||||
let column = &layout.columns[first.column];
|
||||
// Base text occupies the base sub-band; any ruby gutter is excluded
|
||||
// so the editor overlay and selection track the glyphs.
|
||||
// Base sub-band only, so overlay and selection skip the ruby gutter.
|
||||
let rect = (
|
||||
origin_x + column.x,
|
||||
origin_y + first.top,
|
||||
@ -152,11 +144,9 @@ pub fn position_data(
|
||||
i = j;
|
||||
}
|
||||
|
||||
// Ruby annotation strips, with the exact flow-axis placement the canvas
|
||||
// paints. `start_pos`/`end_pos` are UTF-16 offsets
|
||||
// into the span's ruby string, taken from the shaped clusters so
|
||||
// surrogate-pair readings slice correctly. The consumer renders them as
|
||||
// their own half-size vertical strips in the column's right-side gutter.
|
||||
// Ruby strips at the flow positions the canvas paints. `start_pos` and
|
||||
// `end_pos` are UTF-16 offsets into the span's ruby string, taken from the
|
||||
// shaped clusters so surrogate pairs slice correctly.
|
||||
for ruby in &layout.ruby_cells {
|
||||
let (Some(first), Some(last)) = (ruby.glyphs.first(), ruby.glyphs.last()) else {
|
||||
continue;
|
||||
@ -255,8 +245,8 @@ fn warichu_first_line_len(
|
||||
Some(first_end.saturating_sub(cell_start).min(chars))
|
||||
}
|
||||
|
||||
/// Caret position (paragraph index, paragraph-relative Unicode scalar offset) for
|
||||
/// a point given relative to the content block's top-left origin.
|
||||
/// Caret position (paragraph index, paragraph-relative Unicode scalar
|
||||
/// offset) for a point relative to the content block's top-left origin.
|
||||
pub fn caret_from_point(layout: &VerticalLayout, x: f32, y: f32) -> Option<(usize, usize)> {
|
||||
if layout.columns.is_empty() {
|
||||
return None;
|
||||
@ -307,9 +297,8 @@ pub fn caret_from_point(layout: &VerticalLayout, x: f32, y: f32) -> Option<(usiz
|
||||
cell_start + ((frac * chars as f32).round() as usize).min(chars)
|
||||
}
|
||||
CellKind::Warichu { first_chars, .. } => {
|
||||
// Right sub-column holds the first sub-line's characters,
|
||||
// left sub-column the second; the vertical position picks
|
||||
// the offset within the chosen sub-line.
|
||||
// The right sub-column holds the first sub-line, the left
|
||||
// the second; y picks the offset within that sub-line.
|
||||
let column = &layout.columns[cell.column];
|
||||
let centre = column_base_center(column);
|
||||
let first =
|
||||
@ -341,11 +330,10 @@ pub fn caret_from_point(layout: &VerticalLayout, x: f32, y: f32) -> Option<(usiz
|
||||
))
|
||||
}
|
||||
|
||||
/// Caret rectangle for a paragraph-relative Unicode scalar offset, in
|
||||
/// content-local coordinates. The rect spans the column width; its height
|
||||
/// is the extent of the character at the offset (used by overtype-mode
|
||||
/// carets to cover the glyph) or zero when the caret sits after the last
|
||||
/// character, where a thin bar is drawn instead.
|
||||
/// Caret rect for a paragraph-relative Unicode scalar offset, in
|
||||
/// content-local coordinates. It spans the column width; its height is the
|
||||
/// extent of the character at the offset (overtype carets cover the glyph),
|
||||
/// or zero after the last character.
|
||||
pub fn caret_rect(layout: &VerticalLayout, paragraph: usize, offset: usize) -> Option<Rect> {
|
||||
let (col_start, _) = *layout.paragraph_columns.get(paragraph)?;
|
||||
|
||||
@ -371,10 +359,9 @@ pub fn caret_rect(layout: &VerticalLayout, paragraph: usize, offset: usize) -> O
|
||||
Rect::from_xywh(x, cell.top, width, cell.extent)
|
||||
}
|
||||
CellKind::Warichu { first_chars, .. } => {
|
||||
// Right sub-column holds the first sub-line's
|
||||
// characters, left sub-column the second (jlreq reading
|
||||
// order); the caret tracks the offset down the chosen
|
||||
// half-width sub-line.
|
||||
// The right sub-column holds the first sub-line, the
|
||||
// left the second (jlreq reading order); the caret moves
|
||||
// down that half-width sub-line.
|
||||
let first =
|
||||
warichu_first_line_len(layout, cell, first_chars, cell_start, chars)?;
|
||||
let within = offset - cell_start;
|
||||
@ -420,7 +407,7 @@ pub fn caret_rect(layout: &VerticalLayout, paragraph: usize, offset: usize) -> O
|
||||
})
|
||||
}
|
||||
|
||||
/// Selection rectangles for a paragraph-relative Unicode scalar offset range, in
|
||||
/// Selection rects for a paragraph-relative Unicode scalar offset range, in
|
||||
/// content-local coordinates.
|
||||
pub fn range_rects(
|
||||
layout: &VerticalLayout,
|
||||
@ -649,7 +636,6 @@ mod tests {
|
||||
"second sub-line restarts at the composite top, got {}",
|
||||
second.top
|
||||
);
|
||||
// Offsets advance down the sub-line.
|
||||
let deeper = caret_rect(&layout, 0, cell.start + 4).expect("caret rect");
|
||||
assert!(
|
||||
deeper.top > second.top,
|
||||
|
||||
@ -114,8 +114,8 @@ impl ShapedRun {
|
||||
)
|
||||
}
|
||||
|
||||
/// Spread the glyphs apart by `letter_spacing` per glyph, mirroring the
|
||||
/// horizontal per-glyph spacing. Returns the spaced run advance.
|
||||
/// Spreads the glyphs by `letter_spacing` each, like horizontal
|
||||
/// per-glyph spacing. Returns the spaced run advance.
|
||||
pub(super) fn spread_glyphs(&mut self, letter_spacing: f32) -> f32 {
|
||||
if letter_spacing != 0.0 {
|
||||
for (i, position) in self.positions.iter_mut().enumerate() {
|
||||
@ -125,9 +125,9 @@ impl ShapedRun {
|
||||
self.advance + letter_spacing * self.glyphs.len() as f32
|
||||
}
|
||||
|
||||
/// Normalize ASCII word spaces inside a sideways Western run to JLREQ's
|
||||
/// preferred one-third em. Shaping retains the source scalar and break
|
||||
/// opportunity; only its advance and following glyph positions change.
|
||||
/// Sets ASCII word spaces in a sideways Western run to JLREQ's one-third
|
||||
/// em. The space keeps its scalar and break opportunity; only its advance
|
||||
/// and the following glyph positions change.
|
||||
pub(super) fn normalize_word_spaces(&mut self, segment_text: &str, font_size: f32) {
|
||||
let clusters: Vec<(usize, usize)> = self.cluster_spans().collect();
|
||||
let mut accumulated_shift = 0.0f32;
|
||||
@ -232,10 +232,9 @@ fn feature(tag: &[u8; 4]) -> Feature {
|
||||
}
|
||||
}
|
||||
|
||||
/// `vpal` is deliberately absent: SkShaper shapes on a horizontal line,
|
||||
/// where HarfBuzz would apply the feature's y-placement deltas as glyph
|
||||
/// offsets without its advance deltas. Vertical layout applies the parsed
|
||||
/// GPOS `vpal` metrics to upright cells itself.
|
||||
/// Omits `vpal`: SkShaper shapes on a horizontal line, where HarfBuzz would
|
||||
/// apply its y-placement deltas as glyph offsets without the advance deltas.
|
||||
/// Vertical layout applies the parsed GPOS `vpal` to upright cells itself.
|
||||
fn font_feature(font_features: FontFeatures) -> Option<Feature> {
|
||||
match font_features {
|
||||
FontFeatures::None => None,
|
||||
@ -321,10 +320,9 @@ fn typeface_chunks(text: &str, typefaces: &[Typeface]) -> Vec<(Range<usize>, usi
|
||||
chunks
|
||||
}
|
||||
|
||||
/// Shape a segment with explicit per-character font fallback. Penpot's
|
||||
/// `TypefaceFontProvider` can resolve named families, but its character
|
||||
/// fallback hook is not available consistently in every Skia build. Resolve
|
||||
/// coverage here so missing glyphs never depend on that hook.
|
||||
/// Shape a segment with explicit per-character font fallback.
|
||||
/// `TypefaceFontProvider` resolves named families, but some Skia builds lack
|
||||
/// its character fallback hook, so glyph coverage is resolved here.
|
||||
pub(crate) fn shape_segment_with_fallbacks(
|
||||
text: &str,
|
||||
font_size: f32,
|
||||
@ -438,8 +436,8 @@ mod tests {
|
||||
let shift = run.rotated_baseline_shift;
|
||||
assert!(((top + bottom) / 2.0 + shift).abs() < 0.01);
|
||||
|
||||
// Lowercase ink does not occupy the face's full ascent/descent band;
|
||||
// this guards against regressing to font-wide metric centring.
|
||||
// Lowercase ink does not fill the face's ascent/descent band, so ink
|
||||
// centring differs from font-wide metric centring.
|
||||
let (_, metrics) = run.font.metrics();
|
||||
let metrics_shift = rotated_baseline_shift(metrics.ascent, metrics.descent);
|
||||
assert!((shift - metrics_shift).abs() > 0.1);
|
||||
|
||||
@ -16,15 +16,10 @@ use super::layout::{layout_vertical, VerticalLayout};
|
||||
|
||||
pub(super) const TEST_FONT: &[u8] = include_bytes!("../../fonts/sourcesanspro-regular.ttf");
|
||||
|
||||
/// A tiny Noto Sans JP subset carrying `vmtx`/`vhea`: U+3031 (〱, the
|
||||
/// vertical kana repeat mark) has a 2em vertical advance vs a 1em
|
||||
/// horizontal advance; U+3042/U+304F are symmetric controls.
|
||||
/// Noto Sans JP subset with `vmtx`/`vhea`: 〱 advances 2em vertically, あ/く 1em.
|
||||
pub(super) const VMTX_TEST_FONT: &[u8] = include_bytes!("../../fonts/notosansjp-vmtx-test.ttf");
|
||||
|
||||
/// Subset of Noto Sans JP carrying GSUB `vert` and GPOS `vpal`: the
|
||||
/// vertical alternates of 、。「」 halve their vertical advances (「 also
|
||||
/// lifts its ink by 481 units) and the あ/く alternates tighten by
|
||||
/// 58/60 units with small placement lifts.
|
||||
/// Noto Sans JP subset with GSUB `vert` and GPOS `vpal` for 、。「」あく.
|
||||
pub(super) const VPAL_TEST_FONT: &[u8] = include_bytes!("../../fonts/notosansjp-vpal-test.ttf");
|
||||
|
||||
/// Font size of every test span.
|
||||
|
||||
@ -281,8 +281,7 @@ pub struct TextComposition {
|
||||
pub previous: String,
|
||||
pub current: String,
|
||||
pub is_composing: bool,
|
||||
/// Where the preview text starts; the preview replaced by each update is
|
||||
/// `[start, start + previous]`, independent of where the caret is.
|
||||
/// Preview start: each update replaces `[start, start + previous]`.
|
||||
pub start: Option<TextPositionWithAffinity>,
|
||||
}
|
||||
|
||||
@ -898,9 +897,8 @@ impl TextEditorState {
|
||||
TextDirection::LTR
|
||||
};
|
||||
|
||||
// In vertical-rl the physical arrow keys map onto logical navigation
|
||||
// differently: Up/Down walk characters along the column, and Left/Right
|
||||
// cross columns (columns advance right-to-left).
|
||||
// In vertical-rl, Up/Down move by character along the column and
|
||||
// Left/Right cross columns (columns advance right-to-left).
|
||||
let is_vertical = text_content.is_vertical();
|
||||
let direction = if is_vertical {
|
||||
match direction {
|
||||
@ -987,8 +985,8 @@ mod tests {
|
||||
assert_eq!((replaced.start().offset, replaced.end().offset), (2, 2));
|
||||
composition.start = Some(replaced.start());
|
||||
|
||||
// The caret now sits after the preview; the next update still
|
||||
// replaces the preview itself.
|
||||
// The caret sits after the preview; the next update still replaces
|
||||
// the preview itself.
|
||||
composition.update("にほ");
|
||||
let replaced = composition.get_selection(&caret(0, 3));
|
||||
assert_eq!((replaced.start().offset, replaced.end().offset), (2, 3));
|
||||
|
||||
@ -108,8 +108,7 @@ pub struct RawParagraphData {
|
||||
text_transform: RawTextTransform,
|
||||
writing_mode: RawWritingMode,
|
||||
text_orientation: RawTextOrientation,
|
||||
// Explicit padding so the CLJS writer and this struct agree on a
|
||||
// 4-byte-aligned layout; always written as zero.
|
||||
// Padding for the CLJS writer's 4-byte-aligned layout; always zero.
|
||||
_padding: [u8; 2],
|
||||
line_height: f32,
|
||||
letter_spacing: f32,
|
||||
@ -149,8 +148,7 @@ pub struct RawTextSpan {
|
||||
ruby_align: RawRubyAlign,
|
||||
ruby_overhang: RawRubyOverhang,
|
||||
ruby_side: RawRubySide,
|
||||
// Explicit padding so the CLJS writer and this struct agree on a
|
||||
// 4-byte-aligned layout; always written as zero.
|
||||
// Padding for the CLJS writer's 4-byte-aligned layout; always zero.
|
||||
_padding: [u8; 2],
|
||||
font_size: f32,
|
||||
line_height: f32,
|
||||
@ -501,8 +499,8 @@ mod tests {
|
||||
|
||||
/// The CLJS writer (texts.cljs) writes PARAGRAPH-ATTR-U8-SIZE (16)
|
||||
/// attr bytes after the u32 span count, and SPAN-ATTR-U8-SIZE (80)
|
||||
/// attr bytes before the fills block. These sizes must move in
|
||||
/// lockstep with the struct layouts.
|
||||
/// attr bytes before the fills block. These sizes must match the struct
|
||||
/// layouts.
|
||||
#[test]
|
||||
fn raw_struct_sizes_match_cljs_writer() {
|
||||
const PARAGRAPH_ATTR_U8_SIZE: usize = 16;
|
||||
|
||||
@ -710,8 +710,8 @@ pub extern "C" fn text_editor_move_cursor(
|
||||
// ============================================================================
|
||||
|
||||
/// Caret rectangle in page coordinates (the unrotated selrect space of the
|
||||
/// text overlay), reported independently of the blink phase. The frontend
|
||||
/// keeps the IME capture surface on it.
|
||||
/// text overlay), regardless of the blink phase. The frontend keeps the IME
|
||||
/// capture surface on it.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn text_editor_get_cursor_rect() -> *mut u8 {
|
||||
with_state!(state, {
|
||||
@ -725,7 +725,7 @@ pub extern "C" fn text_editor_get_cursor_rect() -> *mut u8 {
|
||||
};
|
||||
|
||||
// A preview update clears the layout; rebuild it so the rect is
|
||||
// current right away instead of after the next render.
|
||||
// current before the next render.
|
||||
update_text_layout_if_needed(state, shape_id);
|
||||
|
||||
let Some(shape) = state.shapes.get(&shape_id) else {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user