1
0
Fork 0
mirror of https://github.com/juce-framework/JUCE.git synced 2026-01-09 23:34:20 +00:00

Font: Allow overriding typeface ascent and descent metrics

This commit is contained in:
reuk 2025-01-20 17:43:35 +00:00
parent f12d29899c
commit a381fdf81d
No known key found for this signature in database
7 changed files with 190 additions and 48 deletions

View file

@ -54,8 +54,7 @@
//==============================================================================
class FontsDemo final : public Component,
private ListBoxModel,
private Slider::Listener
private ListBoxModel
{
public:
FontsDemo()
@ -68,6 +67,10 @@ public:
addAndMakeVisible (heightLabel);
addAndMakeVisible (kerningLabel);
addAndMakeVisible (kerningSlider);
addAndMakeVisible (ascentLabel);
addAndMakeVisible (ascentSlider);
addAndMakeVisible (descentLabel);
addAndMakeVisible (descentSlider);
addAndMakeVisible (scaleLabel);
addAndMakeVisible (horizontalJustificationLabel);
addAndMakeVisible (verticalJustificationLabel);
@ -84,12 +87,13 @@ public:
heightLabel .attachToComponent (&heightSlider, true);
scaleLabel .attachToComponent (&scaleSlider, true);
styleLabel .attachToComponent (&styleBox, true);
ascentLabel .attachToComponent (&ascentSlider, true);
descentLabel .attachToComponent (&descentSlider, true);
horizontalJustificationLabel.attachToComponent (&horizontalJustificationBox, true);
verticalJustificationLabel .attachToComponent (&verticalJustificationBox, true);
heightSlider .addListener (this);
kerningSlider.addListener (this);
scaleSlider .addListener (this);
for (auto* slider : { &heightSlider, &kerningSlider, &scaleSlider, &ascentSlider, &descentSlider })
slider->onValueChange = [this] { refreshPreviewBoxFont(); };
boldToggle .onClick = [this] { refreshPreviewBoxFont(); };
italicToggle .onClick = [this] { refreshPreviewBoxFont(); };
@ -107,6 +111,11 @@ public:
heightSlider .setRange (3.0, 150.0, 0.01);
scaleSlider .setRange (0.2, 3.0, 0.01);
kerningSlider.setRange (-2.0, 2.0, 0.01);
ascentSlider .setRange (0.0, 2.0, 0.01);
descentSlider.setRange (0.0, 2.0, 0.01);
ascentSlider .setValue (1, dontSendNotification);
descentSlider.setValue (1, dontSendNotification);
// set up the layout and resizer bars..
verticalLayout.setItemLayout (0, -0.2, -0.8, -0.35); // width of the font list must be
@ -190,6 +199,10 @@ public:
r.removeFromBottom (8);
verticalJustificationBox.setBounds (r.removeFromBottom (30).withTrimmedLeft (labelWidth * 3));
r.removeFromBottom (8);
descentSlider.setBounds (r.removeFromBottom (30).withTrimmedLeft (labelWidth));
r.removeFromBottom (8);
ascentSlider.setBounds (r.removeFromBottom (30).withTrimmedLeft (labelWidth));
r.removeFromBottom (8);
scaleSlider.setBounds (r.removeFromBottom (30).withTrimmedLeft (labelWidth));
r.removeFromBottom (8);
kerningSlider.setBounds (r.removeFromBottom (30).withTrimmedLeft (labelWidth));
@ -199,13 +212,6 @@ public:
demoTextBox.setBounds (r);
}
void sliderValueChanged (Slider* sliderThatWasMoved) override
{
if (sliderThatWasMoved == &heightSlider) refreshPreviewBoxFont();
else if (sliderThatWasMoved == &kerningSlider) refreshPreviewBoxFont();
else if (sliderThatWasMoved == &scaleSlider) refreshPreviewBoxFont();
}
// The following methods implement the ListBoxModel virtual methods:
int getNumRows() override
{
@ -223,7 +229,7 @@ public:
AttributedString s;
s.setWordWrap (AttributedString::none);
s.setJustification (Justification::centredLeft);
s.append (getNameForRow (rowNumber), font.withHeight ((float) height * 0.7f), Colours::black);
s.append (getNameForRow (rowNumber), font.withPointHeight ((float) height * 0.7f), Colours::black);
s.append (" " + font.getTypefaceName(), FontOptions ((float) height * 0.5f, Font::italic), Colours::grey);
s.draw (g, Rectangle<int> (width, height).expanded (-4, 50).toFloat());
@ -236,6 +242,7 @@ public:
void selectedRowsChanged (int /*lastRowselected*/) override
{
resetMetricsSliders();
refreshPreviewBoxFont();
}
@ -251,14 +258,12 @@ private:
ListBox listBox;
TextEditor demoTextBox;
const double defaultScale = 1.0, defaultHeight = 20.0, defaultKerning = 0.0;
const bool defaultBold = false, defaultItalic = false, defaultUnderlined = false;
const int defaultStyle = 0, defaultHorizontalJustification = 0, defaultVerticalJustification = 0;
Label heightLabel { {}, "Height:" },
kerningLabel { {}, "Kerning:" },
scaleLabel { {}, "Scale:" },
styleLabel { {}, "Style:" },
ascentLabel { {}, "Ascent:" },
descentLabel { {}, "Descent:" },
horizontalJustificationLabel { {}, "Justification (horizontal):" },
verticalJustificationLabel { {}, "Justification (vertical):" };
@ -268,7 +273,7 @@ private:
TextButton resetButton { "Reset" };
Slider heightSlider, kerningSlider, scaleSlider;
Slider heightSlider, kerningSlider, scaleSlider, ascentSlider, descentSlider;
ComboBox styleBox, horizontalJustificationBox, verticalJustificationBox;
StretchableLayoutManager verticalLayout;
@ -283,17 +288,28 @@ private:
//==============================================================================
void resetToDefaultParameters()
{
scaleSlider .setValue (defaultScale);
heightSlider .setValue (defaultHeight);
kerningSlider.setValue (defaultKerning);
scaleSlider .setValue (1.0);
heightSlider .setValue (20.0);
kerningSlider.setValue (0.0);
boldToggle .setToggleState (defaultBold, sendNotificationSync);
italicToggle .setToggleState (defaultItalic, sendNotificationSync);
underlineToggle.setToggleState (defaultUnderlined, sendNotificationSync);
boldToggle .setToggleState (false, sendNotificationSync);
italicToggle .setToggleState (false, sendNotificationSync);
underlineToggle.setToggleState (false, sendNotificationSync);
styleBox.setSelectedItemIndex (defaultStyle);
horizontalJustificationBox.setSelectedItemIndex (defaultHorizontalJustification);
verticalJustificationBox .setSelectedItemIndex (defaultVerticalJustification);
styleBox.setSelectedItemIndex (0);
horizontalJustificationBox.setSelectedItemIndex (0);
verticalJustificationBox .setSelectedItemIndex (0);
resetMetricsSliders();
}
void resetMetricsSliders()
{
auto font = getFont (listBox.getSelectedRow());
font.setPointHeight (1.0f);
ascentSlider .setValue (font.getAscentInPoints());
descentSlider.setValue (font.getDescentInPoints());
}
void setupJustificationOptions()
@ -339,6 +355,8 @@ private:
font = font.withTypefaceStyle (styleBox.getText());
font.setUnderline (underlineToggle.getToggleState());
font.setAscentOverride ((float) ascentSlider .getValue());
font.setDescentOverride ((float) descentSlider.getValue());
demoTextBox.applyFontToAllText (font);
}
@ -353,7 +371,7 @@ private:
styleBox.clear();
styleBox.addItemList (newStyles, 1);
styleBox.setSelectedItemIndex (defaultStyle);
styleBox.setSelectedItemIndex (0);
}
}

View file

@ -220,17 +220,24 @@ public:
const ScopedLock lock (mutex);
if (auto ptr = getTypefacePtr (f))
return ptr->getNativeDetails().getFontAtSizeAndScale (f.getMetricsKind(), f.getHeight(), f.getHorizontalScale());
return ptr->getNativeDetails().getFontAtPointSizeAndScale (f.getHeightInPoints(), f.getHorizontalScale());
return {};
}
TypefaceMetrics getMetrics (const Font& f)
TypefaceAscentDescent getAscentDescent (const Font& f)
{
const ScopedLock lock (mutex);
if (auto ptr = getTypefacePtr (f))
return ptr->getMetrics (f.getMetricsKind());
{
const auto ascentDescent = ptr->getNativeDetails().getAscentDescent (f.getMetricsKind());
auto adjusted = ascentDescent;
adjusted.ascent = getAscentOverride().value_or (adjusted.ascent);
adjusted.descent = getDescentOverride().value_or (adjusted.descent);
return adjusted;
}
return {};
}
@ -262,6 +269,9 @@ public:
bool getFallbackEnabled() const { return options.getFallbackEnabled(); }
TypefaceMetricsKind getMetricsKind() const { return options.getMetricsKind(); }
std::optional<float> getAscentOverride() const { return options.getAscentOverride(); }
std::optional<float> getDescentOverride() const { return options.getDescentOverride(); }
/* This shared state may be shared between two or more Font instances that are being
read/modified from multiple threads.
Before modifying a shared instance you *must* call dupeInternalIfShared to
@ -316,6 +326,18 @@ public:
options = options.withKerningFactor (x);
}
void setAscentOverride (std::optional<float> x)
{
jassert (getReferenceCount() == 1);
options = options.withAscentOverride (x);
}
void setDescentOverride (std::optional<float> x)
{
jassert (getReferenceCount() == 1);
options = options.withDescentOverride (x);
}
void setUnderline (bool x)
{
jassert (getReferenceCount() == 1);
@ -544,7 +566,7 @@ Font Font::withHeight (const float newHeight) const
float Font::getHeightToPointsFactor() const
{
return getTypefacePtr()->getMetrics (getMetricsKind()).heightToPoints;
return font->getAscentDescent (*this).getHeightToPointsFactor();
}
Font Font::withPointHeight (float heightInPoints) const
@ -699,6 +721,26 @@ void Font::setExtraKerningFactor (const float extraKerning)
font->resetTypeface();
}
std::optional<float> Font::getAscentOverride() const noexcept
{
return font->getAscentOverride();
}
void Font::setAscentOverride (std::optional<float> x)
{
font->setAscentOverride (x);
}
std::optional<float> Font::getDescentOverride() const noexcept
{
return font->getDescentOverride();
}
void Font::setDescentOverride (std::optional<float> x)
{
font->setDescentOverride (x);
}
Font Font::boldened() const { return withStyle (getStyleFlags() | bold); }
Font Font::italicised() const { return withStyle (getStyleFlags() | italic); }
@ -731,14 +773,14 @@ void Font::setUnderline (const bool shouldBeUnderlined)
float Font::getAscent() const
{
return font->getMetrics (*this).ascent * getHeight();
return font->getAscentDescent (*this).getScaledAscent() * getHeight();
}
float Font::getHeight() const noexcept
{
jassert ((font->getHeight() > 0.0f) != (font->getPointHeight() > 0.0f));
const auto height = font->getHeight();
return height > 0.0f ? height : font->getPointHeight() / getHeightToPointsFactor();
return height > 0.0f ? height : font->getPointHeight() * font->getAscentDescent (*this).getPointsToHeightFactor();
}
float Font::getDescent() const { return getHeight() - getAscent(); }
@ -747,7 +789,17 @@ float Font::getHeightInPoints() const
{
jassert ((font->getHeight() > 0.0f) != (font->getPointHeight() > 0.0f));
const auto pointHeight = font->getPointHeight();
return pointHeight > 0.0f ? pointHeight : font->getHeight() * getHeightToPointsFactor();
if (pointHeight > 0.0f)
return pointHeight;
const auto factor = font->getAscentDescent (*this).getPointsToHeightFactor();
if (factor > 0.0f)
return font->getHeight() / factor;
jassertfalse;
return 0.0f;
}
float Font::getAscentInPoints() const { return getAscent() * getHeightToPointsFactor(); }

View file

@ -434,6 +434,50 @@ public:
*/
void setExtraKerningFactor (float extraKerning);
/** @see setAscentOverride() */
std::optional<float> getAscentOverride() const noexcept;
/** This is designed to mirror CSS's ascent-override property.
When the font size is specified in points (using setPointHeight(),
FontOptions::withPointHeight(), etc.), then the font's ascent value in points will be equal
to the font's size in points multiplied by the override value. That is, if the font size
is 14pt and the ascent override is 0.5f, then the ascent will be 7pt.
When the font size is *not* specified in points (using setHeight(),
FontOptions::withHeight(), etc.), then the behaviour is more subtle.
The ascent override still specifies the size of the font's ascender as a proportion of the
font's em size.
However, the point size of the font is now found by multiplying the JUCE height by the
height-to-point factor, where this factor is equal to
(1.0f / (ascent-in-em-units + descent-in-em-units)).
As an example, if the JUCE font height is 14, the ascent override is 0.5f, and the
descent override is 0.5f, then the font size will be 14pt and the ascent will be 7pt.
Changing the ascent override to 1.0f and the descent override to 0.0f will preserve the
font size of 14pt but give an ascender of 14pt and a descender of 0pt.
Changing the ascent and descent overrides both to 1.0f will result in the
font's size changing to 7pt with an ascent of 3.5pt.
@see setDescentOverride()
*/
void setAscentOverride (std::optional<float>);
/** @see setDescentOverride() */
std::optional<float> getDescentOverride() const noexcept;
/** This is designed to mirror CSS's descent-override property.
Specifies a value to replace the built-in typeface descent metric.
The final descent value will be found by multiplying the provided value by the font
size. You may also pass std::nullopt to use the descent value specified in the typeface.
The documentation for setAscentOverride() includes a more thorough discussion
of the mechanism used for overriding.
@see setAscentOverride()
*/
void setDescentOverride (std::optional<float>);
//==============================================================================
/** Changes all the font's characteristics with one call. */
void setSizeAndStyle (float newHeight,

View file

@ -93,6 +93,8 @@ auto FontOptions::tie() const
typeface.get(),
fallbacks,
metricsKind,
ascentOverride,
descentOverride,
height,
pointHeight,
tracking,

View file

@ -161,9 +161,21 @@ public:
/** Returns a copy of these options with underline enabled or disabled, defaults to disabled. */
[[nodiscard]] FontOptions withUnderline (bool x = true) const { return withMember (*this, &FontOptions::underlined, x); }
/** Returns a copy of these options the specified metrics kind. */
/** Returns a copy of these options with the specified metrics kind. */
[[nodiscard]] FontOptions withMetricsKind (TypefaceMetricsKind x) const { return withMember (*this, &FontOptions::metricsKind, x); }
/** Returns a copy of these options with the specified font metrics value override.
std::nullopt indicates that the font should use the built-in typeface metric; otherwise,
the ascent value will be found by multiplying the provided value by the font size in points.
*/
[[nodiscard]] FontOptions withAscentOverride (std::optional<float> x) const { return withMember (*this, &FontOptions::ascentOverride, x.value_or (-1.0f)); }
/** Returns a copy of these options with the specified font metrics value override.
std::nullopt indicates that the font should use the built-in typeface metric; otherwise,
the descent value will be found by multiplying the provided value by the font size in points.
*/
[[nodiscard]] FontOptions withDescentOverride (std::optional<float> x) const { return withMember (*this, &FontOptions::descentOverride, x.value_or (-1.0f)); }
/** @see withName() */
[[nodiscard]] auto getName() const { return name; }
/** @see withStyle() */
@ -186,6 +198,10 @@ public:
[[nodiscard]] auto getUnderline() const { return underlined; }
/** @see withMetricsKind() */
[[nodiscard]] auto getMetricsKind() const { return metricsKind; }
/** @see withAscentOverride() */
[[nodiscard]] auto getAscentOverride() const { return ascentOverride >= 0.0f ? std::make_optional (ascentOverride) : std::nullopt; }
/** @see withDescentOverride() */
[[nodiscard]] auto getDescentOverride() const { return descentOverride >= 0.0f ? std::make_optional (descentOverride) : std::nullopt; }
/** Equality operator. */
[[nodiscard]] bool operator== (const FontOptions& other) const;
@ -211,6 +227,8 @@ private:
float pointHeight = -1.0f;
float tracking{};
float horizontalScale = 1.0f;
float ascentOverride = -1.0f;
float descentOverride = -1.0f;
bool fallbackEnabled = true;
bool underlined{};
};

View file

@ -185,7 +185,7 @@ public:
auto* getFont() const { return font; }
TypefaceAscentDescent getMetrics (TypefaceMetricsKind kind) const
TypefaceAscentDescent getAscentDescent (TypefaceMetricsKind kind) const
{
switch (kind)
{
@ -199,10 +199,9 @@ public:
return {};
}
HbFont getFontAtSizeAndScale (TypefaceMetricsKind kind, float height, float horizontalScale) const
HbFont getFontAtPointSizeAndScale (float points, float horizontalScale) const
{
HbFont subFont { hb_font_create_sub_font (font) };
const auto points = height * getMetrics (kind).getHeightToPointsFactor();
hb_font_set_ptem (subFont.get(), points);
hb_font_set_scale (subFont.get(), HbScale::juceToHb (points * horizontalScale), HbScale::juceToHb (points));
@ -405,8 +404,10 @@ void Typeface::getOutlineForGlyph (TypefaceMetricsKind kind, int glyphNumber, Pa
{
const auto native = getNativeDetails();
auto* font = native.getFont();
const auto metrics = native.getMetrics (kind);
const auto scale = metrics.getHeightToPointsFactor() / (float) hb_face_get_upem (hb_font_get_face (font));
const auto metrics = native.getAscentDescent (kind);
const auto factor = metrics.getHeightToPointsFactor();
jassert (! std::isinf (factor));
const auto scale = factor / (float) hb_face_get_upem (hb_font_get_face (font));
// getTypefaceGlyph returns glyphs in em space, getOutlineForGlyph returns glyphs in "special JUCE units" space
path = getGlyphPathInGlyphUnits ((hb_codepoint_t) glyphNumber, getNativeDetails().getFont());
@ -422,8 +423,10 @@ Rectangle<float> Typeface::getGlyphBounds (TypefaceMetricsKind kind, int glyphNu
return {};
const auto native = getNativeDetails();
const auto metrics = native.getMetrics (kind);
const auto scale = metrics.getHeightToPointsFactor() / (float) hb_face_get_upem (hb_font_get_face (font));
const auto metrics = native.getAscentDescent (kind);
const auto factor = metrics.getHeightToPointsFactor();
jassert (! std::isinf (factor));
const auto scale = factor / (float) hb_face_get_upem (hb_font_get_face (font));
return Rectangle { (float) extents.width, (float) extents.height }
.withPosition ((float) extents.x_bearing, (float) extents.y_bearing)
@ -524,8 +527,10 @@ static std::vector<GlyphLayer> getBitmapLayer (const Typeface& typeface, int gly
std::vector<GlyphLayer> Typeface::getLayersForGlyph (TypefaceMetricsKind kind, int glyphNumber, const AffineTransform& transform, float) const
{
auto* font = getNativeDetails().getFont();
const auto metrics = getNativeDetails().getMetrics (kind);
const auto scale = metrics.getHeightToPointsFactor() / (float) hb_face_get_upem (hb_font_get_face (font));
const auto metrics = getNativeDetails().getAscentDescent (kind);
const auto factor = metrics.getHeightToPointsFactor();
jassert (! std::isinf (factor));
const auto scale = factor / (float) hb_face_get_upem (hb_font_get_face (font));
const auto combinedTransform = AffineTransform::scale (scale, -scale).followedBy (transform);
if (auto bitmapLayer = getBitmapLayer (*this, glyphNumber, combinedTransform); ! bitmapLayer.empty())
@ -558,7 +563,7 @@ int Typeface::getColourGlyphFormats() const
TypefaceMetrics Typeface::getMetrics (TypefaceMetricsKind kind) const
{
return getNativeDetails().getMetrics (kind).getTypefaceMetrics();
return getNativeDetails().getAscentDescent (kind).getTypefaceMetrics();
}
Typeface::Ptr Typeface::createSystemTypefaceFor (const void* fontFileData, size_t fontFileDataSize)
@ -601,7 +606,8 @@ static float doSimpleShapeWithNoBreaks (const Typeface& typeface,
hb_buffer_guess_segment_properties (buffer.get());
const auto& native = typeface.getNativeDetails();
const auto sized = native.getFontAtSizeAndScale (kind, height, horizontalScale);
const auto points = typeface.getMetrics (kind).heightToPoints * height;
const auto sized = native.getFontAtPointSizeAndScale (points, horizontalScale);
auto* font = sized.get();
// Disable ligatures, because TextEditor requires a 1:1 codepoint/glyph mapping for caret

View file

@ -96,7 +96,7 @@ enum class TypefaceMetricsKind
*/
struct TypefaceMetrics
{
/** The proportion of the typeface's height that it above the baseline, as a value between 0 and 1.
/** The proportion of the typeface's height that is above the baseline, as a value between 0 and 1.
Note that 'height' here refers to the result of adding the absolute ascent and descent values.
That is, the sum of the ascent and descent will equal 1.
The sum of the ascent and descent will normally differ from the em size of the font.
@ -107,6 +107,8 @@ struct TypefaceMetrics
/** The factor by which a JUCE font height should be multiplied in order to convert to a font
size in points.
May be inf if the font ascent and descent overrides have both been set to 0!
*/
float heightToPoints{};
};