🐛 Fix font height problem also in the font dropdown

This commit is contained in:
Luis de Dios 2026-08-28 23:21:18 +02:00
parent 036b30e75c
commit 55a40ddcd3
2 changed files with 95 additions and 70 deletions

View File

@ -90,66 +90,72 @@
(constantly nil))))) (constantly nil)))))
@loaded?)) @loaded?))
;; --- OPTICAL CENTERING OF THE SAMPLE -------------------------------------- ;; --- OPTICAL CENTERING OF SAMPLE TEXT --------------------------------------
;; Fonts with exaggerated vertical metrics (huge ascender/descender, small ;; Fonts with exaggerated vertical metrics (huge ascender/descender, small
;; caps) render their line box lower within the fixed-height row, so a plain ;; caps) render their line box lower within a fixed-height row, so a plain
;; `align-items: center` leaves the "Ag" sample sitting low. We measure the ;; `align-items: center` leaves the visible glyphs sitting low. We measure the
;; font-wide vs glyph-ink bounding boxes once per font and shift the sample by ;; font-wide vs glyph-ink bounding boxes once per font/sample and shift the
;; the computed offset so the visible glyphs are optically centered. ;; text by the computed offset so the visible glyphs are optically centered.
;; The offset is expressed in `em`, which makes it size-independent: the same
;; measurement corrects both the 16px `Ag` sample and the smaller font-name
;; labels in the font selector.
(defonce ^:private sample-offset-cache (atom {})) (defonce ^:private optical-offset-cache (atom {}))
(defn- sample-offset-key [family weight style] (defn- optical-offset-key [family weight style text]
(dm/str family "|" weight "|" style)) (dm/str family "|" weight "|" style "|" text))
(defn- sample-offset-for (defn- optical-offset-em
"Vertical shift (px) that centers the `Ag` ink within the sample box. "Vertical shift (in `em` units, i.e. relative to the font size) that centers
the ink of `text` within a single line box.
For a single centered line the shift reduces to the difference between the For a centered line the shift reduces to the difference between the font-wide
font-wide and ink bounding boxes: and ink bounding boxes:
dy = ((ink-ascent - font-ascent) + (font-descent - ink-descent)) / 2." dy = ((ink-ascent - font-ascent) + (font-descent - ink-descent)) / 2.
[family weight style] Measuring at 16px and dividing the pixel shift by it yields the `em` value."
[family weight style text]
(when-some [{:keys [font-ascent font-descent ink-ascent ink-descent]} (when-some [{:keys [font-ascent font-descent ink-ascent ink-descent]}
(dom/measure-text-metrics family weight style)] (dom/measure-text-metrics family weight style text 16)]
(let [dy (/ (+ (- ink-ascent font-ascent) (let [dy (/ (+ (- ink-ascent font-ascent)
(- font-descent ink-descent)) (- font-descent ink-descent))
2)] 2)
em (/ dy 16)]
;; Round to avoid float noise leaking into the transform string. ;; Round to avoid float noise leaking into the transform string.
(/ (js/Math.round (* dy 1000)) 1000)))) (/ (js/Math.round (* em 10000)) 10000))))
(defn- load-sample-offset (defn- load-optical-offset
[font-id family weight style] [font-id family weight style text]
(let [key (sample-offset-key family weight style)] (let [key (optical-offset-key family weight style text)]
(if-let [cached (get @sample-offset-cache key)] (if-let [cached (get @optical-offset-cache key)]
(p/resolved cached) (p/resolved cached)
(-> (fonts/ensure-loaded! font-id) (-> (fonts/ensure-loaded! font-id)
(p/then (p/then
(fn [_] (fn [_]
(let [dy (or (sample-offset-for family weight style) 0)] (let [em (or (optical-offset-em family weight style text) 0)]
(swap! sample-offset-cache assoc key dy) (swap! optical-offset-cache assoc key em)
dy))))))) em)))))))
(defn- use-sample-offset (defn- use-optical-offset
"Lazily resolve the optical-centering offset for a typography sample, "Lazily resolve the optical-centering offset (in `em`) for sample text in a
measuring once per font and caching it. Falls back to 0 when the font isn't given font, measuring once per font/sample and caching it. Falls back to 0
available or the metrics can't be measured." when the font isn't available or the metrics can't be measured."
[font-id family weight style] [font-id family weight style text]
(let [offset* (mf/use-state 0)] (let [offset* (mf/use-state 0)]
(mf/use-effect (mf/use-effect
(mf/deps font-id family weight style) (mf/deps font-id family weight style text)
(fn [] (fn []
(let [cancelled? (volatile! false) (let [cancelled? (volatile! false)
key (sample-offset-key family weight style)] key (optical-offset-key family weight style text)]
(if (contains? @sample-offset-cache key) (if (contains? @optical-offset-cache key)
(reset! offset* (get @sample-offset-cache key)) (reset! offset* (get @optical-offset-cache key))
(let [task (tm/schedule-on-idle (let [task (tm/schedule-on-idle
(fn [] (fn []
(-> (load-sample-offset font-id family weight style) (-> (load-optical-offset font-id family weight style text)
(p/then (p/then
(fn [offset] (fn [em]
(when-not @cancelled? (when-not @cancelled?
(reset! offset* offset)))))))] (reset! offset* em)))))))]
(fn [] (fn []
(vreset! cancelled? true) (vreset! cancelled? true)
(tm/dispose! task))))) (tm/dispose! task)))))
@ -159,13 +165,13 @@
(defn- sample-style (defn- sample-style
"Inline style that applies the typography font and optically centers the "Inline style that applies the typography font and optically centers the
sample glyphs within the fixed-height row." sample glyphs within the fixed-height row."
[typography offset] [typography em]
(let [base {:font-family (:font-family typography) (let [base {:font-family (:font-family typography)
:font-weight (:font-weight typography) :font-weight (:font-weight typography)
:font-style (:font-style typography)}] :font-style (:font-style typography)}]
(if (zero? offset) (if (zero? em)
base base
(assoc base :transform (dm/str "translateY(" offset "px)"))))) (assoc base :transform (dm/str "translateY(" em "em)")))))
;; --- FONT SELECTOR -------------------------------------------------------- ;; --- FONT SELECTOR --------------------------------------------------------
@ -189,7 +195,18 @@
;; we show the plain name rather than runtime-loading the whole catalog. ;; we show the plain name rather than runtime-loading the whole catalog.
in-sprite? (and attached? (contains? (:ids sprite) font-id)) in-sprite? (and attached? (contains? (:ids sprite) font-id))
fallback? (and (= :ready (:status sprite)) attached? (not in-sprite?)) fallback? (and (= :ready (:status sprite)) attached? (not in-sprite?))
loaded? (use-font-lazy-load font-id fallback?)] loaded? (use-font-lazy-load font-id fallback?)
;; Optical centering for the fallback name (custom fonts the sprite
;; doesn't cover): extreme vertical metrics would push the name low in
;; the row, so shift it by the measured offset once the font is known.
;; The label renders at `body-medium` (400/normal), which is the weight
;; and style we measure against.
label-offset (use-optical-offset font-id
(:family font)
"400"
"normal"
(:name font))]
(if in-sprite? (if in-sprite?
;; `fill: currentColor` (scss) makes the sprite glyph follow the row color. ;; `fill: currentColor` (scss) makes the sprite glyph follow the row color.
[:svg {:class (stl/css :font-item-preview) [:svg {:class (stl/css :font-item-preview)
@ -197,8 +214,11 @@
:aria-label (:name font)} :aria-label (:name font)}
[:use {:href (dm/str "#" fonts/preview-sprite-prefix font-id)}]] [:use {:href (dm/str "#" fonts/preview-sprite-prefix font-id)}]]
[:span {:class (stl/css :font-item-label) [:span {:class (stl/css :font-item-label)
:style (when loaded? :style (cond-> {}
#js {:fontFamily (dm/str "\"" (:family font) "\", sans-serif")})} loaded?
(assoc :font-family (dm/str "\"" (:family font) "\", sans-serif"))
(not (zero? label-offset))
(assoc :transform (dm/str "translateY(" label-offset "em)")))}
(:name font)]))) (:name font)])))
(mf/defc font-item* (mf/defc font-item*
@ -669,10 +689,11 @@
font-data (fonts/get-font-data (:font-id typography)) font-data (fonts/get-font-data (:font-id typography))
typography-id (:id typography) typography-id (:id typography)
show-actions? (and is-asset? is-editable) show-actions? (and is-asset? is-editable)
offset (use-sample-offset (:font-id typography) offset (use-optical-offset (:font-id typography)
(:font-family typography) (:font-family typography)
(:font-weight typography) (:font-weight typography)
(:font-style typography)) (:font-style typography)
"Ag")
on-delete on-delete
(mf/use-fn (mf/use-fn
@ -790,10 +811,11 @@
open? (deref open*) open? (deref open*)
font-data (fonts/get-font-data (:font-id typography)) font-data (fonts/get-font-data (:font-id typography))
name-only? (= (:name typography) (:name font-data)) name-only? (= (:name typography) (:name font-data))
offset (use-sample-offset (:font-id typography) offset (use-optical-offset (:font-id typography)
(:font-family typography) (:font-family typography)
(:font-weight typography) (:font-weight typography)
(:font-style typography)) (:font-style typography)
"Ag")
on-name-blur on-name-blur
(mf/use-fn (mf/use-fn

View File

@ -950,31 +950,34 @@
:descent (.-fontBoundingBoxDescent measure)})) :descent (.-fontBoundingBoxDescent measure)}))
(defn measure-text-metrics (defn measure-text-metrics
"Measure the font-wide (bounding-box) and glyph-ink vertical metrics of the "Measure the font-wide (bounding-box) and glyph-ink vertical metrics of `text`
`Ag` sample at 16px, mirroring the assets typography sample rendering. at `font-size` px for the given font.
Returns `{:font-ascent :font-descent :ink-ascent :ink-descent}` in px, or nil Returns `{:font-ascent :font-descent :ink-ascent :ink-descent}` in px, or nil
when the browser doesn't expose the bounding-box metrics. The font-wide when the browser doesn't expose the bounding-box metrics. The font-wide
values track what CSS uses for the line box, while the ink ones track the values track what CSS uses for the line box, while the ink ones track the
visible glyphs, which is what an optical centering shift needs." visible glyphs, which is what an optical centering shift needs."
[family weight style] ([family weight style]
(let [element (.createElement globals/document "canvas") (measure-text-metrics family weight style "Ag" 16))
context (.getContext element "2d") ([family weight style text font-size]
_ (set! (.-font context) (let [element (.createElement globals/document "canvas")
(dm/str (or weight "400") " " (or style "normal") " 16px \"" family "\"")) context (.getContext element "2d")
measure ^js (.measureText context "Ag") _ (set! (.-font context)
font-ascent (.-fontBoundingBoxAscent measure) (dm/str (or weight "400") " " (or style "normal") " "
font-descent (.-fontBoundingBoxDescent measure) font-size "px \"" family "\""))
ink-ascent (.-actualBoundingBoxAscent measure) measure ^js (.measureText context (str text))
ink-descent (.-actualBoundingBoxDescent measure)] font-ascent (.-fontBoundingBoxAscent measure)
(when (and (number? font-ascent) font-descent (.-fontBoundingBoxDescent measure)
(number? font-descent) ink-ascent (.-actualBoundingBoxAscent measure)
(number? ink-ascent) ink-descent (.-actualBoundingBoxDescent measure)]
(number? ink-descent)) (when (and (number? font-ascent)
{:font-ascent font-ascent (number? font-descent)
:font-descent font-descent (number? ink-ascent)
:ink-ascent ink-ascent (number? ink-descent))
:ink-descent ink-descent}))) {:font-ascent font-ascent
:font-descent font-descent
:ink-ascent ink-ascent
:ink-descent ink-descent}))))
(defn clone-node (defn clone-node
([^js node] ([^js node]