diff --git a/examples/GUI/FontsDemo.h b/examples/GUI/FontsDemo.h index 4b4fa6c68f..83bd29c45d 100644 --- a/examples/GUI/FontsDemo.h +++ b/examples/GUI/FontsDemo.h @@ -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 (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); } } diff --git a/modules/juce_graphics/fonts/juce_Font.cpp b/modules/juce_graphics/fonts/juce_Font.cpp index 92fd06192f..09e63cf051 100644 --- a/modules/juce_graphics/fonts/juce_Font.cpp +++ b/modules/juce_graphics/fonts/juce_Font.cpp @@ -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 getAscentOverride() const { return options.getAscentOverride(); } + std::optional 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 x) + { + jassert (getReferenceCount() == 1); + options = options.withAscentOverride (x); + } + + void setDescentOverride (std::optional 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 Font::getAscentOverride() const noexcept +{ + return font->getAscentOverride(); +} + +void Font::setAscentOverride (std::optional x) +{ + font->setAscentOverride (x); +} + +std::optional Font::getDescentOverride() const noexcept +{ + return font->getDescentOverride(); +} + +void Font::setDescentOverride (std::optional 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(); } diff --git a/modules/juce_graphics/fonts/juce_Font.h b/modules/juce_graphics/fonts/juce_Font.h index 99b9c2dfcf..9cbc7f0263 100644 --- a/modules/juce_graphics/fonts/juce_Font.h +++ b/modules/juce_graphics/fonts/juce_Font.h @@ -434,6 +434,50 @@ public: */ void setExtraKerningFactor (float extraKerning); + /** @see setAscentOverride() */ + std::optional 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); + + /** @see setDescentOverride() */ + std::optional 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); + //============================================================================== /** Changes all the font's characteristics with one call. */ void setSizeAndStyle (float newHeight, diff --git a/modules/juce_graphics/fonts/juce_FontOptions.cpp b/modules/juce_graphics/fonts/juce_FontOptions.cpp index 74e27485ef..2b756c447e 100644 --- a/modules/juce_graphics/fonts/juce_FontOptions.cpp +++ b/modules/juce_graphics/fonts/juce_FontOptions.cpp @@ -93,6 +93,8 @@ auto FontOptions::tie() const typeface.get(), fallbacks, metricsKind, + ascentOverride, + descentOverride, height, pointHeight, tracking, diff --git a/modules/juce_graphics/fonts/juce_FontOptions.h b/modules/juce_graphics/fonts/juce_FontOptions.h index d2d0543709..266818d778 100644 --- a/modules/juce_graphics/fonts/juce_FontOptions.h +++ b/modules/juce_graphics/fonts/juce_FontOptions.h @@ -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 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 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{}; }; diff --git a/modules/juce_graphics/fonts/juce_Typeface.cpp b/modules/juce_graphics/fonts/juce_Typeface.cpp index be4ab55c51..0c49be92eb 100644 --- a/modules/juce_graphics/fonts/juce_Typeface.cpp +++ b/modules/juce_graphics/fonts/juce_Typeface.cpp @@ -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 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 getBitmapLayer (const Typeface& typeface, int gly std::vector 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 diff --git a/modules/juce_graphics/fonts/juce_Typeface.h b/modules/juce_graphics/fonts/juce_Typeface.h index dedfee1b3b..aa95f2513e 100644 --- a/modules/juce_graphics/fonts/juce_Typeface.h +++ b/modules/juce_graphics/fonts/juce_Typeface.h @@ -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{}; };