mirror of
https://github.com/penpot/penpot.git
synced 2026-10-03 01:06:14 +00:00
Feature improvements
This commit is contained in:
parent
11d4cc6459
commit
c6ce618bab
@ -89,7 +89,7 @@
|
|||||||
[:writing-mode {:optional true} ::sm/text]
|
[:writing-mode {:optional true} ::sm/text]
|
||||||
[:text-orientation {:optional true} ::sm/text]
|
[:text-orientation {:optional true} ::sm/text]
|
||||||
[:text-combine-upright {:optional true} ::sm/text]
|
[:text-combine-upright {:optional true} ::sm/text]
|
||||||
[:text-emphasis {:optional true} ::sm/text]
|
[:emphasis-mark {:optional true} :boolean]
|
||||||
[:ruby {:optional true} :string]
|
[:ruby {:optional true} :string]
|
||||||
[:ruby-size {:optional true} ::sm/text]
|
[:ruby-size {:optional true} ::sm/text]
|
||||||
[:ruby-align {:optional true} ::sm/text]
|
[:ruby-align {:optional true} ::sm/text]
|
||||||
@ -97,8 +97,6 @@
|
|||||||
[:ruby-side {:optional true} ::sm/text]
|
[:ruby-side {:optional true} ::sm/text]
|
||||||
[:warichu {:optional true} ::sm/text]
|
[:warichu {:optional true} ::sm/text]
|
||||||
[:font-features {:optional true} ::sm/text]
|
[:font-features {:optional true} ::sm/text]
|
||||||
[:annotation-clearance {:optional true} ::sm/text]
|
|
||||||
[:annotation-has-ruby {:optional true} :boolean]
|
|
||||||
[:text {:optional true} :string]
|
[:text {:optional true} :string]
|
||||||
[:text-decoration {:optional true} ::sm/text]
|
[:text-decoration {:optional true} ::sm/text]
|
||||||
[:text-transform {:optional true} ::sm/text]]])
|
[:text-transform {:optional true} ::sm/text]]])
|
||||||
|
|||||||
@ -65,8 +65,21 @@
|
|||||||
:font-features "none"
|
:font-features "none"
|
||||||
:annotation-clearance "none"})
|
:annotation-clearance "none"})
|
||||||
|
|
||||||
;; Annotation font sizes relative to the base font size.
|
;; Glyph of each emphasis style, per CSS `text-emphasis-style`.
|
||||||
(def emphasis-font-scale 0.5)
|
(def ^:private emphasis-mark-chars
|
||||||
|
{"filled-dot" "•"
|
||||||
|
"open-dot" "◦"
|
||||||
|
"filled-circle" "●"
|
||||||
|
"open-circle" "○"
|
||||||
|
"filled-sesame" "﹅"
|
||||||
|
"open-sesame" "﹆"})
|
||||||
|
|
||||||
|
(defn emphasis-mark-char
|
||||||
|
"Mark glyph of a text-emphasis value, or nil for none."
|
||||||
|
[text-emphasis]
|
||||||
|
(get emphasis-mark-chars text-emphasis))
|
||||||
|
|
||||||
|
;; Warichu sub-line font size relative to the base font size.
|
||||||
(def warichu-font-scale 0.5)
|
(def warichu-font-scale 0.5)
|
||||||
|
|
||||||
(defn ruby-font-scale
|
(defn ruby-font-scale
|
||||||
|
|||||||
@ -22,71 +22,6 @@
|
|||||||
|
|
||||||
(def fill-attrs [:fill-color :fill-color-gradient :fill-opacity])
|
(def fill-attrs [:fill-color :fill-color-gradient :fill-opacity])
|
||||||
|
|
||||||
;; Kinsoku classes for the warichu sub-line split (same characters the
|
|
||||||
;; renderer's kinsoku module suppresses at line boundaries).
|
|
||||||
(def ^:private warichu-forbidden-at-start
|
|
||||||
(str "、。,.)」』]】〕〉》’”!?;:ー"
|
|
||||||
"ぁぃぅぇぉっゃゅょゎァィゥェォッャュョヮヵヶ"
|
|
||||||
"々ゝゞヽヾ・"))
|
|
||||||
|
|
||||||
(def ^:private warichu-forbidden-at-end "(「『[【〔〈《‘“")
|
|
||||||
|
|
||||||
(defn- warichu-split-index
|
|
||||||
"Safe JavaScript string index where a warichu run splits into its two
|
|
||||||
sub-lines. The split is chosen in Unicode code-point space, then translated
|
|
||||||
back to a UTF-16 boundary for `subs`: the
|
|
||||||
balanced midpoint (first sub-line longer), nudged forward then backward
|
|
||||||
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.
|
|
||||||
Mirrors the renderer's `warichu_split_chars`."
|
|
||||||
[text]
|
|
||||||
(let [characters (vec (js/Array.from text))
|
|
||||||
n (count characters)
|
|
||||||
mid (js/Math.ceil (/ n 2))
|
|
||||||
valid? (fn [split]
|
|
||||||
(and (>= split 1)
|
|
||||||
(< split n)
|
|
||||||
(not (.includes warichu-forbidden-at-start (nth characters split)))
|
|
||||||
(not (.includes warichu-forbidden-at-end (nth characters (dec split))))))
|
|
||||||
split (if (valid? mid)
|
|
||||||
mid
|
|
||||||
(or (->> (range 1 n)
|
|
||||||
(some (fn [distance]
|
|
||||||
(cond
|
|
||||||
(valid? (+ mid distance)) (+ mid distance)
|
|
||||||
(and (> mid distance) (valid? (- mid distance))) (- mid distance)))))
|
|
||||||
mid))]
|
|
||||||
(->> (take split characters)
|
|
||||||
(reduce (fn [index character] (+ index (.-length character))) 0))))
|
|
||||||
|
|
||||||
;; CSS `text-emphasis-style` character mapping (same glyphs the canvas
|
|
||||||
;; renderer shapes for each mark style).
|
|
||||||
(def ^:private emphasis-mark-chars
|
|
||||||
{"filled-dot" "•"
|
|
||||||
"open-dot" "◦"
|
|
||||||
"filled-circle" "●"
|
|
||||||
"open-circle" "○"
|
|
||||||
"filled-sesame" "﹅"
|
|
||||||
"open-sesame" "﹆"})
|
|
||||||
|
|
||||||
(def ^:private emphasis-prohibited-chars
|
|
||||||
"、。,.「」『』()[]【】〔〕〈〉《》‘’“”")
|
|
||||||
|
|
||||||
(defn- emphasis-character?
|
|
||||||
[character]
|
|
||||||
(and (not (re-matches #"\s" character))
|
|
||||||
(not (.includes emphasis-prohibited-chars character))))
|
|
||||||
|
|
||||||
(defn- emphasis-marks-text
|
|
||||||
"One mark per eligible Unicode base character. Spaces replace whitespace
|
|
||||||
and Japanese punctuation that does not normally carry emphasis so marks
|
|
||||||
stay aligned with the base slots."
|
|
||||||
[text mark]
|
|
||||||
(->> (js/Array.from text)
|
|
||||||
(map (fn [character]
|
|
||||||
(if (emphasis-character? character) mark " ")))
|
|
||||||
(apply str)))
|
|
||||||
|
|
||||||
(defn- px
|
(defn- px
|
||||||
"CSS pixel size, or nil for a non-positive size."
|
"CSS pixel size, or nil for a non-positive size."
|
||||||
[size]
|
[size]
|
||||||
@ -157,36 +92,25 @@
|
|||||||
(some? browser-props)
|
(some? browser-props)
|
||||||
(obj/merge! browser-props)))))
|
(obj/merge! browser-props)))))
|
||||||
|
|
||||||
(defn- warichu-lines
|
(defn- warichu-line-props
|
||||||
"Warichu: two half-size sub-lines within one inline strip. Vertical
|
"Props of the `text` element drawing one warichu sub-line at half size in
|
||||||
reading order is right then left; horizontal is top then bottom."
|
its own strip: the renderer splits the span and places each sub-line."
|
||||||
[shape data index fill]
|
[data fill]
|
||||||
(let [vertical? (= "vertical-rl" (:writing-mode data))
|
(let [vertical? (= "vertical-rl" (:writing-mode data))
|
||||||
font-size (* (js/parseFloat (:font-size data)) jl/warichu-font-scale)
|
font-size (* (js/parseFloat (:font-size data)) jl/warichu-font-scale)
|
||||||
font-size (if (js/isNaN font-size) 0 font-size)
|
font-size (if (js/isNaN font-size) 0 font-size)
|
||||||
style (annotation-style data font-size vertical? (when vertical? "upright") fill)
|
style (annotation-style data font-size vertical? (when vertical? "upright") fill)
|
||||||
text (:text data)
|
top (- (:y data) (:height data))]
|
||||||
split-index (warichu-split-index text)
|
(if vertical?
|
||||||
lines [(subs text 0 split-index) (subs text split-index)]
|
#js {:x (+ (:x data) (/ (:width data) 2))
|
||||||
top (- (:y data) (:height data))
|
:y top
|
||||||
centre (+ (:x data) (/ (:width data) 2))
|
:style style}
|
||||||
quarter (/ font-size 2)]
|
#js {:x (:x data)
|
||||||
(mf/html
|
:y top
|
||||||
[:g {:key (dm/str "warichu-" (:id shape) "-" index)}
|
:dominantBaseline "hanging"
|
||||||
(if vertical?
|
:textLength (:width data)
|
||||||
[:*
|
:lengthAdjust "spacingAndGlyphs"
|
||||||
[:> :text {:x (+ centre quarter) :y top :style style} (first lines)]
|
:style style})))
|
||||||
[:> :text {:x (- centre quarter) :y top :style style} (second lines)]]
|
|
||||||
[:*
|
|
||||||
(for [[line-index line] (d/enumerate lines)]
|
|
||||||
[:> :text {:key line-index
|
|
||||||
:x (:x data)
|
|
||||||
:y (+ top (* line-index (/ (:height data) 2)))
|
|
||||||
:dominantBaseline "hanging"
|
|
||||||
:textLength (:width data)
|
|
||||||
:lengthAdjust "spacingAndGlyphs"
|
|
||||||
:style style}
|
|
||||||
line])])])))
|
|
||||||
|
|
||||||
(defn- ruby-text-props
|
(defn- ruby-text-props
|
||||||
"Props of the `text` element drawing a strip's ruby annotation beside it
|
"Props of the `text` element drawing a strip's ruby annotation beside it
|
||||||
@ -230,25 +154,16 @@
|
|||||||
(-> (obj/set! "textLength" (:width data))
|
(-> (obj/set! "textLength" (:width data))
|
||||||
(obj/set! "lengthAdjust" "spacing")))))))
|
(obj/set! "lengthAdjust" "spacing")))))))
|
||||||
|
|
||||||
(defn- emphasis-text-props
|
(defn- emphasis-mark-props
|
||||||
"Props of the `text` element drawing a strip's emphasis marks: right of
|
"Props of the `text` element drawing one emphasis mark centred in its em
|
||||||
vertical text or above horizontal text, outside a stacked ruby layer."
|
box, as placed by the renderer."
|
||||||
[shape data index font-size ruby-offset style]
|
[data fill]
|
||||||
(let [key (dm/str "emphasis-" (:id shape) "-" index)]
|
(let [size (:width data)]
|
||||||
(if (= "vertical-rl" (:writing-mode data))
|
#js {:x (+ (:x data) (/ size 2))
|
||||||
#js {:key key
|
:y (- (:y data) (/ (:height data) 2))
|
||||||
:x (+ (:x data) (:width data) (/ font-size 2) ruby-offset)
|
:textAnchor "middle"
|
||||||
:y (- (:y data) (:height data))
|
:dominantBaseline "central"
|
||||||
:textLength (:height data)
|
:style (annotation-style data size false nil fill)}))
|
||||||
:lengthAdjust "spacing"
|
|
||||||
:style style}
|
|
||||||
#js {:key key
|
|
||||||
:x (if (= "rtl" (:direction data)) (+ (:x data) (:width data)) (:x data))
|
|
||||||
:y (- (:y data) (:height data) ruby-offset)
|
|
||||||
:dominantBaseline "text-after-edge"
|
|
||||||
:textLength (:width data)
|
|
||||||
:lengthAdjust "spacing"
|
|
||||||
:style style})))
|
|
||||||
|
|
||||||
(defn set-white-fill
|
(defn set-white-fill
|
||||||
[shape]
|
[shape]
|
||||||
@ -303,10 +218,10 @@
|
|||||||
(for [[index data] (d/enumerate position-data)]
|
(for [[index data] (d/enumerate position-data)]
|
||||||
(let [vertical? (= "vertical-rl" (:writing-mode data))
|
(let [vertical? (= "vertical-rl" (:writing-mode data))
|
||||||
ruby (jl/visible-ruby data)
|
ruby (jl/visible-ruby data)
|
||||||
emphasis-mark (get emphasis-mark-chars (:text-emphasis data))
|
|
||||||
font-size (js/parseFloat (:font-size data))
|
font-size (js/parseFloat (:font-size data))
|
||||||
scaled-size (fn [scale] (if (js/isNaN font-size) 0 (* font-size scale)))
|
ruby-font-size (if (js/isNaN font-size)
|
||||||
ruby-font-size (scaled-size (jl/ruby-font-scale (:ruby-size data)))
|
0
|
||||||
|
(* font-size (jl/ruby-font-scale (:ruby-size data))))
|
||||||
;; Annotations take the fills of this strip's own render id.
|
;; Annotations take the fills of this strip's own render id.
|
||||||
annotation-fill (str "url(#fill-" index "-" render-id "-" index ")")
|
annotation-fill (str "url(#fill-" index "-" render-id "-" index ")")
|
||||||
|
|
||||||
@ -333,26 +248,16 @@
|
|||||||
[:defs
|
[:defs
|
||||||
[:& fills/fills {:shape shape :render-id render-id}]]
|
[:& fills/fills {:shape shape :render-id render-id}]]
|
||||||
|
|
||||||
[:& shape-custom-strokes {:shape shape :position index :render-id render-id}
|
(if (:emphasis-mark data)
|
||||||
(if (jl/warichu-text? data)
|
;; Emphasis marks (圏点) are not stroked.
|
||||||
(warichu-lines shape data index annotation-fill)
|
[:> :text (emphasis-mark-props data annotation-fill) (:text data)]
|
||||||
[:> :text text-props (:text data)])]
|
[:*
|
||||||
(when (some? ruby)
|
[:& shape-custom-strokes {:shape shape :position index :render-id render-id}
|
||||||
[:> :text (ruby-text-props shape data index ruby-font-size
|
(if (= "warichu" (:warichu data))
|
||||||
(annotation-style data ruby-font-size vertical?
|
[:> :text (warichu-line-props data annotation-fill) (:text data)]
|
||||||
"upright" annotation-fill))
|
[:> :text text-props (:text data)])]
|
||||||
ruby])
|
(when (some? ruby)
|
||||||
;; Emphasis marks (圏点): one half-size mark per eligible base
|
[:> :text (ruby-text-props shape data index ruby-font-size
|
||||||
;; character.
|
(annotation-style data ruby-font-size vertical?
|
||||||
(when (some? emphasis-mark)
|
"upright" annotation-fill))
|
||||||
(let [ruby-layer? (and (= "auto" (:annotation-clearance data))
|
ruby])])]))]]))
|
||||||
(not= "under" (:ruby-side data "over"))
|
|
||||||
(or (some? ruby) (:annotation-has-ruby data)))
|
|
||||||
mark-size (scaled-size jl/emphasis-font-scale)
|
|
||||||
style (annotation-style data mark-size vertical?
|
|
||||||
(when vertical? "upright")
|
|
||||||
annotation-fill)]
|
|
||||||
[:> :text (emphasis-text-props shape data index mark-size
|
|
||||||
(if ruby-layer? ruby-font-size 0)
|
|
||||||
style)
|
|
||||||
(emphasis-marks-text (:text data) emphasis-mark)]))]))]]))
|
|
||||||
|
|||||||
@ -2934,6 +2934,29 @@
|
|||||||
|
|
||||||
(def ^:private POSITION-DATA-VERTICAL 2)
|
(def ^:private POSITION-DATA-VERTICAL 2)
|
||||||
(def ^:private POSITION-DATA-RUBY 3)
|
(def ^:private POSITION-DATA-RUBY 3)
|
||||||
|
(def ^:private POSITION-DATA-EMPHASIS-MARK 4)
|
||||||
|
|
||||||
|
(defn- emphasis-mark-entry
|
||||||
|
"Position-data entry for one emphasis mark (圏点): the geometry is the
|
||||||
|
mark's em box, centred where the canvas paints it."
|
||||||
|
[element {:keys [x y width height]}]
|
||||||
|
(when-let [mark (jl/emphasis-mark-char (get element :text-emphasis))]
|
||||||
|
(d/patch-object
|
||||||
|
(txt/get-default-text-attrs)
|
||||||
|
(d/without-nils
|
||||||
|
{:x x
|
||||||
|
:y (+ y height)
|
||||||
|
:width width
|
||||||
|
:height height
|
||||||
|
:direction "ltr"
|
||||||
|
:emphasis-mark true
|
||||||
|
:font-id (get element :font-id)
|
||||||
|
:font-family (get element :font-family)
|
||||||
|
:font-size (dm/str width "px")
|
||||||
|
:font-weight (get element :font-weight)
|
||||||
|
:font-style (get element :font-style)
|
||||||
|
:fills (get element :fills)
|
||||||
|
:text mark}))))
|
||||||
|
|
||||||
(defn- ruby-strip-entry
|
(defn- ruby-strip-entry
|
||||||
"Position-data entry for a ruby annotation strip: the offsets index the
|
"Position-data entry for a ruby annotation strip: the offsets index the
|
||||||
@ -2997,14 +3020,11 @@
|
|||||||
:text-transform (get element :text-transform)
|
:text-transform (get element :text-transform)
|
||||||
:text-decoration (get element :text-decoration)
|
:text-decoration (get element :text-decoration)
|
||||||
:text-combine-upright (get element :text-combine-upright)
|
:text-combine-upright (get element :text-combine-upright)
|
||||||
;; Emphasis marks (圏点) and warichu are drawn by the static SVG
|
:font-features (let [features (get element :font-features)]
|
||||||
;; renderer; "none" carries no information.
|
(when (and (seq features) (not= "none" features))
|
||||||
:text-emphasis (let [emphasis (get element :text-emphasis)]
|
features))
|
||||||
(when (and (seq emphasis) (not= "none" emphasis))
|
;; Each warichu sub-line has its own entry, drawn at half size.
|
||||||
emphasis))
|
|
||||||
:warichu (when (= "warichu" (get element :warichu)) "warichu")
|
:warichu (when (= "warichu" (get element :warichu)) "warichu")
|
||||||
:annotation-clearance (when (= "auto" (get element :annotation-clearance)) "auto")
|
|
||||||
:annotation-has-ruby (when (some? ruby) true)
|
|
||||||
;; Horizontal position data has no separate ruby strip, so the base
|
;; Horizontal position data has no separate ruby strip, so the base
|
||||||
;; entry carries the annotation for static SVG export.
|
;; entry carries the annotation for static SVG export.
|
||||||
:ruby (when (and (not vertical?) (seq element-text)) ruby)
|
:ruby (when (and (not vertical?) (seq element-text)) ruby)
|
||||||
@ -3049,8 +3069,9 @@
|
|||||||
(get 0) :children ;; paragraph-set
|
(get 0) :children ;; paragraph-set
|
||||||
(get paragraph))
|
(get paragraph))
|
||||||
element (-> paragraph-node :children (get span))]
|
element (-> paragraph-node :children (get span))]
|
||||||
(if (= direction POSITION-DATA-RUBY)
|
(condp = direction
|
||||||
(ruby-strip-entry element entry)
|
POSITION-DATA-RUBY (ruby-strip-entry element entry)
|
||||||
|
POSITION-DATA-EMPHASIS-MARK (emphasis-mark-entry element entry)
|
||||||
(text-strip-entry paragraph-node element entry)))))
|
(text-strip-entry paragraph-node element entry)))))
|
||||||
result))))
|
result))))
|
||||||
|
|
||||||
|
|||||||
@ -45,12 +45,12 @@
|
|||||||
:font-weight "400"})
|
:font-weight "400"})
|
||||||
|
|
||||||
(defn- setup-text
|
(defn- setup-text
|
||||||
"A text shape whose single position-data strip is `strip` (over
|
"A text shape with the given position-data `strips` (over
|
||||||
`strip-defaults`), with its objects and selection."
|
`strip-defaults`), with its objects and selection."
|
||||||
[{:keys [width height]} strip]
|
[{:keys [width height]} & strips]
|
||||||
(let [shape (-> (cts/setup-shape {:type :text :x 10 :y 20 :width width :height height})
|
(let [shape (-> (cts/setup-shape {:type :text :x 10 :y 20 :width width :height height})
|
||||||
(assoc :name "Text"
|
(assoc :name "Text"
|
||||||
:position-data [(merge strip-defaults strip)]))
|
:position-data (mapv #(merge strip-defaults %) strips)))
|
||||||
file (cths/add-sample-shape
|
file (cths/add-sample-shape
|
||||||
(cthf/sample-file :file1 :page-label :page1)
|
(cthf/sample-file :file1 :page-label :page1)
|
||||||
:text
|
:text
|
||||||
@ -72,10 +72,10 @@
|
|||||||
:writing-mode "horizontal-tb"}
|
:writing-mode "horizontal-tb"}
|
||||||
attrs))
|
attrs))
|
||||||
|
|
||||||
(defn- setup-vertical-warichu-text
|
(defn- emphasis-mark
|
||||||
[text]
|
"A mark entry as the renderer emits it: the mark glyph in its em box."
|
||||||
(setup-text {:width 60 :height 100}
|
[x y]
|
||||||
(vertical-strip {:height 40 :text text :warichu "warichu"})))
|
{:x x :y y :width 10 :height 10 :font-size "10px" :emphasis-mark true :text "•"})
|
||||||
|
|
||||||
(deftest empty-selection-yields-empty-string
|
(deftest empty-selection-yields-empty-string
|
||||||
(is (= "" (svg/generate-markup {} []))))
|
(is (= "" (svg/generate-markup {} []))))
|
||||||
@ -111,87 +111,48 @@
|
|||||||
(is (re-find #"font-feature-settings:"vpal"" markup))
|
(is (re-find #"font-feature-settings:"vpal"" markup))
|
||||||
(is (not (re-find #"<foreignObject\b" markup))))))
|
(is (not (re-find #"<foreignObject\b" markup))))))
|
||||||
|
|
||||||
(deftest vertical-emphasis-svg-emits-static-marks
|
(deftest emphasis-svg-draws-each-mark-in-its-box
|
||||||
(testing "Static SVG draws emphasis marks and skips whitespace characters"
|
(testing "Static SVG draws the marks the renderer placed, centred in their boxes"
|
||||||
(let [{:keys [objects shapes]} (setup-text {:width 60 :height 100}
|
(let [{:keys [objects shapes]} (setup-text {:width 60 :height 100}
|
||||||
(vertical-strip {:text "強調 あ" :text-emphasis "filled-dot"}))
|
(vertical-strip {:text "強調"})
|
||||||
|
(emphasis-mark 44 40)
|
||||||
|
(emphasis-mark 44 60))
|
||||||
markup (svg/generate-markup objects shapes)]
|
markup (svg/generate-markup objects shapes)]
|
||||||
(is (re-find #"強調 あ" markup))
|
(is (re-find #"強調" markup))
|
||||||
;; 4 base chars: mark, mark, space (whitespace keeps its slot), mark.
|
(is (= 2 (count-matches #">•<" markup)) "one text element per mark")
|
||||||
(is (re-find #"•• •" markup))
|
(is (re-find #"x=\"49\"" markup) "marks centre on their box")
|
||||||
|
(is (re-find #"text-anchor=\"middle\"" markup))
|
||||||
(is (re-find #"font-size:10px" markup))
|
(is (re-find #"font-size:10px" markup))
|
||||||
|
(is (re-find #"fill:url\(#fill-1-[^)]+-1\)" markup)
|
||||||
|
"a mark references its generated per-strip fill")
|
||||||
(is (not (re-find #"<foreignObject\b" markup))))))
|
(is (not (re-find #"<foreignObject\b" markup))))))
|
||||||
|
|
||||||
(deftest horizontal-emphasis-svg-emits-static-marks-above-the-text
|
(deftest vertical-warichu-svg-draws-each-sub-line-at-half-size
|
||||||
(testing "Static SVG draws horizontal emphasis and excludes punctuation and whitespace"
|
(testing "Static SVG draws each renderer-split warichu sub-line in its own strip"
|
||||||
(let [{:keys [objects shapes]} (setup-text {:width 120 :height 40}
|
(let [{:keys [objects shapes]} (setup-text {:width 60 :height 100}
|
||||||
(horizontal-strip {:text "強調、 あ" :text-emphasis "filled-dot"}))
|
(vertical-strip {:x 30 :width 10 :height 40
|
||||||
|
:text "割注" :warichu "warichu"})
|
||||||
|
(vertical-strip {:x 20 :width 10 :height 40
|
||||||
|
:text "入り" :warichu "warichu"}))
|
||||||
markup (svg/generate-markup objects shapes)]
|
markup (svg/generate-markup objects shapes)]
|
||||||
(is (re-find #"強調、 あ" markup))
|
(is (re-find #"x=\"35\"[^>]*>割注<" markup) "first sub-line on the right half")
|
||||||
;; Five Unicode base characters: two marks, punctuation and whitespace
|
(is (re-find #"x=\"25\"[^>]*>入り<" markup) "second sub-line on the left half")
|
||||||
;; slots, then one mark.
|
(is (= 2 (count-matches #"font-size:10px" markup)))
|
||||||
(is (re-find #"•• •" markup))
|
|
||||||
(is (re-find #"writing-mode:horizontal-tb" markup))
|
|
||||||
(is (re-find #"dominant-baseline=\"text-after-edge\"" markup))
|
|
||||||
(is (re-find #"lengthAdjust=\"spacing\"" markup))
|
|
||||||
(is (re-find #"font-size:10px" markup))
|
|
||||||
(is (re-find #"fill:url\(#fill-0-[^)]+-0\)" markup)
|
|
||||||
"the emphasis mark references its generated per-strip fill")
|
|
||||||
(is (not (re-find #"<foreignObject\b" markup))))))
|
(is (not (re-find #"<foreignObject\b" markup))))))
|
||||||
|
|
||||||
(deftest horizontal-auto-clearance-stacks-emphasis-outside-ruby
|
(deftest horizontal-warichu-svg-draws-each-sub-line-at-half-size
|
||||||
(testing "Static SVG keeps ruby nearest the base and offsets emphasis by another half-em"
|
(testing "Static SVG draws horizontal warichu sub-lines from their top edges"
|
||||||
(let [{:keys [objects shapes]} (setup-text {:width 120 :height 40}
|
(let [{:keys [objects shapes]} (setup-text {:width 120 :height 40}
|
||||||
(horizontal-strip {:text "漢字"
|
(horizontal-strip {:width 40 :height 10 :y 50
|
||||||
:ruby "かんじ"
|
:text "割注" :warichu "warichu"})
|
||||||
:text-emphasis "filled-dot"
|
(horizontal-strip {:width 40 :height 10 :y 60
|
||||||
:annotation-clearance "auto"
|
:text "入り" :warichu "warichu"}))
|
||||||
:annotation-has-ruby true}))
|
|
||||||
markup (svg/generate-markup objects shapes)]
|
|
||||||
(is (re-find #"かんじ" markup))
|
|
||||||
(is (re-find #"••" markup))
|
|
||||||
(is (re-find #"y=\"40\"" markup) "ruby occupies the first half-em layer")
|
|
||||||
(is (re-find #"y=\"30\"" markup) "emphasis occupies the outer half-em layer"))))
|
|
||||||
|
|
||||||
(deftest vertical-warichu-svg-emits-two-sub-columns
|
|
||||||
(testing "Static SVG splits a warichu strip into two half-size sub-columns"
|
|
||||||
(let [{:keys [objects shapes]} (setup-vertical-warichu-text "割注入り")
|
|
||||||
markup (svg/generate-markup objects shapes)]
|
markup (svg/generate-markup objects shapes)]
|
||||||
(is (re-find #"割注" markup))
|
(is (re-find #"割注" markup))
|
||||||
(is (re-find #"入り" markup))
|
(is (re-find #"入り" markup))
|
||||||
(is (not (re-find #"割注入り" markup))
|
|
||||||
"the base text must be split, not drawn as one strip")
|
|
||||||
(is (re-find #"font-size:10px" markup))
|
|
||||||
(is (not (re-find #"<foreignObject\b" markup))))))
|
|
||||||
|
|
||||||
(deftest vertical-warichu-svg-split-respects-kinsoku
|
|
||||||
(testing "The second sub-line must not start with a line-start-prohibited mark"
|
|
||||||
(let [{:keys [objects shapes]} (setup-vertical-warichu-text "割り注、と")
|
|
||||||
markup (svg/generate-markup objects shapes)]
|
|
||||||
(is (re-find #"割り注、" markup)
|
|
||||||
"the comma is pulled up into the first sub-line")
|
|
||||||
(is (not (re-find #"割り注、と" markup))
|
|
||||||
"the strip is still split into two sub-lines"))))
|
|
||||||
|
|
||||||
(deftest vertical-warichu-svg-preserves-non-bmp-characters
|
|
||||||
(testing "Static SVG never splits a surrogate pair between warichu sub-lines"
|
|
||||||
(let [{:keys [objects shapes]} (setup-vertical-warichu-text "割𠀀注😀")
|
|
||||||
markup (svg/generate-markup objects shapes)]
|
|
||||||
(is (re-find #"割𠀀" markup))
|
|
||||||
(is (re-find #"注😀" markup))
|
|
||||||
(is (not (re-find #"<22>" markup))))))
|
|
||||||
|
|
||||||
(deftest horizontal-warichu-svg-emits-two-stacked-sub-lines
|
|
||||||
(testing "Static SVG splits horizontal warichu into top and bottom half-size lines"
|
|
||||||
(let [{:keys [objects shapes]} (setup-text {:width 120 :height 40}
|
|
||||||
(horizontal-strip {:width 40 :text "割注入り" :warichu "warichu"}))
|
|
||||||
markup (svg/generate-markup objects shapes)]
|
|
||||||
(is (re-find #"割注" markup))
|
|
||||||
(is (re-find #"入り" markup))
|
|
||||||
(is (not (re-find #"割注入り" markup)))
|
|
||||||
(is (re-find #"writing-mode:horizontal-tb" markup))
|
(is (re-find #"writing-mode:horizontal-tb" markup))
|
||||||
(is (= 2 (count-matches #"dominant-baseline=.?hanging" markup)))
|
(is (= 2 (count-matches #"dominant-baseline=.?hanging" markup)))
|
||||||
(is (re-find #"font-size:10px" markup)))))
|
(is (= 2 (count-matches #"font-size:10px" markup))))))
|
||||||
|
|
||||||
(deftest vertical-ruby-svg-emits-static-annotation
|
(deftest vertical-ruby-svg-emits-static-annotation
|
||||||
(testing "Static SVG keeps ruby visible without falling back to foreignObject"
|
(testing "Static SVG keeps ruby visible without falling back to foreignObject"
|
||||||
|
|||||||
@ -1750,201 +1750,41 @@ impl RenderState {
|
|||||||
let rebound_text_content =
|
let rebound_text_content =
|
||||||
stored_text_content.paint_content_for_selrect(shape.selrect());
|
stored_text_content.paint_content_for_selrect(shape.selrect());
|
||||||
let text_content = rebound_text_content.as_ref();
|
let text_content = rebound_text_content.as_ref();
|
||||||
let count_inner_strokes = shape.count_visible_inner_strokes();
|
|
||||||
// Erode the main text fill by 1px when there are inner strokes, to avoid a visible seam at the glyph edge.
|
|
||||||
let text_fill_inset = (count_inner_strokes > 0).then(|| 1.0 / self.get_scale());
|
|
||||||
let text_stroke_blur_outset =
|
|
||||||
Stroke::max_bounds_width(shape.visible_strokes(), false);
|
|
||||||
let mut paragraph_builders =
|
|
||||||
text_content.paragraph_builder_group_from_text(None);
|
|
||||||
let stroke_kinds: Vec<StrokeKind> =
|
|
||||||
shape.visible_strokes().rev().map(|s| s.kind).collect();
|
|
||||||
let (mut stroke_paragraphs_list, stroke_opacities): (Vec<_>, Vec<_>) = shape
|
|
||||||
.visible_strokes()
|
|
||||||
.rev()
|
|
||||||
.map(|stroke| {
|
|
||||||
text::stroke_paragraph_builder_group_from_text(
|
|
||||||
text_content,
|
|
||||||
stroke,
|
|
||||||
&shape.selrect(),
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.unzip();
|
|
||||||
if text_content.is_vertical() {
|
if text_content.is_vertical() {
|
||||||
text::render_vertical_text(
|
text::render_vertical_text(
|
||||||
self,
|
self,
|
||||||
&shape,
|
&shape,
|
||||||
text_content,
|
text_content,
|
||||||
&mut paragraph_builders,
|
|
||||||
fills_surface_id,
|
fills_surface_id,
|
||||||
strokes_surface_id,
|
strokes_surface_id,
|
||||||
skip_effects,
|
skip_effects,
|
||||||
)?;
|
)?;
|
||||||
} else if skip_effects {
|
|
||||||
// Fast path: render fills and strokes only (skip shadows/blur).
|
|
||||||
text::render(
|
|
||||||
Some(self),
|
|
||||||
None,
|
|
||||||
&shape,
|
|
||||||
&mut paragraph_builders,
|
|
||||||
Some(fills_surface_id),
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
text_fill_inset,
|
|
||||||
None,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
for (i, (stroke_paragraphs, layer_opacity)) in stroke_paragraphs_list
|
|
||||||
.iter_mut()
|
|
||||||
.zip(stroke_opacities.iter())
|
|
||||||
.enumerate()
|
|
||||||
{
|
|
||||||
if stroke_kinds[i] == StrokeKind::Inner {
|
|
||||||
let mut fill_builders =
|
|
||||||
text_content.paragraph_builder_group_from_text(None);
|
|
||||||
text::render_inner_stroke(
|
|
||||||
Some(self),
|
|
||||||
None,
|
|
||||||
&shape,
|
|
||||||
stroke_paragraphs,
|
|
||||||
&mut fill_builders,
|
|
||||||
Some(strokes_surface_id),
|
|
||||||
None,
|
|
||||||
text_stroke_blur_outset,
|
|
||||||
*layer_opacity,
|
|
||||||
)?;
|
|
||||||
} else if stroke_kinds[i] == StrokeKind::Outer {
|
|
||||||
text::render_outer_stroke(
|
|
||||||
Some(self),
|
|
||||||
None,
|
|
||||||
&shape,
|
|
||||||
stroke_paragraphs,
|
|
||||||
Some(strokes_surface_id),
|
|
||||||
None,
|
|
||||||
text_stroke_blur_outset,
|
|
||||||
*layer_opacity,
|
|
||||||
)?;
|
|
||||||
} else {
|
|
||||||
text::render_with_bounds_outset(
|
|
||||||
Some(self),
|
|
||||||
None,
|
|
||||||
&shape,
|
|
||||||
stroke_paragraphs,
|
|
||||||
Some(strokes_surface_id),
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
text_stroke_blur_outset,
|
|
||||||
None,
|
|
||||||
*layer_opacity,
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if shape.has_visible_strokes() && text_content.has_non_ascii() {
|
|
||||||
let mut emoji_builders = text_content.paragraph_builder_group_opaque();
|
|
||||||
let mut deco_builders =
|
|
||||||
text_content.paragraph_builder_group_from_text(None);
|
|
||||||
text::render_emoji_overlay(
|
|
||||||
self,
|
|
||||||
&shape,
|
|
||||||
&mut emoji_builders,
|
|
||||||
&mut deco_builders,
|
|
||||||
strokes_surface_id,
|
|
||||||
None,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
let shape_scale = self.get_scale();
|
let count_inner_strokes = shape.count_visible_inner_strokes();
|
||||||
let mut drop_shadows = if skip_drop_shadows {
|
// Erode the main text fill by 1px when there are inner strokes, to avoid a visible seam at the glyph edge.
|
||||||
Vec::new()
|
let text_fill_inset =
|
||||||
} else {
|
(count_inner_strokes > 0).then(|| 1.0 / self.get_scale());
|
||||||
|
let text_stroke_blur_outset =
|
||||||
|
Stroke::max_bounds_width(shape.visible_strokes(), false);
|
||||||
|
let mut paragraph_builders =
|
||||||
|
text_content.paragraph_builder_group_from_text(None);
|
||||||
|
let stroke_kinds: Vec<StrokeKind> =
|
||||||
|
shape.visible_strokes().rev().map(|s| s.kind).collect();
|
||||||
|
let (mut stroke_paragraphs_list, stroke_opacities): (Vec<_>, Vec<_>) =
|
||||||
shape
|
shape
|
||||||
.drop_shadows_visible()
|
.visible_strokes()
|
||||||
.filter(|s| s.is_perceptible_at_scale(shape_scale))
|
.rev()
|
||||||
.map(|shadow| {
|
.map(|stroke| {
|
||||||
let mut paint = skia_safe::Paint::default();
|
text::stroke_paragraph_builder_group_from_text(
|
||||||
paint.set_image_filter(shadow.get_drop_shadow_filter());
|
|
||||||
paint
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
};
|
|
||||||
|
|
||||||
if !skip_drop_shadows {
|
|
||||||
if let Some(inherited_shadows) = self.get_inherited_drop_shadows() {
|
|
||||||
drop_shadows.extend(inherited_shadows);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let inner_shadows = shape.inner_shadow_paints();
|
|
||||||
let blur_filter = shape.image_filter(1.);
|
|
||||||
let mut paragraphs_with_shadows =
|
|
||||||
text_content.paragraph_builder_group_from_text(Some(true));
|
|
||||||
let (mut stroke_paragraphs_with_shadows_list, _shadow_opacities): (
|
|
||||||
Vec<_>,
|
|
||||||
Vec<_>,
|
|
||||||
) = shape
|
|
||||||
.visible_strokes()
|
|
||||||
.rev()
|
|
||||||
.map(|stroke| {
|
|
||||||
text::stroke_paragraph_builder_group_from_text(
|
|
||||||
text_content,
|
|
||||||
stroke,
|
|
||||||
&shape.selrect(),
|
|
||||||
Some(true),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.unzip();
|
|
||||||
|
|
||||||
if let Some(parent_shadows) = parent_shadows {
|
|
||||||
if !skip_drop_shadows {
|
|
||||||
if !shape.has_visible_strokes() {
|
|
||||||
for shadow in parent_shadows {
|
|
||||||
text::render(
|
|
||||||
Some(self),
|
|
||||||
None,
|
|
||||||
&shape,
|
|
||||||
&mut paragraphs_with_shadows,
|
|
||||||
text_drop_shadows_surface_id.into(),
|
|
||||||
Some(&shadow),
|
|
||||||
blur_filter.as_ref(),
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
shadows::render_text_shadows(
|
|
||||||
self,
|
|
||||||
&shape,
|
|
||||||
&mut paragraphs_with_shadows,
|
|
||||||
&mut stroke_paragraphs_with_shadows_list,
|
|
||||||
text_drop_shadows_surface_id.into(),
|
|
||||||
&parent_shadows,
|
|
||||||
&blur_filter,
|
|
||||||
&stroke_kinds,
|
|
||||||
text_content,
|
text_content,
|
||||||
)?;
|
stroke,
|
||||||
}
|
&shape.selrect(),
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 1. Text drop shadows
|
|
||||||
if !shape.has_visible_strokes() {
|
|
||||||
for shadow in &drop_shadows {
|
|
||||||
text::render(
|
|
||||||
Some(self),
|
|
||||||
None,
|
None,
|
||||||
&shape,
|
)
|
||||||
&mut paragraphs_with_shadows,
|
})
|
||||||
text_drop_shadows_surface_id.into(),
|
.unzip();
|
||||||
Some(shadow),
|
if skip_effects {
|
||||||
blur_filter.as_ref(),
|
// Fast path: render fills and strokes only (skip shadows/blur).
|
||||||
None,
|
|
||||||
None,
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Text fills
|
|
||||||
text::render(
|
text::render(
|
||||||
Some(self),
|
Some(self),
|
||||||
None,
|
None,
|
||||||
@ -1952,25 +1792,11 @@ impl RenderState {
|
|||||||
&mut paragraph_builders,
|
&mut paragraph_builders,
|
||||||
Some(fills_surface_id),
|
Some(fills_surface_id),
|
||||||
None,
|
None,
|
||||||
blur_filter.as_ref(),
|
None,
|
||||||
text_fill_inset,
|
text_fill_inset,
|
||||||
None,
|
None,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
// 3. Stroke drop shadows
|
|
||||||
shadows::render_text_shadows(
|
|
||||||
self,
|
|
||||||
&shape,
|
|
||||||
&mut paragraphs_with_shadows,
|
|
||||||
&mut stroke_paragraphs_with_shadows_list,
|
|
||||||
text_drop_shadows_surface_id.into(),
|
|
||||||
&drop_shadows,
|
|
||||||
&blur_filter,
|
|
||||||
&stroke_kinds,
|
|
||||||
text_content,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
// 4. Stroke fills
|
|
||||||
for (i, (stroke_paragraphs, layer_opacity)) in stroke_paragraphs_list
|
for (i, (stroke_paragraphs, layer_opacity)) in stroke_paragraphs_list
|
||||||
.iter_mut()
|
.iter_mut()
|
||||||
.zip(stroke_opacities.iter())
|
.zip(stroke_opacities.iter())
|
||||||
@ -1986,7 +1812,7 @@ impl RenderState {
|
|||||||
stroke_paragraphs,
|
stroke_paragraphs,
|
||||||
&mut fill_builders,
|
&mut fill_builders,
|
||||||
Some(strokes_surface_id),
|
Some(strokes_surface_id),
|
||||||
blur_filter.as_ref(),
|
None,
|
||||||
text_stroke_blur_outset,
|
text_stroke_blur_outset,
|
||||||
*layer_opacity,
|
*layer_opacity,
|
||||||
)?;
|
)?;
|
||||||
@ -1997,7 +1823,7 @@ impl RenderState {
|
|||||||
&shape,
|
&shape,
|
||||||
stroke_paragraphs,
|
stroke_paragraphs,
|
||||||
Some(strokes_surface_id),
|
Some(strokes_surface_id),
|
||||||
blur_filter.as_ref(),
|
None,
|
||||||
text_stroke_blur_outset,
|
text_stroke_blur_outset,
|
||||||
*layer_opacity,
|
*layer_opacity,
|
||||||
)?;
|
)?;
|
||||||
@ -2009,7 +1835,7 @@ impl RenderState {
|
|||||||
stroke_paragraphs,
|
stroke_paragraphs,
|
||||||
Some(strokes_surface_id),
|
Some(strokes_surface_id),
|
||||||
None,
|
None,
|
||||||
blur_filter.as_ref(),
|
None,
|
||||||
text_stroke_blur_outset,
|
text_stroke_blur_outset,
|
||||||
None,
|
None,
|
||||||
*layer_opacity,
|
*layer_opacity,
|
||||||
@ -2028,37 +1854,216 @@ impl RenderState {
|
|||||||
&mut emoji_builders,
|
&mut emoji_builders,
|
||||||
&mut deco_builders,
|
&mut deco_builders,
|
||||||
strokes_surface_id,
|
strokes_surface_id,
|
||||||
blur_filter.as_ref(),
|
None,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
let shape_scale = self.get_scale();
|
||||||
|
let mut drop_shadows = if skip_drop_shadows {
|
||||||
|
Vec::new()
|
||||||
|
} else {
|
||||||
|
shape
|
||||||
|
.drop_shadows_visible()
|
||||||
|
.filter(|s| s.is_perceptible_at_scale(shape_scale))
|
||||||
|
.map(|shadow| {
|
||||||
|
let mut paint = skia_safe::Paint::default();
|
||||||
|
paint.set_image_filter(shadow.get_drop_shadow_filter());
|
||||||
|
paint
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
};
|
||||||
|
|
||||||
// 5. Stroke inner shadows
|
if !skip_drop_shadows {
|
||||||
shadows::render_text_shadows(
|
if let Some(inherited_shadows) = self.get_inherited_drop_shadows() {
|
||||||
self,
|
drop_shadows.extend(inherited_shadows);
|
||||||
&shape,
|
}
|
||||||
&mut paragraphs_with_shadows,
|
}
|
||||||
&mut stroke_paragraphs_with_shadows_list,
|
|
||||||
Some(innershadows_surface_id),
|
|
||||||
&inner_shadows,
|
|
||||||
&blur_filter,
|
|
||||||
&stroke_kinds,
|
|
||||||
text_content,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
// 6. Fill Inner shadows
|
let inner_shadows = shape.inner_shadow_paints();
|
||||||
if !shape.has_visible_strokes() {
|
let blur_filter = shape.image_filter(1.);
|
||||||
for shadow in &inner_shadows {
|
let mut paragraphs_with_shadows =
|
||||||
text::render(
|
text_content.paragraph_builder_group_from_text(Some(true));
|
||||||
Some(self),
|
let (mut stroke_paragraphs_with_shadows_list, _shadow_opacities): (
|
||||||
None,
|
Vec<_>,
|
||||||
|
Vec<_>,
|
||||||
|
) = shape
|
||||||
|
.visible_strokes()
|
||||||
|
.rev()
|
||||||
|
.map(|stroke| {
|
||||||
|
text::stroke_paragraph_builder_group_from_text(
|
||||||
|
text_content,
|
||||||
|
stroke,
|
||||||
|
&shape.selrect(),
|
||||||
|
Some(true),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.unzip();
|
||||||
|
|
||||||
|
if let Some(parent_shadows) = parent_shadows {
|
||||||
|
if !skip_drop_shadows {
|
||||||
|
if !shape.has_visible_strokes() {
|
||||||
|
for shadow in parent_shadows {
|
||||||
|
text::render(
|
||||||
|
Some(self),
|
||||||
|
None,
|
||||||
|
&shape,
|
||||||
|
&mut paragraphs_with_shadows,
|
||||||
|
text_drop_shadows_surface_id.into(),
|
||||||
|
Some(&shadow),
|
||||||
|
blur_filter.as_ref(),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
shadows::render_text_shadows(
|
||||||
|
self,
|
||||||
|
&shape,
|
||||||
|
&mut paragraphs_with_shadows,
|
||||||
|
&mut stroke_paragraphs_with_shadows_list,
|
||||||
|
text_drop_shadows_surface_id.into(),
|
||||||
|
&parent_shadows,
|
||||||
|
&blur_filter,
|
||||||
|
&stroke_kinds,
|
||||||
|
text_content,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 1. Text drop shadows
|
||||||
|
if !shape.has_visible_strokes() {
|
||||||
|
for shadow in &drop_shadows {
|
||||||
|
text::render(
|
||||||
|
Some(self),
|
||||||
|
None,
|
||||||
|
&shape,
|
||||||
|
&mut paragraphs_with_shadows,
|
||||||
|
text_drop_shadows_surface_id.into(),
|
||||||
|
Some(shadow),
|
||||||
|
blur_filter.as_ref(),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Text fills
|
||||||
|
text::render(
|
||||||
|
Some(self),
|
||||||
|
None,
|
||||||
|
&shape,
|
||||||
|
&mut paragraph_builders,
|
||||||
|
Some(fills_surface_id),
|
||||||
|
None,
|
||||||
|
blur_filter.as_ref(),
|
||||||
|
text_fill_inset,
|
||||||
|
None,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// 3. Stroke drop shadows
|
||||||
|
shadows::render_text_shadows(
|
||||||
|
self,
|
||||||
|
&shape,
|
||||||
|
&mut paragraphs_with_shadows,
|
||||||
|
&mut stroke_paragraphs_with_shadows_list,
|
||||||
|
text_drop_shadows_surface_id.into(),
|
||||||
|
&drop_shadows,
|
||||||
|
&blur_filter,
|
||||||
|
&stroke_kinds,
|
||||||
|
text_content,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// 4. Stroke fills
|
||||||
|
for (i, (stroke_paragraphs, layer_opacity)) in
|
||||||
|
stroke_paragraphs_list
|
||||||
|
.iter_mut()
|
||||||
|
.zip(stroke_opacities.iter())
|
||||||
|
.enumerate()
|
||||||
|
{
|
||||||
|
if stroke_kinds[i] == StrokeKind::Inner {
|
||||||
|
let mut fill_builders =
|
||||||
|
text_content.paragraph_builder_group_from_text(None);
|
||||||
|
text::render_inner_stroke(
|
||||||
|
Some(self),
|
||||||
|
None,
|
||||||
|
&shape,
|
||||||
|
stroke_paragraphs,
|
||||||
|
&mut fill_builders,
|
||||||
|
Some(strokes_surface_id),
|
||||||
|
blur_filter.as_ref(),
|
||||||
|
text_stroke_blur_outset,
|
||||||
|
*layer_opacity,
|
||||||
|
)?;
|
||||||
|
} else if stroke_kinds[i] == StrokeKind::Outer {
|
||||||
|
text::render_outer_stroke(
|
||||||
|
Some(self),
|
||||||
|
None,
|
||||||
|
&shape,
|
||||||
|
stroke_paragraphs,
|
||||||
|
Some(strokes_surface_id),
|
||||||
|
blur_filter.as_ref(),
|
||||||
|
text_stroke_blur_outset,
|
||||||
|
*layer_opacity,
|
||||||
|
)?;
|
||||||
|
} else {
|
||||||
|
text::render_with_bounds_outset(
|
||||||
|
Some(self),
|
||||||
|
None,
|
||||||
|
&shape,
|
||||||
|
stroke_paragraphs,
|
||||||
|
Some(strokes_surface_id),
|
||||||
|
None,
|
||||||
|
blur_filter.as_ref(),
|
||||||
|
text_stroke_blur_outset,
|
||||||
|
None,
|
||||||
|
*layer_opacity,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if shape.has_visible_strokes() && text_content.has_non_ascii() {
|
||||||
|
let mut emoji_builders =
|
||||||
|
text_content.paragraph_builder_group_opaque();
|
||||||
|
let mut deco_builders =
|
||||||
|
text_content.paragraph_builder_group_from_text(None);
|
||||||
|
text::render_emoji_overlay(
|
||||||
|
self,
|
||||||
&shape,
|
&shape,
|
||||||
&mut paragraphs_with_shadows,
|
&mut emoji_builders,
|
||||||
Some(innershadows_surface_id),
|
&mut deco_builders,
|
||||||
Some(shadow),
|
strokes_surface_id,
|
||||||
blur_filter.as_ref(),
|
blur_filter.as_ref(),
|
||||||
None,
|
);
|
||||||
None,
|
}
|
||||||
)?;
|
|
||||||
|
// 5. Stroke inner shadows
|
||||||
|
shadows::render_text_shadows(
|
||||||
|
self,
|
||||||
|
&shape,
|
||||||
|
&mut paragraphs_with_shadows,
|
||||||
|
&mut stroke_paragraphs_with_shadows_list,
|
||||||
|
Some(innershadows_surface_id),
|
||||||
|
&inner_shadows,
|
||||||
|
&blur_filter,
|
||||||
|
&stroke_kinds,
|
||||||
|
text_content,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// 6. Fill Inner shadows
|
||||||
|
if !shape.has_visible_strokes() {
|
||||||
|
for shadow in &inner_shadows {
|
||||||
|
text::render(
|
||||||
|
Some(self),
|
||||||
|
None,
|
||||||
|
&shape,
|
||||||
|
&mut paragraphs_with_shadows,
|
||||||
|
Some(innershadows_surface_id),
|
||||||
|
Some(shadow),
|
||||||
|
blur_filter.as_ref(),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,66 +16,57 @@ use skia_safe::{
|
|||||||
Canvas, ImageFilter, Paint,
|
Canvas, ImageFilter, Paint,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// 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.
|
||||||
pub fn render_vertical_text(
|
pub fn render_vertical_text(
|
||||||
state: &mut RenderState,
|
state: &mut RenderState,
|
||||||
shape: &Shape,
|
shape: &Shape,
|
||||||
text_content: &TextContent,
|
text_content: &TextContent,
|
||||||
paragraph_builders: &mut [ParagraphBuilderGroup],
|
|
||||||
fills_surface_id: SurfaceId,
|
fills_surface_id: SurfaceId,
|
||||||
strokes_surface_id: SurfaceId,
|
strokes_surface_id: SurfaceId,
|
||||||
skip_effects: bool,
|
skip_effects: bool,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let blur_filter = (!skip_effects).then(|| shape.image_filter(1.0)).flatten();
|
let blur_filter = (!skip_effects).then(|| shape.image_filter(1.0)).flatten();
|
||||||
|
let text_content = text_content.new_bounds(shape.selrect());
|
||||||
let bounds = text_content.bounds();
|
let bounds = text_content.bounds();
|
||||||
|
let vertical_align = shape.vertical_align();
|
||||||
|
let layout = text_vertical::layout_for_box(&text_content, bounds.height());
|
||||||
|
|
||||||
let skip_shadows = skip_effects || state.should_skip_drop_shadows();
|
let skip_shadows = skip_effects || state.should_skip_drop_shadows();
|
||||||
let mut drop_shadows = if skip_shadows {
|
|
||||||
Vec::new()
|
|
||||||
} else {
|
|
||||||
shape.drop_shadow_paints()
|
|
||||||
};
|
|
||||||
if !skip_shadows {
|
if !skip_shadows {
|
||||||
|
let mut drop_shadows = shape.drop_shadow_paints();
|
||||||
if let Some(inherited_shadows) = state.get_inherited_drop_shadows() {
|
if let Some(inherited_shadows) = state.get_inherited_drop_shadows() {
|
||||||
drop_shadows.extend(inherited_shadows);
|
drop_shadows.extend(inherited_shadows);
|
||||||
}
|
}
|
||||||
}
|
if !drop_shadows.is_empty() {
|
||||||
let strokes: Vec<Stroke> = shape.visible_strokes().rev().cloned().collect();
|
let canvas = state.surfaces.canvas_and_mark_dirty(fills_surface_id);
|
||||||
let layout = (!drop_shadows.is_empty() || !strokes.is_empty())
|
for shadow in &drop_shadows {
|
||||||
.then(|| text_vertical::layout_for_box(text_content, bounds.height()));
|
text_vertical::paint_drop_shadow(canvas, &layout, &bounds, vertical_align, shadow);
|
||||||
|
}
|
||||||
if let Some(layout) = layout.as_ref().filter(|_| !drop_shadows.is_empty()) {
|
|
||||||
let canvas = state.surfaces.canvas_and_mark_dirty(fills_surface_id);
|
|
||||||
for shadow in &drop_shadows {
|
|
||||||
text_vertical::paint_drop_shadow(
|
|
||||||
canvas,
|
|
||||||
layout,
|
|
||||||
&bounds,
|
|
||||||
shape.vertical_align(),
|
|
||||||
shadow,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render(
|
render_to_surface(
|
||||||
Some(state),
|
state,
|
||||||
None,
|
|
||||||
shape,
|
shape,
|
||||||
paragraph_builders,
|
fills_surface_id,
|
||||||
Some(fills_surface_id),
|
|
||||||
None,
|
|
||||||
blur_filter.as_ref(),
|
blur_filter.as_ref(),
|
||||||
None,
|
0.0,
|
||||||
None,
|
|canvas| paint_vertical_fill(canvas, shape, &layout, &bounds, blur_filter.as_ref()),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
if let Some(layout) = layout.as_ref().filter(|_| !strokes.is_empty()) {
|
let strokes: Vec<&Stroke> = shape.visible_strokes().rev().collect();
|
||||||
|
if !strokes.is_empty() {
|
||||||
let selrect = shape.selrect();
|
let selrect = shape.selrect();
|
||||||
let canvas = state.surfaces.canvas_and_mark_dirty(strokes_surface_id);
|
let canvas = state.surfaces.canvas_and_mark_dirty(strokes_surface_id);
|
||||||
for stroke in &strokes {
|
for stroke in strokes {
|
||||||
text_vertical::paint_stroke(
|
text_vertical::paint_stroke(
|
||||||
canvas,
|
canvas,
|
||||||
layout,
|
&layout,
|
||||||
&bounds,
|
&bounds,
|
||||||
shape.vertical_align(),
|
vertical_align,
|
||||||
stroke,
|
stroke,
|
||||||
&selrect,
|
&selrect,
|
||||||
blur_filter.as_ref(),
|
blur_filter.as_ref(),
|
||||||
@ -84,16 +75,8 @@ pub fn render_vertical_text(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if state.options.is_text_grid_visible() {
|
if state.options.is_text_grid_visible() {
|
||||||
let owned_layout;
|
|
||||||
let grid_layout = match layout.as_ref() {
|
|
||||||
Some(layout) => layout,
|
|
||||||
None => {
|
|
||||||
owned_layout = text_vertical::layout_for_box(text_content, bounds.height());
|
|
||||||
&owned_layout
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let canvas = state.surfaces.canvas_and_mark_dirty(fills_surface_id);
|
let canvas = state.surfaces.canvas_and_mark_dirty(fills_surface_id);
|
||||||
text_vertical::paint_grid(canvas, grid_layout, &bounds, shape.vertical_align());
|
text_vertical::paint_grid(canvas, &layout, &bounds, vertical_align);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -327,6 +310,70 @@ pub fn render_with_bounds_outset_overlay_emoji(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Run `paint` on `target_surface`. With a blur, it paints into a filter
|
||||||
|
/// surface sized to the blurred text bounds (grown by `stroke_bounds_outset`)
|
||||||
|
/// when those bounds are usable.
|
||||||
|
fn render_to_surface(
|
||||||
|
render_state: &mut RenderState,
|
||||||
|
shape: &Shape,
|
||||||
|
target_surface: SurfaceId,
|
||||||
|
blur: Option<&ImageFilter>,
|
||||||
|
stroke_bounds_outset: f32,
|
||||||
|
mut paint: impl FnMut(&Canvas),
|
||||||
|
) -> Result<()> {
|
||||||
|
if let Some(blur_filter) = blur {
|
||||||
|
let mut text_bounds = shape
|
||||||
|
.get_text_content()
|
||||||
|
.calculate_bounds(shape, false)
|
||||||
|
.to_rect();
|
||||||
|
if stroke_bounds_outset > 0.0 {
|
||||||
|
text_bounds.inset((-stroke_bounds_outset, -stroke_bounds_outset));
|
||||||
|
}
|
||||||
|
let bounds = blur_filter.compute_fast_bounds(text_bounds);
|
||||||
|
if bounds.is_finite()
|
||||||
|
&& bounds.width() > 0.0
|
||||||
|
&& bounds.height() > 0.0
|
||||||
|
&& filters::render_with_filter_surface(
|
||||||
|
render_state,
|
||||||
|
bounds,
|
||||||
|
target_surface,
|
||||||
|
|state, temp_surface| {
|
||||||
|
paint(state.surfaces.canvas(temp_surface));
|
||||||
|
Ok(())
|
||||||
|
},
|
||||||
|
)?
|
||||||
|
{
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
paint(render_state.surfaces.canvas_and_mark_dirty(target_surface));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fill pass of vertical text: `layout` painted inside the shape's layer
|
||||||
|
/// blur.
|
||||||
|
pub fn paint_vertical_fill(
|
||||||
|
canvas: &Canvas,
|
||||||
|
shape: &Shape,
|
||||||
|
layout: &text_vertical::VerticalLayout,
|
||||||
|
bounds: &Rect,
|
||||||
|
blur: Option<&ImageFilter>,
|
||||||
|
) {
|
||||||
|
if let Some(blur_filter) = blur {
|
||||||
|
let mut blur_paint = Paint::default();
|
||||||
|
blur_paint.set_image_filter(blur_filter.clone());
|
||||||
|
canvas.save_layer(
|
||||||
|
&SaveLayerRec::default()
|
||||||
|
.bounds(&shape.layer_bounds())
|
||||||
|
.paint(&blur_paint),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
text_vertical::paint_layout(canvas, layout, bounds, shape.vertical_align());
|
||||||
|
if blur.is_some() {
|
||||||
|
canvas.restore();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn render_with_bounds_outset_inner(
|
fn render_with_bounds_outset_inner(
|
||||||
render_state: Option<&mut RenderState>,
|
render_state: Option<&mut RenderState>,
|
||||||
@ -342,55 +389,25 @@ fn render_with_bounds_outset_inner(
|
|||||||
overlay_emoji: bool,
|
overlay_emoji: bool,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
if let Some(render_state) = render_state {
|
if let Some(render_state) = render_state {
|
||||||
let target_surface = surface_id.unwrap_or(SurfaceId::Fills);
|
return render_to_surface(
|
||||||
|
render_state,
|
||||||
if let Some(blur_filter) = blur {
|
|
||||||
let mut text_bounds = shape
|
|
||||||
.get_text_content()
|
|
||||||
.calculate_bounds(shape, false)
|
|
||||||
.to_rect();
|
|
||||||
if stroke_bounds_outset > 0.0 {
|
|
||||||
text_bounds.inset((-stroke_bounds_outset, -stroke_bounds_outset));
|
|
||||||
}
|
|
||||||
let bounds = blur_filter.compute_fast_bounds(text_bounds);
|
|
||||||
if bounds.is_finite() && bounds.width() > 0.0 && bounds.height() > 0.0 {
|
|
||||||
let blur_filter_clone = blur_filter.clone();
|
|
||||||
if filters::render_with_filter_surface(
|
|
||||||
render_state,
|
|
||||||
bounds,
|
|
||||||
target_surface,
|
|
||||||
|state, temp_surface| {
|
|
||||||
let temp_canvas = state.surfaces.canvas(temp_surface);
|
|
||||||
render_text_on_canvas(
|
|
||||||
temp_canvas,
|
|
||||||
shape,
|
|
||||||
paragraph_builders,
|
|
||||||
shadow,
|
|
||||||
Some(&blur_filter_clone),
|
|
||||||
fill_inset,
|
|
||||||
layer_opacity,
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
Ok(())
|
|
||||||
},
|
|
||||||
)? {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let canvas = render_state.surfaces.canvas_and_mark_dirty(target_surface);
|
|
||||||
render_text_on_canvas(
|
|
||||||
canvas,
|
|
||||||
shape,
|
shape,
|
||||||
paragraph_builders,
|
surface_id.unwrap_or(SurfaceId::Fills),
|
||||||
shadow,
|
|
||||||
blur,
|
blur,
|
||||||
fill_inset,
|
stroke_bounds_outset,
|
||||||
layer_opacity,
|
|canvas| {
|
||||||
false,
|
render_text_on_canvas(
|
||||||
|
canvas,
|
||||||
|
shape,
|
||||||
|
paragraph_builders,
|
||||||
|
shadow,
|
||||||
|
blur,
|
||||||
|
fill_inset,
|
||||||
|
layer_opacity,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
},
|
||||||
);
|
);
|
||||||
return Ok(());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(canvas) = canvas {
|
if let Some(canvas) = canvas {
|
||||||
|
|||||||
@ -274,12 +274,13 @@ fn calculate_cursor_rect(
|
|||||||
|
|
||||||
// Cursor offsets count source characters; the laid-out paragraph
|
// Cursor offsets count source characters; the laid-out paragraph
|
||||||
// indexes the transformed, kinsoku-shifted builder text.
|
// indexes the transformed, kinsoku-shifted builder text.
|
||||||
|
let offsets = crate::shapes::HorizontalOffsets::new(para);
|
||||||
let (cursor_x, cursor_y, cursor_width, cursor_height) = if para_char_count == 0 {
|
let (cursor_x, cursor_y, cursor_width, cursor_height) = if para_char_count == 0 {
|
||||||
// Empty paragraph - use default height
|
// Empty paragraph - use default height
|
||||||
(0.0, 0.0, 1.0, laid_out_para.height())
|
(0.0, 0.0, 1.0, laid_out_para.height())
|
||||||
} else if char_pos == 0 {
|
} else if char_pos == 0 {
|
||||||
let rects = laid_out_para.get_rects_for_range(
|
let rects = laid_out_para.get_rects_for_range(
|
||||||
0..crate::shapes::horizontal_source_to_builder(para, 1),
|
0..offsets.source_to_builder(1),
|
||||||
RectHeightStyle::Max,
|
RectHeightStyle::Max,
|
||||||
RectWidthStyle::Tight,
|
RectWidthStyle::Tight,
|
||||||
);
|
);
|
||||||
@ -290,11 +291,8 @@ fn calculate_cursor_rect(
|
|||||||
(0.0, 0.0, 1.0, laid_out_para.height())
|
(0.0, 0.0, 1.0, laid_out_para.height())
|
||||||
}
|
}
|
||||||
} else if char_pos >= para_char_count {
|
} else if char_pos >= para_char_count {
|
||||||
let last_start = crate::shapes::horizontal_source_to_builder(
|
let last_start = offsets.source_to_builder(para_char_count.saturating_sub(1));
|
||||||
para,
|
let last_end = offsets.source_to_builder(para_char_count);
|
||||||
para_char_count.saturating_sub(1),
|
|
||||||
);
|
|
||||||
let last_end = crate::shapes::horizontal_source_to_builder(para, para_char_count);
|
|
||||||
let rects = laid_out_para.get_rects_for_range(
|
let rects = laid_out_para.get_rects_for_range(
|
||||||
last_start..last_end,
|
last_start..last_end,
|
||||||
RectHeightStyle::Max,
|
RectHeightStyle::Max,
|
||||||
@ -314,8 +312,8 @@ fn calculate_cursor_rect(
|
|||||||
(0.0, 0.0, 1.0, laid_out_para.height())
|
(0.0, 0.0, 1.0, laid_out_para.height())
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let start = crate::shapes::horizontal_source_to_builder(para, char_pos);
|
let start = offsets.source_to_builder(char_pos);
|
||||||
let end = crate::shapes::horizontal_source_to_builder(para, char_pos + 1);
|
let end = offsets.source_to_builder(char_pos + 1);
|
||||||
let rects = laid_out_para.get_rects_for_range(
|
let rects = laid_out_para.get_rects_for_range(
|
||||||
start..end,
|
start..end,
|
||||||
RectHeightStyle::Max,
|
RectHeightStyle::Max,
|
||||||
@ -411,8 +409,8 @@ fn calculate_selection_rects(
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
use skia_safe::textlayout::{RectHeightStyle, RectWidthStyle};
|
use skia_safe::textlayout::{RectHeightStyle, RectWidthStyle};
|
||||||
for builder_range in
|
for builder_range in crate::shapes::HorizontalOffsets::new(para)
|
||||||
crate::shapes::horizontal_normal_selection_ranges(para, range_start, range_end)
|
.normal_selection_ranges(para, range_start, range_end)
|
||||||
{
|
{
|
||||||
for text_box in laid_out_para.get_rects_for_range(
|
for text_box in laid_out_para.get_rects_for_range(
|
||||||
builder_range,
|
builder_range,
|
||||||
|
|||||||
@ -3,8 +3,7 @@ use skia_safe::{self as skia, Canvas, Paint, RRect};
|
|||||||
use crate::error::Result;
|
use crate::error::Result;
|
||||||
use crate::shapes::{
|
use crate::shapes::{
|
||||||
circle_segments_local, merge_fills, radius_to_sigma, rect_segments_local, stroke_to_path,
|
circle_segments_local, merge_fills, radius_to_sigma, rect_segments_local, stroke_to_path,
|
||||||
BlurType, Fill, Frame, ParagraphBuilderGroup, Path, Rect, Shape, Stroke, StrokeKind,
|
BlurType, Fill, Frame, Path, Rect, Shape, Stroke, StrokeKind, StrokeStyle, TextContent, Type,
|
||||||
StrokeStyle, TextContent, Type,
|
|
||||||
};
|
};
|
||||||
use crate::state::ShapesPoolRef;
|
use crate::state::ShapesPoolRef;
|
||||||
use crate::uuid::Uuid;
|
use crate::uuid::Uuid;
|
||||||
@ -58,58 +57,35 @@ impl<'a> VectorRenderer<'a> {
|
|||||||
shape.image_filter(1.)
|
shape.image_filter(1.)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Vertical text: shadows, fill and strokes painted from one layout.
|
||||||
fn draw_vertical_text(
|
fn draw_vertical_text(
|
||||||
&mut self,
|
&mut self,
|
||||||
shape: &Shape,
|
shape: &Shape,
|
||||||
text_content: &TextContent,
|
text_content: &TextContent,
|
||||||
paragraph_builders: &mut [ParagraphBuilderGroup],
|
|
||||||
blur_filter: Option<&skia::ImageFilter>,
|
blur_filter: Option<&skia::ImageFilter>,
|
||||||
) -> Result<()> {
|
) {
|
||||||
use crate::shapes::text_vertical;
|
use crate::shapes::text_vertical;
|
||||||
|
|
||||||
let bounds = text_content.bounds();
|
let bounds = text_content.bounds();
|
||||||
let drop_shadows = shape.drop_shadow_paints();
|
let vertical_align = shape.vertical_align();
|
||||||
let strokes: Vec<&Stroke> = shape.visible_strokes().rev().collect();
|
let layout = text_vertical::layout_for_box(text_content, bounds.height());
|
||||||
let layout = (!drop_shadows.is_empty() || !strokes.is_empty())
|
|
||||||
.then(|| text_vertical::layout_for_box(text_content, bounds.height()));
|
|
||||||
|
|
||||||
if let Some(layout) = &layout {
|
for shadow in &shape.drop_shadow_paints() {
|
||||||
for shadow in &drop_shadows {
|
text_vertical::paint_drop_shadow(self.canvas, &layout, &bounds, vertical_align, shadow);
|
||||||
text_vertical::paint_drop_shadow(
|
|
||||||
self.canvas,
|
|
||||||
layout,
|
|
||||||
&bounds,
|
|
||||||
shape.vertical_align(),
|
|
||||||
shadow,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
text::paint_vertical_fill(self.canvas, shape, &layout, &bounds, blur_filter);
|
||||||
text::render_overlay_emoji(
|
let selrect = shape.selrect();
|
||||||
self.canvas,
|
for stroke in shape.visible_strokes().rev() {
|
||||||
shape,
|
text_vertical::paint_stroke(
|
||||||
paragraph_builders,
|
self.canvas,
|
||||||
None,
|
&layout,
|
||||||
blur_filter,
|
&bounds,
|
||||||
None,
|
vertical_align,
|
||||||
None,
|
stroke,
|
||||||
)?;
|
&selrect,
|
||||||
|
blur_filter,
|
||||||
if let Some(layout) = &layout {
|
);
|
||||||
let selrect = shape.selrect();
|
|
||||||
for stroke in strokes {
|
|
||||||
text_vertical::paint_stroke(
|
|
||||||
self.canvas,
|
|
||||||
layout,
|
|
||||||
&bounds,
|
|
||||||
shape.vertical_align(),
|
|
||||||
stroke,
|
|
||||||
&selrect,
|
|
||||||
blur_filter,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -236,18 +212,15 @@ impl ShapeRenderer for VectorRenderer<'_> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let text_content = text_content.new_bounds(shape.selrect());
|
let text_content = text_content.new_bounds(shape.selrect());
|
||||||
let mut paragraph_builders = text_content.paragraph_builder_group_from_text(None);
|
|
||||||
let blur_filter = self.layer_blur_filter(shape);
|
let blur_filter = self.layer_blur_filter(shape);
|
||||||
|
|
||||||
if text_content.is_vertical() {
|
if text_content.is_vertical() {
|
||||||
return self.draw_vertical_text(
|
self.draw_vertical_text(shape, &text_content, blur_filter.as_ref());
|
||||||
shape,
|
return Ok(());
|
||||||
&text_content,
|
|
||||||
&mut paragraph_builders,
|
|
||||||
blur_filter.as_ref(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut paragraph_builders = text_content.paragraph_builder_group_from_text(None);
|
||||||
|
|
||||||
// Text drop shadows: one filter layer per shadow over fill + stroke
|
// Text drop shadows: one filter layer per shadow over fill + stroke
|
||||||
// silhouettes (mirrors GPU `render_text_shadows`).
|
// silhouettes (mirrors GPU `render_text_shadows`).
|
||||||
let drop_shadows = shape.drop_shadow_paints();
|
let drop_shadows = shape.drop_shadow_paints();
|
||||||
|
|||||||
@ -2168,26 +2168,44 @@ pub fn calculate_text_layout_data(
|
|||||||
// Ranges are in the builder-text (kinsoku-shifted)
|
// Ranges are in the builder-text (kinsoku-shifted)
|
||||||
// space; exported positions are translated back to
|
// space; exported positions are translated back to
|
||||||
// original span-relative offsets through the map.
|
// original span-relative offsets through the map.
|
||||||
let (_, offset_map) = text_para.layout_span_texts();
|
let offsets = HorizontalOffsets::new(text_para);
|
||||||
let span_ranges = horizontal_span_ranges(text_para);
|
let offset_map = &offsets.offset_map;
|
||||||
|
let entry =
|
||||||
|
|span: usize, range: std::ops::Range<usize>, mut rect: Rect, direction| {
|
||||||
|
rect.offset((x, current_y));
|
||||||
|
PositionData {
|
||||||
|
paragraph: paragraph_index as u32,
|
||||||
|
span: span as u32,
|
||||||
|
start_pos: range.start as u32,
|
||||||
|
end_pos: range.end as u32,
|
||||||
|
x: rect.x(),
|
||||||
|
y: rect.y(),
|
||||||
|
width: rect.width(),
|
||||||
|
height: rect.height(),
|
||||||
|
direction,
|
||||||
|
}
|
||||||
|
};
|
||||||
let placeholder_rects = para_layout.paragraph.get_rects_for_placeholders();
|
let placeholder_rects = para_layout.paragraph.get_rects_for_placeholders();
|
||||||
let mut placeholder_index = 0usize;
|
let mut placeholder_index = 0usize;
|
||||||
for range in span_ranges {
|
for range in &offsets.ranges {
|
||||||
if range.warichu {
|
if range.warichu {
|
||||||
if let Some(textbox) = placeholder_rects.get(placeholder_index) {
|
// One strip per sub-line: the top half holds the
|
||||||
let mut rect = textbox.rect;
|
// first line, the bottom half the second.
|
||||||
rect.offset((x, current_y));
|
if let (Some(textbox), Some(span)) = (
|
||||||
position_data.push(PositionData {
|
placeholder_rects.get(placeholder_index),
|
||||||
paragraph: paragraph_index as u32,
|
text_para.children().get(range.span),
|
||||||
span: range.span as u32,
|
) {
|
||||||
start_pos: 0,
|
let rect = textbox.rect;
|
||||||
end_pos: (range.source_end - range.source_start) as u32,
|
let text = span.apply_text_transform();
|
||||||
x: rect.x(),
|
let split = warichu_text_lines(&text).0.encode_utf16().count();
|
||||||
y: rect.y(),
|
let end = range.source_end - range.source_start;
|
||||||
width: rect.width(),
|
let half = rect.height() / 2.0;
|
||||||
height: rect.height(),
|
let ltr = direction_to_int(TextDirection::LTR);
|
||||||
direction: direction_to_int(TextDirection::LTR),
|
let top = Rect::from_xywh(rect.x(), rect.y(), rect.width(), half);
|
||||||
});
|
let bottom =
|
||||||
|
Rect::from_xywh(rect.x(), rect.y() + half, rect.width(), half);
|
||||||
|
position_data.push(entry(range.span, 0..split, top, ltr));
|
||||||
|
position_data.push(entry(range.span, split..end, bottom, ltr));
|
||||||
}
|
}
|
||||||
placeholder_index += 1;
|
placeholder_index += 1;
|
||||||
continue;
|
continue;
|
||||||
@ -2201,7 +2219,7 @@ pub fn calculate_text_layout_data(
|
|||||||
|
|
||||||
for textbox in rects {
|
for textbox in rects {
|
||||||
let direction = textbox.direct;
|
let direction = textbox.direct;
|
||||||
let mut rect = textbox.rect;
|
let rect = textbox.rect;
|
||||||
let cy = rect.top + rect.height() / 2.0;
|
let cy = rect.top + rect.height() / 2.0;
|
||||||
|
|
||||||
// Get byte positions from Skia's transformed text layout
|
// Get byte positions from Skia's transformed text layout
|
||||||
@ -2225,18 +2243,24 @@ pub fn calculate_text_layout_data(
|
|||||||
.position as usize,
|
.position as usize,
|
||||||
) - orig_span_start;
|
) - orig_span_start;
|
||||||
|
|
||||||
rect.offset((x, current_y));
|
position_data.push(entry(
|
||||||
position_data.push(PositionData {
|
range.span,
|
||||||
paragraph: paragraph_index as u32,
|
start_pos..end_pos,
|
||||||
span: range.span as u32,
|
rect,
|
||||||
start_pos: start_pos as u32,
|
direction_to_int(direction),
|
||||||
end_pos: end_pos as u32,
|
));
|
||||||
x: rect.x(),
|
}
|
||||||
y: rect.y(),
|
}
|
||||||
width: rect.width(),
|
for placement in
|
||||||
height: rect.height(),
|
horizontal_emphasis_placements(text_para, &offsets, ¶_layout.paragraph)
|
||||||
direction: direction_to_int(direction),
|
{
|
||||||
});
|
if let Some(span) = text_para.children().get(placement.span) {
|
||||||
|
position_data.push(entry(
|
||||||
|
placement.span,
|
||||||
|
placement.range.clone(),
|
||||||
|
horizontal_emphasis_mark_box(span, &placement),
|
||||||
|
super::text_vertical::DIRECTION_EMPHASIS_MARK,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2457,7 +2481,10 @@ mod tests {
|
|||||||
let para = test_paragraph(&["Añadir"]);
|
let para = test_paragraph(&["Añadir"]);
|
||||||
for offset in 0..=6 {
|
for offset in 0..=6 {
|
||||||
assert_eq!(para.char_offset_to_utf16(offset), offset);
|
assert_eq!(para.char_offset_to_utf16(offset), offset);
|
||||||
assert_eq!(horizontal_source_to_builder(¶, offset), offset);
|
assert_eq!(
|
||||||
|
HorizontalOffsets::new(¶).source_to_builder(offset),
|
||||||
|
offset
|
||||||
|
);
|
||||||
assert_eq!(horizontal_builder_to_source(¶, offset), offset);
|
assert_eq!(horizontal_builder_to_source(¶, offset), offset);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2491,9 +2518,9 @@ mod tests {
|
|||||||
|
|
||||||
assert_eq!(para.char_offset_to_utf16(4), 4);
|
assert_eq!(para.char_offset_to_utf16(4), 4);
|
||||||
assert_eq!(para.char_offset_to_utf16(6), 7);
|
assert_eq!(para.char_offset_to_utf16(6), 7);
|
||||||
assert_eq!(horizontal_source_to_builder(¶, 4), 4);
|
assert_eq!(HorizontalOffsets::new(¶).source_to_builder(4), 4);
|
||||||
assert_eq!(horizontal_source_to_builder(¶, 5), 6);
|
assert_eq!(HorizontalOffsets::new(¶).source_to_builder(5), 6);
|
||||||
assert_eq!(horizontal_source_to_builder(¶, 6), 7);
|
assert_eq!(HorizontalOffsets::new(¶).source_to_builder(6), 7);
|
||||||
assert_eq!(horizontal_builder_to_source(¶, 7), 6);
|
assert_eq!(horizontal_builder_to_source(¶, 7), 6);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2501,8 +2528,8 @@ mod tests {
|
|||||||
fn builder_range_covers_the_whole_glyph() {
|
fn builder_range_covers_the_whole_glyph() {
|
||||||
let para = test_paragraph(&["a😀b"]);
|
let para = test_paragraph(&["a😀b"]);
|
||||||
let len_at = |offset| {
|
let len_at = |offset| {
|
||||||
horizontal_source_to_builder(¶, offset + 1)
|
HorizontalOffsets::new(¶).source_to_builder(offset + 1)
|
||||||
- horizontal_source_to_builder(¶, offset)
|
- HorizontalOffsets::new(¶).source_to_builder(offset)
|
||||||
};
|
};
|
||||||
assert_eq!(len_at(0), 1);
|
assert_eq!(len_at(0), 1);
|
||||||
assert_eq!(len_at(1), 2);
|
assert_eq!(len_at(1), 2);
|
||||||
|
|||||||
@ -112,11 +112,134 @@ pub(crate) struct HorizontalSpanRange {
|
|||||||
pub style_anchor_start: usize,
|
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.
|
||||||
|
pub(crate) struct HorizontalOffsets {
|
||||||
|
/// Map between the transformed text and the kinsoku-adjusted text.
|
||||||
|
pub(crate) offset_map: kinsoku::OffsetMap,
|
||||||
|
pub(crate) ranges: Vec<HorizontalSpanRange>,
|
||||||
|
/// Transformed-text UTF-16 offset of every source character boundary.
|
||||||
|
boundaries: Vec<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HorizontalOffsets {
|
||||||
|
pub(crate) fn new(paragraph: &Paragraph) -> Self {
|
||||||
|
let (span_texts, offset_map) = paragraph.layout_span_texts();
|
||||||
|
let ranges = span_ranges(paragraph, span_texts, &offset_map);
|
||||||
|
Self {
|
||||||
|
offset_map,
|
||||||
|
ranges,
|
||||||
|
boundaries: source_char_boundaries(paragraph),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builder-text UTF-16 offset of a paragraph source character offset.
|
||||||
|
pub(crate) fn source_to_builder(&self, source_char_offset: usize) -> usize {
|
||||||
|
let boundaries = &self.boundaries;
|
||||||
|
let source_utf16 = boundaries
|
||||||
|
.get(source_char_offset)
|
||||||
|
.copied()
|
||||||
|
.unwrap_or_else(|| boundaries.last().copied().unwrap_or(0));
|
||||||
|
let Some(range) = self
|
||||||
|
.ranges
|
||||||
|
.iter()
|
||||||
|
.find(|range| source_utf16 >= range.source_start && source_utf16 <= range.source_end)
|
||||||
|
else {
|
||||||
|
return self
|
||||||
|
.ranges
|
||||||
|
.last()
|
||||||
|
.map(|range| range.builder_end)
|
||||||
|
.unwrap_or(0);
|
||||||
|
};
|
||||||
|
if range.warichu {
|
||||||
|
return if source_utf16 >= range.source_end {
|
||||||
|
range.builder_end
|
||||||
|
} else {
|
||||||
|
range.builder_start
|
||||||
|
};
|
||||||
|
}
|
||||||
|
let shifted = self.offset_map.to_shifted(source_utf16);
|
||||||
|
range.builder_start + shifted.saturating_sub(range.shifted_start)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Paragraph source character offset of a builder-text UTF-16 offset.
|
||||||
|
pub(crate) fn builder_to_source(&self, builder_offset: usize) -> usize {
|
||||||
|
let source_utf16 = self
|
||||||
|
.ranges
|
||||||
|
.iter()
|
||||||
|
.find(|range| {
|
||||||
|
builder_offset >= range.builder_start && builder_offset <= range.builder_end
|
||||||
|
})
|
||||||
|
.map(|range| {
|
||||||
|
if range.warichu {
|
||||||
|
if builder_offset > range.builder_start {
|
||||||
|
range.source_end
|
||||||
|
} else {
|
||||||
|
range.source_start
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let within = builder_offset
|
||||||
|
.saturating_sub(range.builder_start)
|
||||||
|
.min(range.builder_end - range.builder_start);
|
||||||
|
self.offset_map.to_original(range.shifted_start + within)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
self.ranges
|
||||||
|
.last()
|
||||||
|
.map(|range| range.source_end)
|
||||||
|
.unwrap_or(0)
|
||||||
|
});
|
||||||
|
let boundaries = &self.boundaries;
|
||||||
|
boundaries
|
||||||
|
.partition_point(|boundary| *boundary < source_utf16)
|
||||||
|
.min(boundaries.len().saturating_sub(1))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builder-text ranges of the selected source characters
|
||||||
|
/// `[source_start, source_end)` outside warichu spans.
|
||||||
|
pub(crate) fn normal_selection_ranges(
|
||||||
|
&self,
|
||||||
|
paragraph: &Paragraph,
|
||||||
|
source_start: usize,
|
||||||
|
source_end: usize,
|
||||||
|
) -> Vec<std::ops::Range<usize>> {
|
||||||
|
let mut span_start = 0usize;
|
||||||
|
paragraph
|
||||||
|
.children()
|
||||||
|
.iter()
|
||||||
|
.filter_map(|span| {
|
||||||
|
let span_end = span_start + span.text.chars().count();
|
||||||
|
let selected_start = source_start.max(span_start);
|
||||||
|
let selected_end = source_end.min(span_end);
|
||||||
|
span_start = span_end;
|
||||||
|
if span.is_warichu() || selected_start >= selected_end {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(self.source_to_builder(selected_start)..self.source_to_builder(selected_end))
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn horizontal_span_ranges(paragraph: &Paragraph) -> Vec<HorizontalSpanRange> {
|
||||||
|
HorizontalOffsets::new(paragraph).ranges
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn horizontal_builder_to_source(paragraph: &Paragraph, builder_offset: usize) -> usize {
|
||||||
|
HorizontalOffsets::new(paragraph).builder_to_source(builder_offset)
|
||||||
|
}
|
||||||
|
|
||||||
/// Ranges shared by layout, position-data and editor mapping. `shifted_*`
|
/// Ranges shared by layout, position-data and editor mapping. `shifted_*`
|
||||||
/// addresses the normal kinsoku-adjusted paragraph text, while `builder_*`
|
/// addresses the normal kinsoku-adjusted paragraph text, while `builder_*`
|
||||||
/// addresses the paragraph where a whole warichu span occupies one U+FFFC.
|
/// addresses the paragraph where a whole warichu span occupies one U+FFFC.
|
||||||
pub(crate) fn horizontal_span_ranges(paragraph: &Paragraph) -> Vec<HorizontalSpanRange> {
|
fn span_ranges(
|
||||||
let (span_texts, offset_map) = paragraph.layout_span_texts();
|
paragraph: &Paragraph,
|
||||||
|
span_texts: Vec<String>,
|
||||||
|
offset_map: &kinsoku::OffsetMap,
|
||||||
|
) -> Vec<HorizontalSpanRange> {
|
||||||
let mut builder_cursor = 0usize;
|
let mut builder_cursor = 0usize;
|
||||||
let mut builder_byte_cursor = 0usize;
|
let mut builder_byte_cursor = 0usize;
|
||||||
let mut shifted_cursor = 0usize;
|
let mut shifted_cursor = 0usize;
|
||||||
@ -188,61 +311,6 @@ fn source_char_boundaries(paragraph: &Paragraph) -> Vec<usize> {
|
|||||||
boundaries
|
boundaries
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn horizontal_source_to_builder(
|
|
||||||
paragraph: &Paragraph,
|
|
||||||
source_char_offset: usize,
|
|
||||||
) -> usize {
|
|
||||||
let boundaries = source_char_boundaries(paragraph);
|
|
||||||
let source_utf16 = boundaries
|
|
||||||
.get(source_char_offset)
|
|
||||||
.copied()
|
|
||||||
.unwrap_or_else(|| boundaries.last().copied().unwrap_or(0));
|
|
||||||
let (_, offset_map) = paragraph.layout_span_texts();
|
|
||||||
let ranges = horizontal_span_ranges(paragraph);
|
|
||||||
let Some(range) = ranges
|
|
||||||
.iter()
|
|
||||||
.find(|range| source_utf16 >= range.source_start && source_utf16 <= range.source_end)
|
|
||||||
else {
|
|
||||||
return ranges.last().map(|range| range.builder_end).unwrap_or(0);
|
|
||||||
};
|
|
||||||
if range.warichu {
|
|
||||||
return if source_utf16 >= range.source_end {
|
|
||||||
range.builder_end
|
|
||||||
} else {
|
|
||||||
range.builder_start
|
|
||||||
};
|
|
||||||
}
|
|
||||||
let shifted = offset_map.to_shifted(source_utf16);
|
|
||||||
range.builder_start + shifted.saturating_sub(range.shifted_start)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn horizontal_builder_to_source(paragraph: &Paragraph, builder_offset: usize) -> usize {
|
|
||||||
let (_, offset_map) = paragraph.layout_span_texts();
|
|
||||||
let ranges = horizontal_span_ranges(paragraph);
|
|
||||||
let source_utf16 = ranges
|
|
||||||
.iter()
|
|
||||||
.find(|range| builder_offset >= range.builder_start && builder_offset <= range.builder_end)
|
|
||||||
.map(|range| {
|
|
||||||
if range.warichu {
|
|
||||||
if builder_offset > range.builder_start {
|
|
||||||
range.source_end
|
|
||||||
} else {
|
|
||||||
range.source_start
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let within = builder_offset
|
|
||||||
.saturating_sub(range.builder_start)
|
|
||||||
.min(range.builder_end - range.builder_start);
|
|
||||||
offset_map.to_original(range.shifted_start + within)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.unwrap_or_else(|| ranges.last().map(|range| range.source_end).unwrap_or(0));
|
|
||||||
let boundaries = source_char_boundaries(paragraph);
|
|
||||||
boundaries
|
|
||||||
.partition_point(|boundary| *boundary < source_utf16)
|
|
||||||
.min(boundaries.len().saturating_sub(1))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Placeholder rect of each horizontal warichu span, keyed by span index.
|
/// Placeholder rect of each horizontal warichu span, keyed by span index.
|
||||||
/// Tabs in normal spans are placeholders too, so they are skipped.
|
/// Tabs in normal spans are placeholders too, so they are skipped.
|
||||||
fn horizontal_warichu_placeholders(
|
fn horizontal_warichu_placeholders(
|
||||||
@ -377,32 +445,6 @@ pub(crate) fn horizontal_warichu_range_rects(
|
|||||||
rects
|
rects
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn horizontal_normal_selection_ranges(
|
|
||||||
paragraph: &Paragraph,
|
|
||||||
source_start: usize,
|
|
||||||
source_end: usize,
|
|
||||||
) -> Vec<std::ops::Range<usize>> {
|
|
||||||
let mut span_start = 0usize;
|
|
||||||
paragraph
|
|
||||||
.children()
|
|
||||||
.iter()
|
|
||||||
.filter_map(|span| {
|
|
||||||
let span_end = span_start + span.text.chars().count();
|
|
||||||
let selected_start = source_start.max(span_start);
|
|
||||||
let selected_end = source_end.min(span_end);
|
|
||||||
let warichu = span.is_warichu();
|
|
||||||
span_start = span_end;
|
|
||||||
if warichu || selected_start >= selected_end {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
Some(
|
|
||||||
horizontal_source_to_builder(paragraph, selected_start)
|
|
||||||
..horizontal_source_to_builder(paragraph, selected_end),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Char index where a warichu run splits into its two sub-lines: the
|
/// Char index where a warichu run splits into its two sub-lines: the
|
||||||
/// balanced midpoint (first line longer), nudged so the second sub-line
|
/// 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 start with a line-start-prohibited character and the first
|
||||||
@ -499,10 +541,13 @@ pub(crate) fn emphasis_char_allowed(character: char) -> bool {
|
|||||||
&& !crate::shapes::japanese::classify(character).is_emphasis_prohibited()
|
&& !crate::shapes::japanese::classify(character).is_emphasis_prohibited()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone)]
|
||||||
pub(crate) struct HorizontalEmphasisPlacement {
|
pub(crate) struct HorizontalEmphasisPlacement {
|
||||||
pub(crate) span: usize,
|
pub(crate) span: usize,
|
||||||
|
/// UTF-16 range of the base character in the span's transformed text.
|
||||||
|
pub(crate) range: std::ops::Range<usize>,
|
||||||
pub(crate) mark: char,
|
pub(crate) mark: char,
|
||||||
|
/// Base character rect in the laid-out paragraph.
|
||||||
pub(crate) rect: skia::Rect,
|
pub(crate) rect: skia::Rect,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -511,13 +556,13 @@ pub(crate) struct HorizontalEmphasisPlacement {
|
|||||||
/// character range keeps the marks attached to the actual laid-out glyphs.
|
/// character range keeps the marks attached to the actual laid-out glyphs.
|
||||||
pub(crate) fn horizontal_emphasis_placements(
|
pub(crate) fn horizontal_emphasis_placements(
|
||||||
paragraph: &Paragraph,
|
paragraph: &Paragraph,
|
||||||
|
offsets: &HorizontalOffsets,
|
||||||
laid_out: &skia::textlayout::Paragraph,
|
laid_out: &skia::textlayout::Paragraph,
|
||||||
) -> Vec<HorizontalEmphasisPlacement> {
|
) -> Vec<HorizontalEmphasisPlacement> {
|
||||||
let (_, offset_map) = paragraph.layout_span_texts();
|
let offset_map = &offsets.offset_map;
|
||||||
let ranges = horizontal_span_ranges(paragraph);
|
|
||||||
let mut placements = Vec::new();
|
let mut placements = Vec::new();
|
||||||
|
|
||||||
for range in ranges.iter().filter(|range| !range.warichu) {
|
for range in offsets.ranges.iter().filter(|range| !range.warichu) {
|
||||||
let Some(span) = paragraph.children().get(range.span) else {
|
let Some(span) = paragraph.children().get(range.span) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
@ -550,6 +595,7 @@ pub(crate) fn horizontal_emphasis_placements(
|
|||||||
if let Some(rect) = scalar_rect {
|
if let Some(rect) = scalar_rect {
|
||||||
placements.push(HorizontalEmphasisPlacement {
|
placements.push(HorizontalEmphasisPlacement {
|
||||||
span: range.span,
|
span: range.span,
|
||||||
|
range: local_utf16..next_utf16,
|
||||||
mark,
|
mark,
|
||||||
rect,
|
rect,
|
||||||
});
|
});
|
||||||
@ -588,6 +634,23 @@ pub(crate) fn horizontal_annotation_over_top(rect: skia::Rect, font_size: f32) -
|
|||||||
rect.top.max(rect.bottom - font_size) - font_size * (3.0 / 14.0)
|
rect.top.max(rect.bottom - font_size) - font_size * (3.0 / 14.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Em box of a horizontal emphasis mark in the laid-out paragraph: centred
|
||||||
|
/// over its base character, outside any stacked ruby layer.
|
||||||
|
pub(crate) fn horizontal_emphasis_mark_box(
|
||||||
|
span: &TextSpan,
|
||||||
|
placement: &HorizontalEmphasisPlacement,
|
||||||
|
) -> skia::Rect {
|
||||||
|
let size = span.font_size * EMPHASIS_FONT_SCALE;
|
||||||
|
let bottom = horizontal_annotation_over_top(placement.rect, span.font_size)
|
||||||
|
- span.emphasis_ruby_offset();
|
||||||
|
skia::Rect::from_xywh(
|
||||||
|
placement.rect.center_x() - size / 2.0,
|
||||||
|
bottom - size,
|
||||||
|
size,
|
||||||
|
size,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/// Paint horizontal emphasis marks (圏点 / bouten) above their base glyphs.
|
/// Paint horizontal emphasis marks (圏点 / bouten) above their base glyphs.
|
||||||
/// The base paragraph retains its normal metrics; interlinear collision and
|
/// The base paragraph retains its normal metrics; interlinear collision and
|
||||||
/// automatic line-gap expansion remain a separate layout policy.
|
/// automatic line-gap expansion remain a separate layout policy.
|
||||||
@ -598,9 +661,9 @@ pub(crate) fn paint_horizontal_emphasis(
|
|||||||
x: f32,
|
x: f32,
|
||||||
y: f32,
|
y: f32,
|
||||||
) {
|
) {
|
||||||
let ranges = horizontal_span_ranges(paragraph);
|
let offsets = HorizontalOffsets::new(paragraph);
|
||||||
let placements = horizontal_emphasis_placements(paragraph, laid_out);
|
let placements = horizontal_emphasis_placements(paragraph, &offsets, laid_out);
|
||||||
for range in ranges.iter().filter(|range| !range.warichu) {
|
for range in offsets.ranges.iter().filter(|range| !range.warichu) {
|
||||||
let Some(span) = paragraph.children().get(range.span) else {
|
let Some(span) = paragraph.children().get(range.span) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
@ -1162,15 +1225,15 @@ mod tests {
|
|||||||
let ranges = horizontal_span_ranges(¶graph);
|
let ranges = horizontal_span_ranges(¶graph);
|
||||||
assert_eq!(ranges[0].builder_start..ranges[0].builder_end, 0..3);
|
assert_eq!(ranges[0].builder_start..ranges[0].builder_end, 0..3);
|
||||||
assert_eq!(ranges[1].builder_start..ranges[1].builder_end, 3..4);
|
assert_eq!(ranges[1].builder_start..ranges[1].builder_end, 3..4);
|
||||||
assert_eq!(horizontal_source_to_builder(¶graph, 2), 0);
|
assert_eq!(HorizontalOffsets::new(¶graph).source_to_builder(2), 0);
|
||||||
assert_eq!(horizontal_source_to_builder(¶graph, 4), 3);
|
assert_eq!(HorizontalOffsets::new(¶graph).source_to_builder(4), 3);
|
||||||
assert_eq!(horizontal_source_to_builder(¶graph, 5), 4);
|
assert_eq!(HorizontalOffsets::new(¶graph).source_to_builder(5), 4);
|
||||||
assert_eq!(horizontal_builder_to_source(¶graph, 1), 4);
|
assert_eq!(horizontal_builder_to_source(¶graph, 1), 4);
|
||||||
assert_eq!(horizontal_builder_to_source(¶graph, 2), 4);
|
assert_eq!(horizontal_builder_to_source(¶graph, 2), 4);
|
||||||
assert_eq!(horizontal_builder_to_source(¶graph, 3), 4);
|
assert_eq!(horizontal_builder_to_source(¶graph, 3), 4);
|
||||||
assert_eq!(horizontal_builder_to_source(¶graph, 4), 5);
|
assert_eq!(horizontal_builder_to_source(¶graph, 4), 5);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
horizontal_normal_selection_ranges(¶graph, 1, 5),
|
HorizontalOffsets::new(¶graph).normal_selection_ranges(¶graph, 1, 5),
|
||||||
vec![3..4]
|
vec![3..4]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -1180,9 +1243,9 @@ mod tests {
|
|||||||
init_state();
|
init_state();
|
||||||
let paragraph = make_paragraph(vec![make_span("😀A", 0.0)], 0.0);
|
let paragraph = make_paragraph(vec![make_span("😀A", 0.0)], 0.0);
|
||||||
|
|
||||||
assert_eq!(horizontal_source_to_builder(¶graph, 1), 2);
|
assert_eq!(HorizontalOffsets::new(¶graph).source_to_builder(1), 2);
|
||||||
assert_eq!(horizontal_builder_to_source(¶graph, 2), 1);
|
assert_eq!(horizontal_builder_to_source(¶graph, 2), 1);
|
||||||
assert_eq!(horizontal_source_to_builder(¶graph, 2), 3);
|
assert_eq!(HorizontalOffsets::new(¶graph).source_to_builder(2), 3);
|
||||||
assert_eq!(horizontal_builder_to_source(¶graph, 3), 2);
|
assert_eq!(horizontal_builder_to_source(¶graph, 3), 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1255,6 +1318,65 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn horizontal_position_data_carries_warichu_lines_and_emphasis_marks() {
|
||||||
|
init_state();
|
||||||
|
let mut emphasized = make_span("A、B", 0.0);
|
||||||
|
emphasized.text_emphasis = TextEmphasis::FilledDot;
|
||||||
|
let mut warichu = make_span("割注入り", 0.0);
|
||||||
|
warichu.warichu = true;
|
||||||
|
let mut content = super::super::text::TextContent::new(
|
||||||
|
crate::math::Rect::from_xywh(0.0, 0.0, 400.0, 100.0),
|
||||||
|
crate::shapes::GrowType::Fixed,
|
||||||
|
);
|
||||||
|
content.add_paragraph(make_paragraph(vec![emphasized, warichu], 0.0));
|
||||||
|
let mut shape = crate::shapes::Shape::new(Uuid::nil());
|
||||||
|
shape.set_selrect(0.0, 0.0, 400.0, 100.0);
|
||||||
|
let mut resources =
|
||||||
|
crate::render::RenderResources::try_new_headless().expect("headless resources");
|
||||||
|
let _guard = crate::globals::TestRenderResourcesGuard::install(&mut resources);
|
||||||
|
|
||||||
|
let data = super::super::text::calculate_position_data(&shape, &content, false);
|
||||||
|
|
||||||
|
let warichu_lines: Vec<(u32, u32)> = data
|
||||||
|
.iter()
|
||||||
|
.filter(|entry| entry.span == 1)
|
||||||
|
.map(|entry| (entry.start_pos, entry.end_pos))
|
||||||
|
.collect();
|
||||||
|
assert_eq!(warichu_lines, vec![(0, 2), (2, 4)]);
|
||||||
|
let marks: Vec<(u32, u32)> = data
|
||||||
|
.iter()
|
||||||
|
.filter(|entry| entry.direction == super::super::text_vertical::DIRECTION_EMPHASIS_MARK)
|
||||||
|
.map(|entry| (entry.start_pos, entry.end_pos))
|
||||||
|
.collect();
|
||||||
|
assert_eq!(marks, vec![(0, 1), (2, 3)], "A and B are marked; 、 is not");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn horizontal_emphasis_mark_stacks_outside_auto_clearance_ruby() {
|
||||||
|
let placement = HorizontalEmphasisPlacement {
|
||||||
|
span: 0,
|
||||||
|
range: 0..1,
|
||||||
|
mark: '•',
|
||||||
|
rect: skia::Rect::from_xywh(10.0, 40.0, 16.0, 20.0),
|
||||||
|
};
|
||||||
|
let plain = make_span("漢", 0.0);
|
||||||
|
let mut stacked = make_span("漢", 0.0);
|
||||||
|
stacked.ruby = "かん".to_string();
|
||||||
|
stacked.annotation_clearance = AnnotationClearance::Auto;
|
||||||
|
|
||||||
|
let plain_box = horizontal_emphasis_mark_box(&plain, &placement);
|
||||||
|
let stacked_box = horizontal_emphasis_mark_box(&stacked, &placement);
|
||||||
|
|
||||||
|
assert_eq!(plain_box.width(), plain.font_size * EMPHASIS_FONT_SCALE);
|
||||||
|
assert_eq!(plain_box.center_x(), placement.rect.center_x());
|
||||||
|
assert_eq!(
|
||||||
|
stacked_box.bottom,
|
||||||
|
plain_box.bottom - stacked.ruby_font_size(),
|
||||||
|
"the mark sits outside the ruby layer"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn horizontal_emphasis_tracks_eligible_unicode_characters() {
|
fn horizontal_emphasis_tracks_eligible_unicode_characters() {
|
||||||
init_state();
|
init_state();
|
||||||
@ -1274,7 +1396,11 @@ mod tests {
|
|||||||
let mut laid_out = builder.build();
|
let mut laid_out = builder.build();
|
||||||
laid_out.layout(200.0);
|
laid_out.layout(200.0);
|
||||||
|
|
||||||
let placements = horizontal_emphasis_placements(¶graph, &laid_out);
|
let placements = horizontal_emphasis_placements(
|
||||||
|
¶graph,
|
||||||
|
&HorizontalOffsets::new(¶graph),
|
||||||
|
&laid_out,
|
||||||
|
);
|
||||||
assert_eq!(placements.len(), 3, "A, emoji and B receive one mark each");
|
assert_eq!(placements.len(), 3, "A, emoji and B receive one mark each");
|
||||||
assert!(placements
|
assert!(placements
|
||||||
.iter()
|
.iter()
|
||||||
|
|||||||
@ -12,7 +12,7 @@ use crate::shapes::{
|
|||||||
|
|
||||||
use super::cells::Fonts;
|
use super::cells::Fonts;
|
||||||
use super::flow::FlowCell;
|
use super::flow::FlowCell;
|
||||||
use super::layout::{column_base_center, CellKind, VerticalCell, VerticalColumn};
|
use super::layout::{column_base_center, CellKind, VerticalCell, VerticalColumn, VerticalLayout};
|
||||||
use super::shaping::{shape_segment, shape_segment_with_fallbacks, span_font_families, ShapedRun};
|
use super::shaping::{shape_segment, shape_segment_with_fallbacks, span_font_families, ShapedRun};
|
||||||
|
|
||||||
/// One shaped ruby glyph, retaining its fallback-font run and source range.
|
/// One shaped ruby glyph, retaining its fallback-font run and source range.
|
||||||
@ -49,17 +49,15 @@ pub struct RubyCell {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// One emphasis mark (圏点 / bouten) drawn beside a base character. Kept out of
|
/// One emphasis mark (圏点 / bouten) drawn beside a base character. Kept out of
|
||||||
/// `cells` like ruby, so base metrics, caret and position-data never see it.
|
/// `cells` like ruby, so base metrics and caret geometry never see it. The
|
||||||
/// The mark glyph is the single-glyph `run`; it is centred on the base cell's
|
/// mark glyph is the single-glyph `run`; it is centred on its base cell's flow
|
||||||
/// flow extent and drawn in the column's right-side gutter.
|
/// extent and drawn in the column's right-side gutter with the cell's paint.
|
||||||
pub struct EmphasisMark {
|
pub struct EmphasisMark {
|
||||||
pub run: usize,
|
pub run: usize,
|
||||||
pub column: usize,
|
/// Index of the annotated base cell in `VerticalLayout::cells`.
|
||||||
/// Flow-axis top and extent of the annotated base cell.
|
pub cell: usize,
|
||||||
pub top: f32,
|
|
||||||
pub extent: f32,
|
|
||||||
pub paint: usize,
|
|
||||||
pub font_size: f32,
|
pub font_size: f32,
|
||||||
|
/// Cross-axis offset past a stacked ruby layer.
|
||||||
pub outside_offset: f32,
|
pub outside_offset: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -488,6 +486,7 @@ pub(super) fn layout_emphasis(
|
|||||||
text_content: &TextContent,
|
text_content: &TextContent,
|
||||||
cells: &[VerticalCell],
|
cells: &[VerticalCell],
|
||||||
span_utf16_starts: &[Vec<usize>],
|
span_utf16_starts: &[Vec<usize>],
|
||||||
|
span_transforms: &[Vec<AppliedTextTransform>],
|
||||||
fonts: &Fonts,
|
fonts: &Fonts,
|
||||||
) -> (Vec<ShapedRun>, Vec<EmphasisMark>) {
|
) -> (Vec<ShapedRun>, Vec<EmphasisMark>) {
|
||||||
let mut emphasis_runs: Vec<ShapedRun> = Vec::new();
|
let mut emphasis_runs: Vec<ShapedRun> = Vec::new();
|
||||||
@ -525,7 +524,9 @@ pub(super) fn layout_emphasis(
|
|||||||
let run_index = emphasis_runs.len();
|
let run_index = emphasis_runs.len();
|
||||||
emphasis_runs.push(shaped.remove(0));
|
emphasis_runs.push(shaped.remove(0));
|
||||||
let span_start = span_utf16_starts[paragraph_index][span_index];
|
let span_start = span_utf16_starts[paragraph_index][span_index];
|
||||||
for cell in cells {
|
// Cell offsets index the span's transformed text.
|
||||||
|
let span_text = &span_transforms[paragraph_index][span_index].text;
|
||||||
|
for (cell_index, cell) in cells.iter().enumerate() {
|
||||||
if cell.paragraph != paragraph_index
|
if cell.paragraph != paragraph_index
|
||||||
|| cell.span != span_index
|
|| cell.span != span_index
|
||||||
|| !matches!(
|
|| !matches!(
|
||||||
@ -533,7 +534,7 @@ pub(super) fn layout_emphasis(
|
|||||||
CellKind::Upright { .. } | CellKind::SyntheticRotated { .. }
|
CellKind::Upright { .. } | CellKind::SyntheticRotated { .. }
|
||||||
)
|
)
|
||||||
|| !utf16_range_allows_emphasis(
|
|| !utf16_range_allows_emphasis(
|
||||||
&span.text,
|
span_text,
|
||||||
cell.start - span_start,
|
cell.start - span_start,
|
||||||
cell.end - span_start,
|
cell.end - span_start,
|
||||||
)
|
)
|
||||||
@ -542,10 +543,7 @@ pub(super) fn layout_emphasis(
|
|||||||
}
|
}
|
||||||
emphasis_marks.push(EmphasisMark {
|
emphasis_marks.push(EmphasisMark {
|
||||||
run: run_index,
|
run: run_index,
|
||||||
column: cell.column,
|
cell: cell_index,
|
||||||
top: cell.top,
|
|
||||||
extent: cell.extent,
|
|
||||||
paint: cell.paint,
|
|
||||||
font_size: mark_font_size,
|
font_size: mark_font_size,
|
||||||
outside_offset: span.emphasis_ruby_offset(),
|
outside_offset: span.emphasis_ruby_offset(),
|
||||||
});
|
});
|
||||||
@ -555,10 +553,16 @@ pub(super) fn layout_emphasis(
|
|||||||
(emphasis_runs, emphasis_marks)
|
(emphasis_runs, emphasis_marks)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Cross-axis centre of an emphasis mark's gutter, past any stacked ruby.
|
/// Centre of an emphasis mark, relative to the layout's content origin: in
|
||||||
pub(super) fn emphasis_gutter_center(column: &VerticalColumn, mark: &EmphasisMark) -> f32 {
|
/// its column's gutter past any stacked ruby, at the middle of its base cell.
|
||||||
|
pub(super) fn emphasis_mark_center(layout: &VerticalLayout, mark: &EmphasisMark) -> (f32, f32) {
|
||||||
|
let cell = &layout.cells[mark.cell];
|
||||||
let base_font_size = mark.font_size / EMPHASIS_FONT_SCALE;
|
let base_font_size = mark.font_size / EMPHASIS_FONT_SCALE;
|
||||||
column_base_center(column) + base_font_size / 2.0 + mark.outside_offset + mark.font_size / 2.0
|
let x = column_base_center(&layout.columns[cell.column])
|
||||||
|
+ base_font_size / 2.0
|
||||||
|
+ mark.outside_offset
|
||||||
|
+ mark.font_size / 2.0;
|
||||||
|
(x, cell.top + cell.extent / 2.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@ -568,7 +572,9 @@ mod tests {
|
|||||||
use super::super::test_support::*;
|
use super::super::test_support::*;
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::shapes::{AnnotationClearance, GrowType, RubySize};
|
use crate::shapes::{AnnotationClearance, GrowType, RubySize};
|
||||||
use crate::shapes::{TextAlign, TextEmphasis, TextOrientation, TextSpan, VerticalAlign};
|
use crate::shapes::{
|
||||||
|
TextAlign, TextEmphasis, TextOrientation, TextSpan, TextTransform, VerticalAlign,
|
||||||
|
};
|
||||||
|
|
||||||
fn emphasis_content(base: &str, emphasis: TextEmphasis) -> TextContent {
|
fn emphasis_content(base: &str, emphasis: TextEmphasis) -> TextContent {
|
||||||
spans_content(
|
spans_content(
|
||||||
@ -650,6 +656,25 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn emphasis_follows_the_transformed_text() {
|
||||||
|
// Uppercase expands ß into SS, so the transformed cells S S 、 A are
|
||||||
|
// offset from the source text ß、A.
|
||||||
|
let content = spans_content(
|
||||||
|
vec![TextSpan {
|
||||||
|
text_emphasis: TextEmphasis::FilledDot,
|
||||||
|
text_orientation: TextOrientation::Upright,
|
||||||
|
text_transform: Some(TextTransform::Uppercase),
|
||||||
|
..make_span("ß、A")
|
||||||
|
}],
|
||||||
|
400.0,
|
||||||
|
);
|
||||||
|
let layout = layout_content(&content, 400.0);
|
||||||
|
assert_eq!(layout.cells.len(), 4);
|
||||||
|
let marked: Vec<usize> = layout.emphasis_marks.iter().map(|mark| mark.cell).collect();
|
||||||
|
assert_eq!(marked, vec![0, 1, 3], "S, S and A are marked; 、 is not");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn no_emphasis_emits_no_marks() {
|
fn no_emphasis_emits_no_marks() {
|
||||||
let content = make_content(&["AB"], 400.0);
|
let content = make_content(&["AB"], 400.0);
|
||||||
|
|||||||
@ -463,8 +463,13 @@ pub fn layout_vertical(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let (ruby_runs, ruby_cells) = layout_ruby(text_content, &cells, &fonts);
|
let (ruby_runs, ruby_cells) = layout_ruby(text_content, &cells, &fonts);
|
||||||
let (emphasis_runs, emphasis_marks) =
|
let (emphasis_runs, emphasis_marks) = layout_emphasis(
|
||||||
layout_emphasis(text_content, &cells, &span_utf16_starts, &fonts);
|
text_content,
|
||||||
|
&cells,
|
||||||
|
&span_utf16_starts,
|
||||||
|
&span_transforms,
|
||||||
|
&fonts,
|
||||||
|
);
|
||||||
|
|
||||||
let height = cells
|
let height = cells
|
||||||
.iter()
|
.iter()
|
||||||
|
|||||||
@ -46,7 +46,10 @@ mod test_support;
|
|||||||
pub(crate) use annotations::distribute_ruby_tops;
|
pub(crate) use annotations::distribute_ruby_tops;
|
||||||
pub use layout::{block_axis_offset, layout_for_box, measure_content, VerticalLayout};
|
pub use layout::{block_axis_offset, layout_for_box, measure_content, VerticalLayout};
|
||||||
pub use paint::{
|
pub use paint::{
|
||||||
paint_drop_shadow, paint_grid, paint_stroke, paint_text_vertical, vertical_text_paths,
|
paint_drop_shadow, paint_grid, paint_layout, paint_stroke, paint_text_vertical,
|
||||||
|
vertical_text_paths,
|
||||||
|
};
|
||||||
|
pub use positions::{
|
||||||
|
caret_from_point, caret_rect, intersects, position_data, range_rects, DIRECTION_EMPHASIS_MARK,
|
||||||
};
|
};
|
||||||
pub use positions::{caret_from_point, caret_rect, intersects, position_data, range_rects};
|
|
||||||
pub(crate) use shaping::{shape_segment_with_fallbacks, single_glyph_blob, span_font_families};
|
pub(crate) use shaping::{shape_segment_with_fallbacks, single_glyph_blob, span_font_families};
|
||||||
|
|||||||
@ -7,7 +7,7 @@ use skia_safe::{
|
|||||||
use crate::math::Rect;
|
use crate::math::Rect;
|
||||||
use crate::shapes::{Stroke, StrokeKind, TextContent, VerticalAlign};
|
use crate::shapes::{Stroke, StrokeKind, TextContent, VerticalAlign};
|
||||||
|
|
||||||
use super::annotations::{emphasis_gutter_center, ruby_strip_x, EmphasisMark, RubyCell};
|
use super::annotations::{emphasis_mark_center, ruby_strip_x, EmphasisMark, RubyCell};
|
||||||
use super::font_tables::upright_baseline;
|
use super::font_tables::upright_baseline;
|
||||||
use super::layout::{column_base_center, layout_for_box, CellKind, VerticalCell, VerticalLayout};
|
use super::layout::{column_base_center, layout_for_box, CellKind, VerticalCell, VerticalLayout};
|
||||||
use super::shaping::{single_glyph_blob, ShapedRun};
|
use super::shaping::{single_glyph_blob, ShapedRun};
|
||||||
@ -222,10 +222,10 @@ fn emphasis_draw(
|
|||||||
) -> Option<GlyphDraw> {
|
) -> Option<GlyphDraw> {
|
||||||
let run = &layout.emphasis_runs[mark.run];
|
let run = &layout.emphasis_runs[mark.run];
|
||||||
let blob = single_glyph_blob(&run.font, *run.glyphs.first()?)?;
|
let blob = single_glyph_blob(&run.font, *run.glyphs.first()?)?;
|
||||||
let gutter_center = origin.0 + emphasis_gutter_center(&layout.columns[mark.column], mark);
|
let (center_x, center_y) = emphasis_mark_center(layout, mark);
|
||||||
|
let gutter_center = origin.0 + center_x;
|
||||||
let (_, metrics) = run.font.metrics();
|
let (_, metrics) = run.font.metrics();
|
||||||
let cell_center = origin.1 + mark.top + mark.extent / 2.0;
|
let baseline = origin.1 + center_y - (metrics.ascent + metrics.descent) / 2.0;
|
||||||
let baseline = cell_center - (metrics.ascent + metrics.descent) / 2.0;
|
|
||||||
let advance = run.advances.first().copied().unwrap_or(0.0);
|
let advance = run.advances.first().copied().unwrap_or(0.0);
|
||||||
Some(GlyphDraw::at(
|
Some(GlyphDraw::at(
|
||||||
blob,
|
blob,
|
||||||
@ -254,7 +254,7 @@ fn decoration_rects(
|
|||||||
|
|
||||||
/// Fill pass: each cell drawn with its own fill paint and its decorations,
|
/// Fill pass: each cell drawn with its own fill paint and its decorations,
|
||||||
/// then ruby and emphasis marks with their base cell's paint.
|
/// then ruby and emphasis marks with their base cell's paint.
|
||||||
fn paint_layout(
|
pub fn paint_layout(
|
||||||
canvas: &Canvas,
|
canvas: &Canvas,
|
||||||
layout: &VerticalLayout,
|
layout: &VerticalLayout,
|
||||||
bounds: &Rect,
|
bounds: &Rect,
|
||||||
@ -280,7 +280,7 @@ fn paint_layout(
|
|||||||
}
|
}
|
||||||
for mark in &layout.emphasis_marks {
|
for mark in &layout.emphasis_marks {
|
||||||
if let Some(draw) = emphasis_draw(layout, mark, origin) {
|
if let Some(draw) = emphasis_draw(layout, mark, origin) {
|
||||||
draw.draw(canvas, &layout.paints[mark.paint]);
|
draw.draw(canvas, &layout.paints[layout.cells[mark.cell].paint]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -353,7 +353,7 @@ fn paths_from_layout(
|
|||||||
push_text_path(
|
push_text_path(
|
||||||
&mut paths,
|
&mut paths,
|
||||||
draw.into_path(),
|
draw.into_path(),
|
||||||
&layout.paints[mark.paint],
|
&layout.paints[layout.cells[mark.cell].paint],
|
||||||
antialias,
|
antialias,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,7 +5,7 @@ use skia_safe::{Contains, Point as SkPoint};
|
|||||||
use crate::math::Rect;
|
use crate::math::Rect;
|
||||||
use crate::shapes::{PositionData, VerticalAlign};
|
use crate::shapes::{PositionData, VerticalAlign};
|
||||||
|
|
||||||
use super::annotations::ruby_strip_x;
|
use super::annotations::{emphasis_mark_center, ruby_strip_x};
|
||||||
use super::layout::{column_base_center, CellKind, VerticalCell, VerticalLayout};
|
use super::layout::{column_base_center, CellKind, VerticalCell, VerticalLayout};
|
||||||
|
|
||||||
/// Position-data `direction` value marking a vertical (vertical-rl)
|
/// Position-data `direction` value marking a vertical (vertical-rl)
|
||||||
@ -19,15 +19,76 @@ pub const DIRECTION_VERTICAL_RL: u32 = 2;
|
|||||||
/// exact gutter placement the canvas paints.
|
/// exact gutter placement the canvas paints.
|
||||||
pub const DIRECTION_VERTICAL_RUBY: u32 = 3;
|
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.
|
||||||
|
pub const DIRECTION_EMPHASIS_MARK: u32 = 4;
|
||||||
|
|
||||||
|
/// Paragraph source UTF-16 range of a `transformed` span-text range.
|
||||||
|
fn source_utf16_range(
|
||||||
|
layout: &VerticalLayout,
|
||||||
|
paragraph: usize,
|
||||||
|
span: usize,
|
||||||
|
transformed: Range<usize>,
|
||||||
|
) -> Range<usize> {
|
||||||
|
let transformed_span_start = layout.span_utf16_starts[paragraph][span];
|
||||||
|
let source_span_start = layout.span_source_utf16_starts[paragraph][span];
|
||||||
|
let relative = layout.span_transforms[paragraph][span].source_utf16_range(
|
||||||
|
transformed.start - transformed_span_start..transformed.end - transformed_span_start,
|
||||||
|
);
|
||||||
|
source_span_start + relative.start..source_span_start + relative.end
|
||||||
|
}
|
||||||
|
|
||||||
pub(super) fn cell_source_utf16_range(
|
pub(super) fn cell_source_utf16_range(
|
||||||
layout: &VerticalLayout,
|
layout: &VerticalLayout,
|
||||||
cell: &VerticalCell,
|
cell: &VerticalCell,
|
||||||
) -> Range<usize> {
|
) -> Range<usize> {
|
||||||
let transformed_span_start = layout.span_utf16_starts[cell.paragraph][cell.span];
|
source_utf16_range(layout, cell.paragraph, cell.span, cell.start..cell.end)
|
||||||
let source_span_start = layout.span_source_utf16_starts[cell.paragraph][cell.span];
|
}
|
||||||
let relative = layout.span_transforms[cell.paragraph][cell.span]
|
|
||||||
.source_utf16_range(cell.start - transformed_span_start..cell.end - transformed_span_start);
|
/// Entry of `cell`'s span covering the paragraph source range `source`.
|
||||||
source_span_start + relative.start..source_span_start + relative.end
|
fn span_entry(
|
||||||
|
layout: &VerticalLayout,
|
||||||
|
cell: &VerticalCell,
|
||||||
|
source: Range<usize>,
|
||||||
|
(x, y, width, height): (f32, f32, f32, f32),
|
||||||
|
direction: u32,
|
||||||
|
) -> PositionData {
|
||||||
|
let span_start = layout.span_source_utf16_starts[cell.paragraph][cell.span];
|
||||||
|
PositionData {
|
||||||
|
paragraph: cell.paragraph as u32,
|
||||||
|
span: cell.span as u32,
|
||||||
|
start_pos: (source.start - span_start) as u32,
|
||||||
|
end_pos: (source.end - span_start) as u32,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
direction,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The two sub-line strips of a warichu cell: the first on the right half of
|
||||||
|
/// the base band, the second on the left, each holding its own characters.
|
||||||
|
fn warichu_entries(
|
||||||
|
layout: &VerticalLayout,
|
||||||
|
cell: &VerticalCell,
|
||||||
|
first_chars: usize,
|
||||||
|
origin: (f32, f32),
|
||||||
|
) -> [PositionData; 2] {
|
||||||
|
let column = &layout.columns[cell.column];
|
||||||
|
let center = origin.0 + column_base_center(column);
|
||||||
|
let half = cell.font_size / 2.0;
|
||||||
|
let split = cell.start + first_chars;
|
||||||
|
let line = |range: Range<usize>, x: f32| {
|
||||||
|
let source = source_utf16_range(layout, cell.paragraph, cell.span, range);
|
||||||
|
let rect = (x, origin.1 + cell.top, half, cell.extent);
|
||||||
|
span_entry(layout, cell, source, rect, DIRECTION_VERTICAL_RL)
|
||||||
|
};
|
||||||
|
[
|
||||||
|
line(cell.start..split, center),
|
||||||
|
line(split..cell.end, center - half),
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Position-data entries for the v2 editor / exports: consecutive cells of
|
/// Position-data entries for the v2 editor / exports: consecutive cells of
|
||||||
@ -43,6 +104,16 @@ pub fn position_data(
|
|||||||
let mut i = 0;
|
let mut i = 0;
|
||||||
while i < layout.cells.len() {
|
while i < layout.cells.len() {
|
||||||
let first = &layout.cells[i];
|
let first = &layout.cells[i];
|
||||||
|
if let CellKind::Warichu { first_chars, .. } = first.kind {
|
||||||
|
result.extend(warichu_entries(
|
||||||
|
layout,
|
||||||
|
first,
|
||||||
|
first_chars,
|
||||||
|
(origin_x, origin_y),
|
||||||
|
));
|
||||||
|
i += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
let mut source_range = cell_source_utf16_range(layout, first);
|
let mut source_range = cell_source_utf16_range(layout, first);
|
||||||
let mut bottom = first.top + first.extent;
|
let mut bottom = first.top + first.extent;
|
||||||
let mut j = i + 1;
|
let mut j = i + 1;
|
||||||
@ -51,6 +122,7 @@ pub fn position_data(
|
|||||||
if next.paragraph == first.paragraph
|
if next.paragraph == first.paragraph
|
||||||
&& next.span == first.span
|
&& next.span == first.span
|
||||||
&& next.column == first.column
|
&& next.column == first.column
|
||||||
|
&& !matches!(next.kind, CellKind::Warichu { .. })
|
||||||
{
|
{
|
||||||
let next_source = cell_source_utf16_range(layout, next);
|
let next_source = cell_source_utf16_range(layout, next);
|
||||||
source_range.start = source_range.start.min(next_source.start);
|
source_range.start = source_range.start.min(next_source.start);
|
||||||
@ -62,20 +134,21 @@ pub fn position_data(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
let column = &layout.columns[first.column];
|
let column = &layout.columns[first.column];
|
||||||
let span_start = layout.span_source_utf16_starts[first.paragraph][first.span];
|
// Base text occupies the base sub-band; any ruby gutter is excluded
|
||||||
result.push(PositionData {
|
// so the editor overlay and selection track the glyphs.
|
||||||
paragraph: first.paragraph as u32,
|
let rect = (
|
||||||
span: first.span as u32,
|
origin_x + column.x,
|
||||||
start_pos: (source_range.start - span_start) as u32,
|
origin_y + first.top,
|
||||||
end_pos: (source_range.end - span_start) as u32,
|
column.base_width,
|
||||||
x: origin_x + column.x,
|
bottom - first.top,
|
||||||
y: origin_y + first.top,
|
);
|
||||||
// Base text occupies the base sub-band; any ruby gutter is
|
result.push(span_entry(
|
||||||
// excluded so the editor overlay and selection track the glyphs.
|
layout,
|
||||||
width: column.base_width,
|
first,
|
||||||
height: bottom - first.top,
|
source_range,
|
||||||
direction: DIRECTION_VERTICAL_RL,
|
rect,
|
||||||
});
|
DIRECTION_VERTICAL_RL,
|
||||||
|
));
|
||||||
i = j;
|
i = j;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -106,6 +179,26 @@ pub fn position_data(
|
|||||||
direction: DIRECTION_VERTICAL_RUBY,
|
direction: DIRECTION_VERTICAL_RUBY,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for mark in &layout.emphasis_marks {
|
||||||
|
let cell = &layout.cells[mark.cell];
|
||||||
|
let (center_x, center_y) = emphasis_mark_center(layout, mark);
|
||||||
|
let size = mark.font_size;
|
||||||
|
let rect = (
|
||||||
|
origin_x + center_x - size / 2.0,
|
||||||
|
origin_y + center_y - size / 2.0,
|
||||||
|
size,
|
||||||
|
size,
|
||||||
|
);
|
||||||
|
let source = cell_source_utf16_range(layout, cell);
|
||||||
|
result.push(span_entry(
|
||||||
|
layout,
|
||||||
|
cell,
|
||||||
|
source,
|
||||||
|
rect,
|
||||||
|
DIRECTION_EMPHASIS_MARK,
|
||||||
|
));
|
||||||
|
}
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -380,7 +473,8 @@ mod tests {
|
|||||||
use super::super::test_support::*;
|
use super::super::test_support::*;
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::shapes::{
|
use crate::shapes::{
|
||||||
TextCombineUpright, TextOrientation, TextPositionWithAffinity, TextTransform,
|
TextCombineUpright, TextEmphasis, TextOrientation, TextPositionWithAffinity, TextSpan,
|
||||||
|
TextTransform,
|
||||||
};
|
};
|
||||||
use crate::wasm::text::helpers as text_helpers;
|
use crate::wasm::text::helpers as text_helpers;
|
||||||
|
|
||||||
@ -436,6 +530,62 @@ mod tests {
|
|||||||
assert!(rect.height() < 0.01 || rect.top >= cell.top + cell.extent - 0.01);
|
assert!(rect.height() < 0.01 || rect.top >= cell.top + cell.extent - 0.01);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn position_data_emits_one_strip_per_warichu_sub_line() {
|
||||||
|
let content = warichu_content("あいう、えお", 400.0);
|
||||||
|
let layout = layout_content(&content, 400.0);
|
||||||
|
let data = position_data(&layout, &content.bounds(), VerticalAlign::Top);
|
||||||
|
|
||||||
|
let strips: Vec<&PositionData> = data
|
||||||
|
.iter()
|
||||||
|
.filter(|entry| entry.direction == DIRECTION_VERTICAL_RL)
|
||||||
|
.collect();
|
||||||
|
assert_eq!(strips.len(), 2);
|
||||||
|
assert_eq!((strips[0].start_pos, strips[0].end_pos), (0, 4));
|
||||||
|
assert_eq!((strips[1].start_pos, strips[1].end_pos), (4, 6));
|
||||||
|
assert!(
|
||||||
|
strips[0].x > strips[1].x,
|
||||||
|
"the first sub-line reads first, on the right"
|
||||||
|
);
|
||||||
|
assert!((strips[0].width - EM / 2.0).abs() < 0.01);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn position_data_emits_each_emphasis_mark_box() {
|
||||||
|
let content = spans_content(
|
||||||
|
vec![TextSpan {
|
||||||
|
text_emphasis: TextEmphasis::FilledDot,
|
||||||
|
text_orientation: TextOrientation::Upright,
|
||||||
|
..make_span("A、B")
|
||||||
|
}],
|
||||||
|
400.0,
|
||||||
|
);
|
||||||
|
let layout = layout_content(&content, 400.0);
|
||||||
|
let data = position_data(&layout, &content.bounds(), VerticalAlign::Top);
|
||||||
|
|
||||||
|
let marks: Vec<&PositionData> = data
|
||||||
|
.iter()
|
||||||
|
.filter(|entry| entry.direction == DIRECTION_EMPHASIS_MARK)
|
||||||
|
.collect();
|
||||||
|
let ranges: Vec<(u32, u32)> = marks.iter().map(|m| (m.start_pos, m.end_pos)).collect();
|
||||||
|
assert_eq!(
|
||||||
|
ranges,
|
||||||
|
vec![(0, 1), (2, 3)],
|
||||||
|
"A and B are marked; 、 is not"
|
||||||
|
);
|
||||||
|
let base = data
|
||||||
|
.iter()
|
||||||
|
.find(|entry| entry.direction == DIRECTION_VERTICAL_RL)
|
||||||
|
.expect("base strip");
|
||||||
|
for mark in marks {
|
||||||
|
assert!((mark.width - EM / 2.0).abs() < 0.01);
|
||||||
|
assert!(
|
||||||
|
mark.x >= base.x + base.width - 0.01,
|
||||||
|
"marks sit in the gutter right of the base band"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn warichu_cell_carries_kinsoku_split() {
|
fn warichu_cell_carries_kinsoku_split() {
|
||||||
let content = warichu_content("あいう、えお", 400.0);
|
let content = warichu_content("あいう、えお", 400.0);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user