mirror of
https://github.com/penpot/penpot.git
synced 2026-09-08 13:09:22 +00:00
🐛 Fix font preview in assets breaks the font row (#11428)
* 🐛 Fix font preview in assets breaks the font row * 🐛 Fix font height problem also in the font dropdown * 🐛 Fix a small bug within the changes --------- Co-authored-by: Eva Marco <evamarcod@gmail.com>
This commit is contained in:
parent
f22abc9861
commit
a19b3c8d62
@ -419,9 +419,14 @@ $thumbnail-default-height: px2rem(168);
|
||||
}
|
||||
|
||||
.library-typography-sample {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
block-size: px2rem(20);
|
||||
line-height: 1;
|
||||
margin-inline-end: var(--sp-xs);
|
||||
inline-size: px2rem(20);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
// ─── MISC ──────────────────────────────────────
|
||||
|
||||
@ -90,6 +90,97 @@
|
||||
(constantly nil)))))
|
||||
@loaded?))
|
||||
|
||||
;; --- OPTICAL CENTERING OF SAMPLE TEXT --------------------------------------
|
||||
|
||||
;; Fonts with exaggerated vertical metrics (huge ascender/descender, small
|
||||
;; caps) render their line box lower within a fixed-height row, so a plain
|
||||
;; `align-items: center` leaves the visible glyphs sitting low. We measure the
|
||||
;; font-wide vs glyph-ink bounding boxes once per font/sample and shift the
|
||||
;; 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 optical-offset-cache (atom {}))
|
||||
|
||||
(defn- optical-offset-key [family weight style text]
|
||||
(dm/str family "|" weight "|" style "|" text))
|
||||
|
||||
(defn- optical-offset-em
|
||||
"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 centered line the shift reduces to the difference between the font-wide
|
||||
and ink bounding boxes:
|
||||
dy = ((ink-ascent - font-ascent) + (font-descent - ink-descent)) / 2.
|
||||
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]}
|
||||
(dom/measure-text-metrics family weight style text 16)]
|
||||
(let [dy (/ (+ (- ink-ascent font-ascent)
|
||||
(- font-descent ink-descent))
|
||||
2)
|
||||
em (/ dy 16)]
|
||||
;; Round to avoid float noise leaking into the transform string.
|
||||
(/ (js/Math.round (* em 10000)) 10000))))
|
||||
|
||||
(defn- load-optical-offset
|
||||
[font-id family weight style text]
|
||||
(let [key (optical-offset-key family weight style text)]
|
||||
(if-let [cached (get @optical-offset-cache key)]
|
||||
(p/resolved cached)
|
||||
(-> (fonts/ensure-loaded! font-id)
|
||||
(p/then
|
||||
(fn [_]
|
||||
(let [em (or (optical-offset-em family weight style text) 0)]
|
||||
(swap! optical-offset-cache assoc key em)
|
||||
em)))))))
|
||||
|
||||
(defn- use-optical-offset
|
||||
"Lazily resolve the optical-centering offset (in `em`) for sample text in a
|
||||
given font, measuring once per font/sample and caching it. Falls back to 0
|
||||
when the font isn't available or the metrics can't be measured."
|
||||
[font-id family weight style text]
|
||||
(let [offset* (mf/use-state 0)]
|
||||
(mf/use-effect
|
||||
(mf/deps font-id family weight style text)
|
||||
(fn []
|
||||
(let [cancelled? (volatile! false)
|
||||
key (optical-offset-key family weight style text)]
|
||||
(if (contains? @optical-offset-cache key)
|
||||
(reset! offset* (get @optical-offset-cache key))
|
||||
(let [task (tm/schedule-on-idle
|
||||
(fn []
|
||||
(-> (load-optical-offset font-id family weight style text)
|
||||
(p/then
|
||||
(fn [em]
|
||||
(when-not @cancelled?
|
||||
(reset! offset* em)))))))]
|
||||
(fn []
|
||||
(vreset! cancelled? true)
|
||||
(tm/dispose! task)))))
|
||||
nil))
|
||||
(deref offset*)))
|
||||
|
||||
(defn- sample-container-style
|
||||
"Inline style that applies the typography font to the (clipped, fixed-height)
|
||||
sample container."
|
||||
[typography]
|
||||
{:font-family (:font-family typography)
|
||||
:font-weight (:font-weight typography)
|
||||
:font-style (:font-style typography)})
|
||||
|
||||
(defn- sample-text-style
|
||||
"Inline style that optically centers the sample glyphs. Must be applied to
|
||||
the text node itself, not to the clipped container: a transform on an
|
||||
`overflow: hidden` element moves its own clip region along with it, so it
|
||||
would shift the whole box relative to the row instead of the glyphs inside it."
|
||||
[em]
|
||||
(when-not (zero? em)
|
||||
{:transform (dm/str "translateY(" em "em)")}))
|
||||
|
||||
;; --- FONT SELECTOR --------------------------------------------------------
|
||||
|
||||
(mf/defc font-item-preview*
|
||||
"Row content with previews: a vector preview from the shared sprite for catalog
|
||||
fonts, or the font's own name lazily loaded for custom fonts the sprite doesn't
|
||||
@ -110,7 +201,18 @@
|
||||
;; we show the plain name rather than runtime-loading the whole catalog.
|
||||
in-sprite? (and attached? (contains? (:ids sprite) font-id))
|
||||
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?
|
||||
;; `fill: currentColor` (scss) makes the sprite glyph follow the row color.
|
||||
[:svg {:class (stl/css :font-item-preview)
|
||||
@ -118,8 +220,11 @@
|
||||
:aria-label (:name font)}
|
||||
[:use {:href (dm/str "#" fonts/preview-sprite-prefix font-id)}]]
|
||||
[:span {:class (stl/css :font-item-label)
|
||||
:style (when loaded?
|
||||
#js {:fontFamily (dm/str "\"" (:family font) "\", sans-serif")})}
|
||||
:style (cond-> {}
|
||||
loaded?
|
||||
(assoc :font-family (dm/str "\"" (:family font) "\", sans-serif"))
|
||||
(not (zero? label-offset))
|
||||
(assoc :transform (dm/str "translateY(" label-offset "em)")))}
|
||||
(:name font)])))
|
||||
|
||||
(mf/defc font-item*
|
||||
@ -590,6 +695,11 @@
|
||||
font-data (fonts/get-font-data (:font-id typography))
|
||||
typography-id (:id typography)
|
||||
show-actions? (and is-asset? is-editable)
|
||||
offset (use-optical-offset (:font-id typography)
|
||||
(:font-family typography)
|
||||
(:font-weight typography)
|
||||
(:font-style typography)
|
||||
"Ag")
|
||||
|
||||
on-delete
|
||||
(mf/use-fn
|
||||
@ -624,10 +734,9 @@
|
||||
[:*
|
||||
[:div {:class (stl/css :font-name-wrapper)}
|
||||
[:div {:class (stl/css :typography-sample-input)
|
||||
:style {:font-family (:font-family typography)
|
||||
:font-weight (:font-weight typography)
|
||||
:font-style (:font-style typography)}}
|
||||
(tr "workspace.assets.typography.sample")]
|
||||
:style (sample-container-style typography)}
|
||||
[:span {:style (sample-text-style offset)}
|
||||
(tr "workspace.assets.typography.sample")]]
|
||||
|
||||
[:input
|
||||
{:class (stl/css :adv-typography-name)
|
||||
@ -661,11 +770,9 @@
|
||||
[:div {:class (stl/css :typography-info-wrapper)}
|
||||
[:div {:class (stl/css :typography-name-wrapper)}
|
||||
[:div {:class (stl/css :typography-sample)
|
||||
|
||||
:style {:font-family (:font-family typography)
|
||||
:font-weight (:font-weight typography)
|
||||
:font-style (:font-style typography)}}
|
||||
(tr "workspace.assets.typography.sample")]
|
||||
:style (sample-container-style typography)}
|
||||
[:span {:style (sample-text-style offset)}
|
||||
(tr "workspace.assets.typography.sample")]]
|
||||
|
||||
[:div {:class (stl/css :typography-name)
|
||||
:title (:name typography)}
|
||||
@ -712,6 +819,11 @@
|
||||
open? (deref open*)
|
||||
font-data (fonts/get-font-data (:font-id typography))
|
||||
name-only? (= (:name typography) (:name font-data))
|
||||
offset (use-optical-offset (:font-id typography)
|
||||
(:font-family typography)
|
||||
(:font-weight typography)
|
||||
(:font-style typography)
|
||||
"Ag")
|
||||
|
||||
on-name-blur
|
||||
(mf/use-fn
|
||||
@ -769,10 +881,9 @@
|
||||
[:div {:class (stl/css :font-name-wrapper)}
|
||||
[:div
|
||||
{:class (stl/css :typography-sample-input)
|
||||
:style {:font-family (:font-family typography)
|
||||
:font-weight (:font-weight typography)
|
||||
:font-style (:font-style typography)}}
|
||||
(tr "workspace.assets.typography.sample")]
|
||||
:style (sample-container-style typography)}
|
||||
[:span {:style (sample-text-style offset)}
|
||||
(tr "workspace.assets.typography.sample")]]
|
||||
|
||||
[:input
|
||||
{:class (stl/css :adv-typography-name)
|
||||
@ -789,10 +900,9 @@
|
||||
:on-context-menu on-context-menu}
|
||||
[:div
|
||||
{:class (stl/css :typography-sample)
|
||||
:style {:font-family (:font-family typography)
|
||||
:font-weight (:font-weight typography)
|
||||
:font-style (:font-style typography)}}
|
||||
(tr "workspace.assets.typography.sample")]
|
||||
:style (sample-container-style typography)}
|
||||
[:span {:style (sample-text-style offset)}
|
||||
(tr "workspace.assets.typography.sample")]]
|
||||
|
||||
[:div {:class (stl/css :name-block)
|
||||
:title (if name-only?
|
||||
|
||||
@ -119,6 +119,8 @@ $font-preview-box-height: 28px;
|
||||
inline-size: $sz-24;
|
||||
block-size: 100%;
|
||||
font-size: px2rem(16);
|
||||
line-height: 1;
|
||||
overflow: hidden;
|
||||
color: var(--color-foreground-primary);
|
||||
}
|
||||
|
||||
@ -171,8 +173,10 @@ $font-preview-box-height: 28px;
|
||||
align-items: center;
|
||||
min-inline-size: $sz-24;
|
||||
font-size: px2rem(16);
|
||||
line-height: 1;
|
||||
block-size: $sz-32;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
color: var(--color-foreground-primary);
|
||||
}
|
||||
|
||||
|
||||
@ -949,6 +949,36 @@
|
||||
{:ascent (.-fontBoundingBoxAscent measure)
|
||||
:descent (.-fontBoundingBoxDescent measure)}))
|
||||
|
||||
(defn measure-text-metrics
|
||||
"Measure the font-wide (bounding-box) and glyph-ink vertical metrics of `text`
|
||||
at `font-size` px for the given font.
|
||||
|
||||
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
|
||||
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."
|
||||
([family weight style]
|
||||
(measure-text-metrics family weight style "Ag" 16))
|
||||
([family weight style text font-size]
|
||||
(let [element (.createElement globals/document "canvas")
|
||||
context (.getContext element "2d")
|
||||
_ (set! (.-font context)
|
||||
(dm/str (or weight "400") " " (or style "normal") " "
|
||||
font-size "px \"" family "\""))
|
||||
measure ^js (.measureText context (str text))
|
||||
font-ascent (.-fontBoundingBoxAscent measure)
|
||||
font-descent (.-fontBoundingBoxDescent measure)
|
||||
ink-ascent (.-actualBoundingBoxAscent measure)
|
||||
ink-descent (.-actualBoundingBoxDescent measure)]
|
||||
(when (and (number? font-ascent)
|
||||
(number? font-descent)
|
||||
(number? ink-ascent)
|
||||
(number? ink-descent))
|
||||
{:font-ascent font-ascent
|
||||
:font-descent font-descent
|
||||
:ink-ascent ink-ascent
|
||||
:ink-descent ink-descent}))))
|
||||
|
||||
(defn clone-node
|
||||
([^js node]
|
||||
(clone-node node true))
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user