diff --git a/common/src/app/common/fonts.cljs b/common/src/app/common/fonts.cljs index 48fda0c991..5eb9067238 100644 --- a/common/src/app/common/fonts.cljs +++ b/common/src/app/common/fonts.cljs @@ -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]" diff --git a/common/src/app/common/types/text/japanese_layout.cljc b/common/src/app/common/types/text/japanese_layout.cljc index 75877fd4cc..392b951bed 100644 --- a/common/src/app/common/types/text/japanese_layout.cljc +++ b/common/src/app/common/types/text/japanese_layout.cljc @@ -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)) diff --git a/frontend/src/app/main/ui/shapes/text/fo_text.cljs b/frontend/src/app/main/ui/shapes/text/fo_text.cljs index 56afc0d711..6c341a63e3 100644 --- a/frontend/src/app/main/ui/shapes/text/fo_text.cljs +++ b/frontend/src/app/main/ui/shapes/text/fo_text.cljs @@ -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)] diff --git a/frontend/src/app/main/ui/shapes/text/html_text.cljs b/frontend/src/app/main/ui/shapes/text/html_text.cljs index 254afec61c..cf44f72db0 100644 --- a/frontend/src/app/main/ui/shapes/text/html_text.cljs +++ b/frontend/src/app/main/ui/shapes/text/html_text.cljs @@ -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 diff --git a/frontend/src/app/main/ui/shapes/text/styles.cljs b/frontend/src/app/main/ui/shapes/text/styles.cljs index 97a010fa00..ba81df3001 100644 --- a/frontend/src/app/main/ui/shapes/text/styles.cljs +++ b/frontend/src/app/main/ui/shapes/text/styles.cljs @@ -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 " " pair ("filled dot"). + ;; Stored kebab values ("filled-dot") become the CSS " " + ;; 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))) diff --git a/frontend/src/app/main/ui/workspace/shapes/text/v3_editor.cljs b/frontend/src/app/main/ui/workspace/shapes/text/v3_editor.cljs index d33f35cb3a..59b1a696cc 100644 --- a/frontend/src/app/main/ui/workspace/shapes/text/v3_editor.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/text/v3_editor.cljs @@ -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" diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/text_japanese_layout.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/text_japanese_layout.cljs index 51ca4e3393..4cb3c27f07 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/text_japanese_layout.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/text_japanese_layout.cljs @@ -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" diff --git a/frontend/src/app/main/ui/workspace/viewport/selection.cljs b/frontend/src/app/main/ui/workspace/viewport/selection.cljs index 2104e9f1dc..d064e717ec 100644 --- a/frontend/src/app/main/ui/workspace/viewport/selection.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/selection.cljs @@ -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?) diff --git a/frontend/src/app/render_wasm/api.cljs b/frontend/src/app/render_wasm/api.cljs index 6f775661aa..bdc277d3a5 100644 --- a/frontend/src/app/render_wasm/api.cljs +++ b/frontend/src/app/render_wasm/api.cljs @@ -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) diff --git a/frontend/src/app/render_wasm/text_editor.cljs b/frontend/src/app/render_wasm/text_editor.cljs index 1ec2c0f503..95fbc3dd56 100644 --- a/frontend/src/app/render_wasm/text_editor.cljs +++ b/frontend/src/app/render_wasm/text_editor.cljs @@ -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) diff --git a/frontend/src/app/util/text/writing_mode.cljs b/frontend/src/app/util/text/writing_mode.cljs index 0f7624d4bb..a53669584e 100644 --- a/frontend/src/app/util/text/writing_mode.cljs +++ b/frontend/src/app/util/text/writing_mode.cljs @@ -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))) diff --git a/frontend/test/frontend_tests/render_wasm/texts_test.cljs b/frontend/test/frontend_tests/render_wasm/texts_test.cljs index 6098480d40..ead4bb9c91 100644 --- a/frontend/test/frontend_tests/render_wasm/texts_test.cljs +++ b/frontend/test/frontend_tests/render_wasm/texts_test.cljs @@ -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)))) diff --git a/plugins/libs/plugin-types/index.d.ts b/plugins/libs/plugin-types/index.d.ts index ec107ccced..534fc0e36e 100644 --- a/plugins/libs/plugin-types/index.d.ts +++ b/plugins/libs/plugin-types/index.d.ts @@ -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; /** diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs index fa2daad046..ac9577dfeb 100644 --- a/render-wasm/src/render.rs +++ b/render-wasm/src/render.rs @@ -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() diff --git a/render-wasm/src/render/fonts.rs b/render-wasm/src/render/fonts.rs index 822b433988..7c0d950854 100644 --- a/render-wasm/src/render/fonts.rs +++ b/render-wasm/src/render/fonts.rs @@ -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)); diff --git a/render-wasm/src/render/options.rs b/render-wasm/src/render/options.rs index 7868751e6c..841082fa20 100644 --- a/render-wasm/src/render/options.rs +++ b/render-wasm/src/render/options.rs @@ -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 } diff --git a/render-wasm/src/render/text.rs b/render-wasm/src/render/text.rs index d93c5492f2..49a569abae 100644 --- a/render-wasm/src/render/text.rs +++ b/render-wasm/src/render/text.rs @@ -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())); diff --git a/render-wasm/src/shapes/gpos_vpal.rs b/render-wasm/src/shapes/gpos_vpal.rs index 28670ad911..b3edcccca1 100644 --- a/render-wasm/src/shapes/gpos_vpal.rs +++ b/render-wasm/src/shapes/gpos_vpal.rs @@ -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> { /// 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 { let mut delta = VpalDelta::default(); let mut o = offset; diff --git a/render-wasm/src/shapes/japanese.rs b/render-wasm/src/shapes/japanese.rs index 7038400083..b8776e1b98 100644 --- a/render-wasm/src/shapes/japanese.rs +++ b/render-wasm/src/shapes/japanese.rs @@ -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; } diff --git a/render-wasm/src/shapes/kinsoku.rs b/render-wasm/src/shapes/kinsoku.rs index 7130744dfd..71fe3dc237 100644 --- a/render-wasm/src/shapes/kinsoku.rs +++ b/render-wasm/src/shapes/kinsoku.rs @@ -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, } @@ -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>], @@ -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()); diff --git a/render-wasm/src/shapes/modifiers.rs b/render-wasm/src/shapes/modifiers.rs index 84fbf8bccc..78d17085ea 100644 --- a/render-wasm/src/shapes/modifiers.rs +++ b/render-wasm/src/shapes/modifiers.rs @@ -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 { diff --git a/render-wasm/src/shapes/text.rs b/render-wasm/src/shapes/text.rs index 1d7aa1dcbe..9e09ae63ca 100644 --- a/render-wasm/src/shapes/text.rs +++ b/render-wasm/src/shapes/text.rs @@ -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 { - // 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, 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 = diff --git a/render-wasm/src/shapes/text_japanese.rs b/render-wasm/src/shapes/text_japanese.rs index c6bb5dc94b..1a26e42fd7 100644 --- a/render-wasm/src/shapes/text_japanese.rs +++ b/render-wasm/src/shapes/text_japanese.rs @@ -47,9 +47,9 @@ pub(crate) fn layout_span_texts(paragraph: &Paragraph) -> (Vec, 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, @@ -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 = 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] diff --git a/render-wasm/src/shapes/text_vertical/annotations.rs b/render-wasm/src/shapes/text_vertical/annotations.rs index 3d2db5db16..3d4f354e94 100644 --- a/render-wasm/src/shapes/text_vertical/annotations.rs +++ b/render-wasm/src/shapes/text_vertical/annotations.rs @@ -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, 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, 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 = (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); diff --git a/render-wasm/src/shapes/text_vertical/cells.rs b/render-wasm/src/shapes/text_vertical/cells.rs index 8ce1f2c318..82519cd40b 100644 --- a/render-wasm/src/shapes/text_vertical/cells.rs +++ b/render-wasm/src/shapes/text_vertical/cells.rs @@ -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, } @@ -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, cells: &mut Vec, ) -> 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. diff --git a/render-wasm/src/shapes/text_vertical/flow.rs b/render-wasm/src/shapes/text_vertical/flow.rs index 446de4fbb7..302a8931a6 100644 --- a/render-wasm/src/shapes/text_vertical/flow.rs +++ b/render-wasm/src/shapes/text_vertical/flow.rs @@ -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, - /// 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, 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 { 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]) { 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, extent: f32, @@ -429,10 +417,9 @@ pub(super) fn preferred_pair_spacing(classes: &[Option]) -> Vec], @@ -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], @@ -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], @@ -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], @@ -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], @@ -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); diff --git a/render-wasm/src/shapes/text_vertical/font_tables.rs b/render-wasm/src/shapes/text_vertical/font_tables.rs index e31fd2e594..34ae4dbbc0 100644 --- a/render-wasm/src/shapes/text_vertical/font_tables.rs +++ b/render-wasm/src/shapes/text_vertical/font_tables.rs @@ -12,8 +12,7 @@ use crate::shapes::gpos_vpal::{parse_vpal, VpalDelta}; type TableCache = RefCell>>>; 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 = RefCell::default(); static VPAL_TABLES: TableCache = RefCell::default(); static TYPO_METRICS: TableCache = RefCell::default(); @@ -44,10 +43,9 @@ fn table_data(font: &Font, tag: &[u8; 4]) -> Option { } /// 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 { 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> { 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 } diff --git a/render-wasm/src/shapes/text_vertical/layout.rs b/render-wasm/src/shapes/text_vertical/layout.rs index 822f0c7bf3..40aebbdef8 100644 --- a/render-wasm/src/shapes/text_vertical/layout.rs +++ b/render-wasm/src/shapes/text_vertical/layout.rs @@ -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, - /// 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, pub ruby_cells: Vec, - /// Shaped emphasis-mark runs (single glyph each), indexed by - /// `EmphasisMark::run`. + /// Single-glyph emphasis runs, indexed by `EmphasisMark::run`. pub emphasis_runs: Vec, pub emphasis_marks: Vec, /// Per paragraph: [start, end) range into `columns`. @@ -137,19 +123,15 @@ pub struct VerticalLayout { pub span_source_utf16_starts: Vec>, /// Per paragraph and span: transformed scalar ownership in source text. pub span_transforms: Vec>, - /// 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>, 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], diff --git a/render-wasm/src/shapes/text_vertical/mod.rs b/render-wasm/src/shapes/text_vertical/mod.rs index ee376e388a..1c97e9f972 100644 --- a/render-wasm/src/shapes/text_vertical/mod.rs +++ b/render-wasm/src/shapes/text_vertical/mod.rs @@ -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; diff --git a/render-wasm/src/shapes/text_vertical/orientation.rs b/render-wasm/src/shapes/text_vertical/orientation.rs index 2500faed00..e34f01dc33 100644 --- a/render-wasm/src/shapes/text_vertical/orientation.rs +++ b/render-wasm/src/shapes/text_vertical/orientation.rs @@ -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 diff --git a/render-wasm/src/shapes/text_vertical/paint.rs b/render-wasm/src/shapes/text_vertical/paint.rs index 13e301de0d..48dda0d9da 100644 --- a/render-wasm/src/shapes/text_vertical/paint.rs +++ b/render-wasm/src/shapes/text_vertical/paint.rs @@ -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 { 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 Vec { 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) -> 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), diff --git a/render-wasm/src/shapes/text_vertical/positions.rs b/render-wasm/src/shapes/text_vertical/positions.rs index f1b2055070..06532b0646 100644 --- a/render-wasm/src/shapes/text_vertical/positions.rs +++ b/render-wasm/src/shapes/text_vertical/positions.rs @@ -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 { 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, diff --git a/render-wasm/src/shapes/text_vertical/shaping.rs b/render-wasm/src/shapes/text_vertical/shaping.rs index 65994d7ddc..3a00bb2faf 100644 --- a/render-wasm/src/shapes/text_vertical/shaping.rs +++ b/render-wasm/src/shapes/text_vertical/shaping.rs @@ -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 { match font_features { FontFeatures::None => None, @@ -321,10 +320,9 @@ fn typeface_chunks(text: &str, typefaces: &[Typeface]) -> Vec<(Range, 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); diff --git a/render-wasm/src/shapes/text_vertical/test_support.rs b/render-wasm/src/shapes/text_vertical/test_support.rs index 0fce41a7a9..24684e799e 100644 --- a/render-wasm/src/shapes/text_vertical/test_support.rs +++ b/render-wasm/src/shapes/text_vertical/test_support.rs @@ -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. diff --git a/render-wasm/src/state/text_editor.rs b/render-wasm/src/state/text_editor.rs index 7f6409d922..4d3e359500 100644 --- a/render-wasm/src/state/text_editor.rs +++ b/render-wasm/src/state/text_editor.rs @@ -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, } @@ -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)); diff --git a/render-wasm/src/wasm/text.rs b/render-wasm/src/wasm/text.rs index 8c11be5cc6..783892502b 100644 --- a/render-wasm/src/wasm/text.rs +++ b/render-wasm/src/wasm/text.rs @@ -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; diff --git a/render-wasm/src/wasm/text_editor.rs b/render-wasm/src/wasm/text_editor.rs index 240c4c9734..b463cf0368 100644 --- a/render-wasm/src/wasm/text_editor.rs +++ b/render-wasm/src/wasm/text_editor.rs @@ -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 {