1
0
Fork 0
mirror of https://github.com/juce-framework/JUCE.git synced 2026-01-09 23:34:20 +00:00
JUCE/examples/GUI/FontFeaturesDemo.h
2025-09-18 20:51:02 +02:00

528 lines
23 KiB
C++

/*
==============================================================================
This file is part of the JUCE framework examples.
Copyright (c) Raw Material Software Limited
The code included in this file is provided under the terms of the ISC license
http://www.isc.org/downloads/software-support-policy/isc-license. Permission
to use, copy, modify, and/or distribute this software for any purpose with or
without fee is hereby granted provided that the above copyright notice and
this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
==============================================================================
*/
/*******************************************************************************
The block below describes the properties of this PIP. A PIP is a short snippet
of code that can be read by the Projucer and used to generate a JUCE project.
BEGIN_JUCE_PIP_METADATA
name: FontFeaturesDemo
version: 1.0.0
vendor: JUCE
website: http://juce.com
description: Displays different font features.
dependencies: juce_core, juce_data_structures, juce_events, juce_graphics,
juce_gui_basics
exporters: xcode_mac, vs2022, vs2026, linux_make, androidstudio,
xcode_iphone
moduleFlags: JUCE_STRICT_REFCOUNTEDPOINTER=1
type: Component
mainClass: FontFeaturesDemo
useLocalCopy: 1
END_JUCE_PIP_METADATA
*******************************************************************************/
#pragma once
#include "../Assets/DemoUtilities.h"
static const std::map<FontFeatureTag, std::pair<const char*, const char*>> featureDescriptionMap
{
{ "abvs", { "Above-base Substitutions", "\xe0\xa4\x95\xe0\xa4\xbf" } },
{ "abvf", { "Above-base Forms", "\xe0\xa4\x95\xe0\xa4\x82" } },
{ "akhn", { "Akhand Ligatures", "\xe0\xa4\x95\xe0\xa5\x8d\xe0\xa4\xb7" } },
{ "blwf", { "Below-base Forms", "\xe0\xa4\x95\xe0\xa5\x8d\xe0\xa4\xa4" } },
{ "blws", { "Below-base Substitutions", "\xe0\xa4\x9f\xe0\xa5\x81" } },
{ "abvm", { "Above-Base Mark Positioning", "\xe0\xa4\x95\xe0\xa4\x82" } },
{ "blwm", { "Below-Base Mark Positioning", "\xe0\xa4\x95\xe0\xa5\x83\xe0\xa4\xb7\xe0\xa5\x8d\xe0\xa4\xa3" } },
{ "cjct", { "Conjunct Forms", "\xe0\xa4\x95\xe0\xa5\x8d\xe0\xa4\xa4" } },
{ "nukt", { "Nukta Forms", "\xe0\xa4\x95\xe0\xa4\xbc" } },
{ "pres", { "Pre-base Substitutions", "\xe0\xa4\xb0\xe0\xa5\x8d\xe0\xa4\x95" } },
{ "psts", { "Post-base Substitutions", "\xe0\xa4\x95\xe0\xa5\x8d\xe0\xa4\xaf" } },
{ "rkrf", { "Rakar Forms", "\xe0\xa4\x9f\xe0\xa5\x8d\xe0\xa4\xb0" } },
{ "rphf", { "Reph Forms", "\xe0\xa4\xb0\xe0\xa5\x8d\xe0\xa4\x95" } },
{ "vatu", { "Vattu Variants", "\xe0\xa4\x95\xe0\xa5\x8d\xe0\xa4\xb0" } },
{ "mark", { "Mark Positioning", "\x72\xc3\xa9\x73\x75\x6d\xc3\xa9" } },
{ "mkmk", { "Mark to Mark Positioning", "\xe1\xba\xa5" } },
{ "locl", { "Localized Forms", "This is fancy" } },
{ "curs", { "Cursive Positioning", "\xd8\xb9\xd8\xb1\xd8\xa8\xd9\x8a" } },
{ "dist", { "Distances (for complex scripts)", "\xe0\xb9\x80\xe0\xb8\x9b\xe0\xb9\x87\xe0\xb8\x99\xe0\xb9\x84\xe0\xb8\x97\xe0\xb8\xa2" } },
{ "pref", { "Pre-base Forms", "\xe0\xa4\xb0\xe0\xa5\x8d\xe0\xa4\x95" } },
{ "pstf", { "Post-base Forms", "\xe0\xa4\x95\xe0\xa5\x8d\xe0\xa4\xaf" } },
{ "half", { "Half Forms", "\xe0\xa4\x95\xe0\xa5\x8d" } },
{ "haln", { "Halant Forms", "\xe0\xa4\x95\xe0\xa5\x8d" } },
{ "fina", { "Terminal Forms", "\xd8\xb9" } },
{ "init", { "Initial Forms", "\xd8\xb9" } },
{ "isol", { "Isolated Forms", "\xd8\xb9" } },
{ "medi", { "Medical Forms", "\xd8\xb9" } },
{ "rclt", { "Required Contextual Alternates", "\x66\x69" } },
{ "rvrn", { "Required Variation Alternates", "Aaa" } },
{ "liga", { "Standard Ligatures", "official flight" } },
{ "dlig", { "Discretionary Ligatures", "ct sp st" } },
{ "calt", { "Contextual Alternates", "The Last Bloom" } },
{ "clig", { "Contextual Ligatures", "swift" } },
{ "cswh", { "Contextual Swash", "Feeling Good" } },
{ "hlig", { "Historical Ligatures", "historical finger" } },
{ "rlig", { "Required Ligatures", "\xd9\x84\xd8\xa7" } },
{ "ccmp", { "Glyph Composition/Decomposition", "\xc3\xb1" } },
{ "kern", { "Kerning", "AWAY" } },
{ "fwid", { "Full Width", "AMA" } },
{ "hwid", { "Half Width", "AMA" } },
{ "pwid", { "Proportional Width", "AMA" } },
{ "twid", { "Third Width", "AMA" } },
{ "qwid", { "Quarter Widths", "AMA" } },
{ "smcp", { "Small Capitals", "Small" } },
{ "c2sc", { "Caps to Small Caps", "CAPS" } },
{ "pcap", { "Petite Capitals", "Petite" } },
{ "c2pc", { "Caps to Petite Caps", "CAPS" } },
{ "unic", { "Unicase", "Mixed case" } },
{ "case", { "Case-Sensitive Forms", "\x7b\xc2\xbf\x48\x4f\x4c\x41\x21\x7d" } },
{ "cpsp", { "Capital Spacing", "ALL CAPS" } },
{ "salt", { "Stylistic Alternates", "Hidden Garden" } },
{ "aalt", { "Access All Alternates", "a" } },
{ "swsh", { "Swash", "The Juiciest JUCE" } },
{ "titl", { "Titling Alternates", "Headline" } },
{ "hist", { "Historical Forms", "looong s" } },
{ "rand", { "Randomize Alternates", "Random!" } },
{ "frac", { "Fractions", "1/2" } },
{ "afrc", { "Alternative Fractions", "1/2" } },
{ "numr", { "Numerators", "32" } },
{ "dnom", { "Denominators", "45" } },
{ "sups", { "Superscript", "x2" } },
{ "subs", { "Subscript", "H2O" } },
{ "sinf", { "Scientific Inferiors", "H2O SOx YCbCr NO2" } },
{ "mgrk", { "Mathematical Greek", "\xce\x91\xce\xb1\x20\xce\x95\xce\xb5\x20\xce\x94\xce\xb4" } },
{ "ordn", { "Ordinals", "1st, 2nd, 3rd" } },
{ "zero", { "Slashed Zero", "0x0001" } },
{ "pnum", { "Proportional Figures", "0123456789" } },
{ "tnum", { "Tabular Figures", "0123456789" } },
{ "lnum", { "Lining Figures", "0123456789" } },
{ "onum", { "Oldstyle Figures", "0123456789" } },
{ "jp78", { "Japanese 1978 Forms", "\xe8\xbe\xbb" } },
{ "jp83", { "Japanese 1983 Forms", "\xe5\x86\x86" } },
{ "jp90", { "Japanese 1990 Forms", "\xe8\x91\x89" } },
{ "jp04", { "Japanese 2004 Forms", "\xe9\xaa\xa8" } },
{ "trad", { "Traditional Forms", "\xe5\x8f\xb0" } },
{ "vert", { "Vertical Writing", "A" } },
{ "vrt2", { "Vertical Alternates and Rotation", "\xe2\x80\x94" } },
{ "size", { "Optical Size", "Text at 12pts" } },
{ "ornm", { "Ornaments", "zwzwzwzwzwzy" } },
{ "nalt", { "Alternate Annotation Forms", "\xe3\x81\x82" } },
{ "expt", { "Export Forms", "apple" } },
{ "halt", { "Halant Forms", "\xe0\xa4\x95\xe0\xa5\x8d" } },
{ "hkna", { "Horizontal Kana Alternates", "\xe3\x81\x8b" } },
{ "hojo", { "Hojo Kanji Forms", "\xe4\xbe\xae" } },
{ "ital", { "Italics", "Italics" } },
{ "nlck", { "NLC Kanji Forms", "\xe5\x9c\x8b" } },
{ "palt", { "Proportional Alternate Widths", "\xe5\x9b\xbd" } },
{ "ruby", { "Ruby Notation Forms", "\xe6\xbc\xa2" } },
{ "vkna", { "Vertical Kana Alternates", "\xe3\x81\x8b" } },
{ "vkrn", { "Vertical Kerning", "AV" } },
{ "vpal", { "Vertical Alternates and Positioning", "\xe3\x83\xbb" } },
{ "vhal", { "Vertical Alternates for Hangul", "\xed\x95\x9c" } },
{ "pkna", { "Proportional Kana", "\xe3\x81\x8b" } },
{ "requ", { "Required Ligatures", "\x66\x69" } },
{ "smpl", { "Simplified Forms", "\xe8\xaf\xb4" } },
{ "reqd", { "Required Contextual Alternates", "\x66\x69" } },
{ "dpng", { "Diphthongs", "\xc3\xa6" } },
{ "hope", { "Historical OpenType Processing", "\xc5\xbf" } },
{ "cpct", { "Centered CJK Punctuation", "\xe3\x80\x82" } },
{ "rtla", { "Right-to-Left Alternates", "\xd9\xa1" } },
{ "lfbd", { "Left Bounds", "Left" } },
{ "rtbd", { "Right Bounds", "Right" } },
{ "dtls", { "Dotless Forms", "\xc4\xb1" } },
{ "flac", { "Flattened accent components", "\xc3\xa9" } }
};
class FontsListModel : public ListBoxModel
{
public:
FontsListModel()
{
Font::findFonts (fonts);
fonts.removeIf ([] (const Font& f)
{
return f.getTypefacePtr()->getSupportedFeatures().empty();
});
}
std::function<void()> onFontSelected;
int getNumRows() override
{
return fonts.size();
}
void paintListBoxItem (int rowNumber,
Graphics& g,
int width,
int height,
bool rowIsSelected) override
{
if (rowIsSelected)
g.fillAll (Colours::lightblue);
const Font options { FontOptions { getFaceForRow (rowNumber) } };
AttributedString s;
s.setWordWrap (AttributedString::none);
s.setJustification (Justification::centredLeft);
s.append (getNameForRow (rowNumber),
options.withPointHeight ((float) height * 0.7f),
Colours::black);
s.append (" " + getNameForRow (rowNumber),
FontOptions{}.withPointHeight ((float) height * 0.5f).withStyle ("Italic"),
Colours::grey);
s.draw (g, Rectangle (width, height).expanded (-4, 50).toFloat());
}
void selectedRowsChanged (int) override
{
NullCheckedInvocation::invoke (onFontSelected);
}
Typeface::Ptr getFaceForRow (int rowNumber) const
{
return fonts.getReference (rowNumber).getTypefacePtr();
}
String getNameForRow (int rowNumber) override
{
return fonts.getReference (rowNumber).getTypefaceName();
}
private:
Array<Font> fonts;
};
class FeatureListModel : public ListBoxModel
{
struct Feature
{
FontFeatureTag tag;
String description;
String exampleText;
};
public:
FeatureListModel() = default;
int getNumRows() override
{
return (int) features.size();
}
void setFont (Typeface::Ptr face)
{
if (currentFace == face)
return;
features.clear();
currentFace = face;
if (currentFace == nullptr)
return;
for (auto feature : currentFace->getSupportedFeatures())
{
String description;
String exampleText;
const auto iter = featureDescriptionMap.find (feature);
if (iter == featureDescriptionMap.end())
{
const auto string = feature.toString();
// A malformed feature tag can result in a string with less than 4 characters.
if (string.length() != 4)
continue;
const auto isIndexed = std::isalnum ((int) string[2])
&& std::isalnum ((int) string[3]);
const auto isStylisticSet = string.startsWith ("ss")
&& isIndexed;
const auto isCharacterVariant = string.startsWith ("cv")
&& isIndexed;
if (isStylisticSet)
{
description << "Stylistic Set " << string.substring (2)
.getIntValue();
exampleText << "Some Example Text";
}
else if (isCharacterVariant)
{
description << "Character Variant " << string.substring (2)
.getIntValue();
exampleText << "aBcDeF123";
}
else
{
description << "Unknown Feature";
}
}
else
{
description = String::fromUTF8 (iter->second.first);
exampleText = String::fromUTF8 (iter->second.second);
}
features.push_back ({ feature, description, exampleText });
}
}
void paintListBoxItem (int rowNumber,
Graphics& g,
int width,
int height,
bool /*rowIsSelected*/) override
{
auto feature = features[(size_t) rowNumber];
const Font baseLineFont { FontOptions { currentFace }.withFeatureDisabled (feature.tag) };
const Font exampleFont { FontOptions { currentFace }.withFeatureEnabled (feature.tag) };
auto bounds = Rectangle { width, height }.reduced (10, 3).toFloat();
Path boundsPath;
boundsPath.addRoundedRectangle (bounds, 4);
g.reduceClipRegion (boundsPath);
g.fillAll (Colours::white);
bounds.reduce (7, 2);
auto getGlyphArrangementBoundingBox = [] (const GlyphArrangement& ga)
{
return ga.getBoundingBox (0, ga.getNumGlyphs(), true);
};
const FontStringPair description[] =
{
FontStringPair { FontOptions{}.withPointHeight (15).withStyle ("bold"),
feature.tag.toString() },
FontStringPair { FontOptions{}.withPointHeight (15).withStyle ("italic"),
" - " + feature.description }
};
const FontStringPair example[] =
{
FontStringPair { baseLineFont.withPointHeight (16),
feature.exampleText },
FontStringPair { baseLineFont.withPointHeight (16),
" " + String::fromUTF8 ("\xe2\x86\x92") },
FontStringPair { exampleFont.withPointHeight (16),
feature.exampleText }
};
const auto descriptionWidth = getGlyphArrangementBoundingBox (buildMultiFontText (bounds,
Justification::topLeft,
description)).getWidth();
const auto exampleWidth = getGlyphArrangementBoundingBox (buildMultiFontText (bounds,
Justification::topLeft,
example)).getWidth();
const auto exampleBounds = bounds.removeFromRight (exampleWidth);
const auto descriptionBounds = bounds.removeFromLeft (descriptionWidth);
auto descriptionGa = buildMultiFontText (descriptionBounds,
Justification::centredLeft,
description);
g.setGradientFill (ColourGradient (Colours::black,
exampleBounds.getX() - 30.0f,
0,
Colours::transparentBlack,
exampleBounds.getX() - 10.0f,
0,
false));
descriptionGa.draw (g);
auto exampleGa = buildMultiFontText (exampleBounds,
Justification::centredLeft,
example);
exampleGa.justifyGlyphs (0,
exampleGa.getNumGlyphs(),
exampleBounds.getX(),
exampleBounds.getY(),
exampleBounds.getWidth(),
exampleBounds.getHeight(),
Justification::centredRight);
g.setColour (Colours::black);
exampleGa.draw (g);
const FontStringPair strings[] =
{
FontStringPair { baseLineFont.withPointHeight (16), feature.exampleText },
FontStringPair { exampleFont.withPointHeight (16), feature.exampleText }
};
const auto pre = buildMultiFontText (Rectangle<float> { 1000, 50 },
Justification::centredLeft,
Span { strings, 1 });
const auto post = buildMultiFontText (Rectangle<float> { 1000, 50 },
Justification::centredLeft,
Span { strings + 1, 1 });
if (compareArrangements (pre, post))
{
g.setColour (Colours::grey.withAlpha (0.6f));
g.fillRoundedRectangle (Rectangle { width, height }.reduced (10, 3)
.toFloat(), 4);
}
}
struct FontStringPair
{
Font font;
String string;
};
static GlyphArrangement buildMultiFontText (Rectangle<float> bounds,
Justification justification,
Span<const FontStringPair> strings)
{
GlyphArrangement ga;
float offset = 0;
for (const auto& pair : strings)
{
ga.addFittedText (pair.font,
pair.string,
bounds.getX() + offset,
bounds.getY(),
bounds.getWidth(),
bounds.getHeight(),
justification,
1,
1);
const auto whitespaceWidth = GlyphArrangement::getStringWidth (pair.font, " ");
offset = whitespaceWidth + ga.getBoundingBox (0, ga.getNumGlyphs(), true).getWidth();
}
return ga;
}
static bool compareArrangements (const GlyphArrangement& a, const GlyphArrangement& b)
{
static auto compare = [] (const PositionedGlyph& pgA, const PositionedGlyph& pgB)
{
const auto tie = [] (const auto& x) { return std::tuple (x.getGlyphIndex(),
x.getBounds()); };
return tie (pgA) == tie (pgB);
};
return std::equal (a.begin(), a.end(), b.begin(), b.end(), compare);
}
Typeface::Ptr currentFace;
std::vector<Feature> features;
};
class FeaturesListComponent : public Component
{
public:
FeaturesListComponent()
{
featureList.setTitle ("Fonts");
featureList.setRowHeight (40);
addAndMakeVisible (featureList);
}
void setFont (Typeface::Ptr face)
{
listModel.setFont (face);
featureList.updateContent();
}
void resized() override
{
featureList.setBounds (getLocalBounds());
}
FeatureListModel listModel;
ListBox featureList { {}, &listModel };
};
//==============================================================================
class FontFeaturesDemo : public Component
{
public:
FontFeaturesDemo()
{
fontsListBox.setTitle ("Fonts");
fontsListBox.setRowHeight (20);
fontsListBox.setColour (ListBox::textColourId, Colours::black);
fontsListBox.setColour (ListBox::backgroundColourId, Colours::white);
fontsListModel.onFontSelected = [this]
{
featureListBox.setFont (fontsListModel.getFaceForRow (fontsListBox.getSelectedRow()));
};
fontsListBox.selectRow (0);
infoLabel.setFont (FontOptions{}.withPointHeight (16));
infoLabel.setText ("Supported Features - "
"(Greyed out items are supported but not affected by the example)",
dontSendNotification);
addAndMakeVisible (fontsListBox);
addAndMakeVisible (infoLabel);
addAndMakeVisible (featureListBox);
setSize (750, 750);
}
void resized() override
{
auto bounds = getLocalBounds().reduced (5);
fontsListBox.setBounds (bounds.removeFromLeft (bounds.proportionOfWidth (0.3f)));
infoLabel.setBounds (bounds.removeFromTop (30).reduced (5));
featureListBox.setBounds (bounds);
}
private:
FontsListModel fontsListModel;
ListBox fontsListBox { {}, &fontsListModel };
Label infoLabel;
FeaturesListComponent featureListBox;
JUCE_DECLARE_NON_COPYABLE (FontFeaturesDemo)
};