Properties (1)

Modeling the universe of fonts

Let's talk about a useful data structure for fonts. For the sake of this article, let's assume that a font consists of just a font face, a size and a flag for boldness. Turns out that even this simplified view of fonts is very useful in many use cases we encounter in our software:

// don't want to go into the details of possible string representations
struct MyString;

struct SFont {
    MyString m_strName;
    int m_nSize;
    bool m_bBold;
    // add italic, underline, strike-through, baseline offset,
    // spacing, allcaps, ... as needed
};
Listing (1)

In our PowerPoint add-in, we use a font representation like this for many different purposes, e.g.,

  • displaying the font settings of selected text,
  • applying font settings to selected text, in response to some user interaction,
  • applying default font settings to initialize some text that was inserted by our add-in.

This sounds easy enough, but there are some practically relevant limitations:

  • How can we represent a mixed font selection, e.g., where only some of the selected text is bold?
  • How can we represent a font for application when, e.g., the user wants to change only the size while leaving the boldness alone?

For display, we could use std::optional<SFont>, with std::nullopt representing a mixed font. This would work in principle, but even if only boldness is mixed in the selected text, and size and name are consistent, we could only show "Mixed font" without being able to display the size or the name. Not the greatest user interface on earth.

For application, we could use separate functions for the name, and the size, and the boldness setting. Given that in most practically relevant software, the user request for a font change would probably be processed by a hierarchy of nested function calls, the complexity of our SFont struct would proliferate throughout the entire call hierarchy. Want to add support for italic? Add a call tree of SetItalic(...) functions. Not the greatest software architecture on earth.

To solve both problems (and more, as we will see below), let's try to use std::optional inside of SFont:

struct SFont {
    std::optional<MyString> m_ostrName;
    std::optional<int> m_onSize;
    std::optional<bool> m_obBold;
};
Listing (2)

Let's call SFont a "property", and let's call its members m_ostrName, m_onSize and m_obBold "aspects" of the property. Aspects may be "mixed" or "undefined" (std::nullopt).

How does this solve our problem for display? When picking up the font aspects from text selection, we can fill SFont with well-defined information for aspects that are consistent throughout the selection. Aspects that have varying values within the selection can be represented by std::nullopt. When displaying the resulting font, we can display the values of consistent aspects, while for inconsistent aspects, we can show some representation of "mixed": "Arial ... pt bold", "... 12 pt" (implicitly non-bold) or "Arial 12 pt (bold)" are much more meaningful to a user than just "Mixed font".

How does this solve our problem with application? Whether we want to set size, boldness or name, or any combination thereof, we now pass an SFont object (or a reference thereof) through the entire call hierarchy. We call this a partially defined font. Functions that are just passing it on, do not have to care about which aspects of SFont actually carry values and which are std::nullopt.

With partially defined fonts, we can do some interesting things. For instance, in our software, we have hierarchical default settings: When determining the default font for a sum label in a stacked chart, we start with a fully defined global default font, which we infer from the PowerPoint Master Slide. We then apply, e.g., a general default font for chart labels, and finally a specific default font for sum labels. Except for the global default font, all fonts can be partially defined, so the chart default font may set the font size to 10 pt while leaving name and boldness alone, and the sum label default font may set the font to bold without affecting size and name. We write this neatly as:

auto const fontSum = fontDefaultGlobal << fontDefaultChart << fontDefaultSum;
Listing (3)

To facilitate this expression, we define the following operator overloads:

#define SET_ASPECT(member) \
    if(rhs.member) member = rhs.member

void SFont::operator<<=(SFont const& rhs) & noexcept {
    SET_ASPECT(m_ostrName);
    SET_ASPECT(m_onSize);
    SET_ASPECT(m_obBold);
}
SFont operator<<(SFont lhs, SFont const& rhs) noexcept {
    lhs <<= rhs;
    return lhs;
}
Listing (4)

You may argue that you don't want to overload operator<< with functionality that is semantically unrelated to bit shifting. You are free to rename SFont::operator<<=(...) to, e.g., SFont::Set(...), but you are trading it for the conciseness of expression (3). Also, when using operator<< for this purpose, there is a potential issue with operator associativity, and we'll get to that in an upcoming blog post.

For now, while we are at it, let's look into some other useful methods for SFont. How can we pick up mixed font from selected text? If only we had a method to create something like a "union of fonts"...

#define UNION_ASPECT(member) \
    if(member && rhs.member && *member!=*rhs.member) member = std::nullopt

void SFont::Union(SFont const& rhs) & noexcept {
    UNION_ASPECT(m_ostrName);
    UNION_ASPECT(m_onSize);
    UNION_ASPECT(m_obBold);
}

void Union(std::optional<SFont>& ofont, SFont const& font) noexcept {
    if( ofont ) {
        ofont->Union(font);
    } else {
        ofont = font;
    }
}

template<typename RngFont>
std::optional<SFont> CollectFont(RngFont const& rngfont) noexcept {
    // see our public domain range library at https://github.com/think-cell/think-cell-library
    return tc::accumulate(
        rngfont,
        std::optional<SFont>(),
        [&](auto& ofontAccu, auto const& font) noexcept { Union(ofontAccu, font); }
    );
}
Listing (5)

We are using std::optional<SFont> again, but this time it serves a different purpose. As the function name Union(...) suggests, we like to think of properties and aspects in terms of set theory. Our universe is the set of all possible fonts that could be represented by SFont. An SFont object represents a subset in this universe: If all members are well-defined, it represents a singleton. If no members are defined, the SFont object represents the universe. If a font has name "Arial" and size "12 pt" with boldness undefined, it represents the set with the two members "Arial 12 pt non-bold" and "Arial 12 pt bold".

Now that we are thinking in terms of set theory, we can say that by making some aspects undefined, method SFont::Union(...) enlarges the subset represented by *this. Specifically, it removes all aspects that *this and rhs do not agree upon. If there is no agreement at all, the resulting subset is the entire universe. But wait, if we want to iteratively calculate the union of n singleton sets, as is the case in our text selection example, how do we start? An accumulating iteration needs to start with an identity element of the respective operation. We conveniently use std::nullopt as an identity element for our Union(...) function. The identity element for union is the empty set and thus you can think of std::nullopt as the empty set in this example.

With std::nullopt serving as a convenient, generic identity element for any accumulating algorithms, and with the base type X for std::optional<X> being implicitly provided as the range value type, we can wrap this approach into a generic algorithm. We call it tc::accumulatewithfront(...), because the iteration starts with the first element of the range, rather than with an explicit start element. If the range is empty, std::nullopt is returned. Note that we do not need the helper function Union(std::optional<SFont>&, SFont) anymore, because it is implicit in the definition of tc::accumulatewithfront(...):

template<typename RngFont>
std::optional<SFont> CollectFont(RngFont const& rngfont) noexcept {
    // see our public domain range library at
    // https://github.com/think-cell/think-cell-library
    return tc::accumulate_with_front(rngfont, TC_MEM_FN(.Union));
}
Listing (6)

The notion of SFont being a representation of a subset of fonts can also be useful when we want to know whether applying one font on top of another would make any difference: We can now phrase this question as an IsSupersetOf(...) predicate. Here are two implementations that are equivalent with regard to their results, although one uses more memory and more operations than the other:

#define IS_SUPERSET_OF_ASPECT(member) \
    (!member || rhs.member && *member==*rhs.member)

bool SFont::IsSupersetOf(SFont const& rhs) const& noexcept {
    return IS_SUPERSET_OF_ASPECT(m_ostrName)
        && IS_SUPERSET_OF_ASPECT(m_onSize)
        && IS_SUPERSET_OF_ASPECT(m_obBold);
}
Listing (7)
#define IS_EQUAL_ASPECT(member) ( \
    static_cast<bool>(lhs.member)==static_cast<bool>(rhs.member) \
    && (!lhs.member || *lhs.member==*rhs.member) \
)

bool operator==(SFont const& lhs, SFont const& rhs) noexcept {
    return IS_EQUAL_ASPECT(m_ostrName)
        && IS_EQUAL_ASPECT(m_onSize)
        && IS_EQUAL_ASPECT(m_obBold);
}

bool SFont::IsSupersetOf(SFont const& rhs) const& noexcept {
    return rhs==rhs << *this;
}
Listing (8)

While IsSupersetOf(...) in itself may already be useful in some situations, in our software we had one problem to solve that was closely related but a bit more tricky: We needed to extract the relevant information from one font relative to another, in order to store it, e.g., as a default or for quick access. A typical example would be a user applying an arbitrary partially defined font to some label that already has a font. We wanted to avoid storing any redundant, unnecessary information, because that would then inhibit our hierarchical font composition, see expression (3). Similarly, you may want to eliminate any unnecessary aspects from the font before application to (as it happens to be the case in our software) PowerPoint, because calls to PowerPoint can be expensive. Enter Minimize:

#define MINIMIZE_ASPECT(member) \
   if( IS_SUPERSET_OF_ASPECT(member) ) member = std::nullopt

void SFont::Minimize(SFont const& rhs) & noexcept {
    MINIMIZE_ASPECT(m_ostrName);
    MINIMIZE_ASPECT(m_onSize);
    MINIMIZE_ASPECT(m_obBold);
}
Listing (9)

Note: Minimize is not set difference!

You probably noticed that the result of the SFont::Union(...) method is not actually the set-union of the two subsets that were passed as parameters. Rather, it is the smallest set that contains both subsets and can be represented by our definition of SFont. That's the beauty of the data structure as presented in this acticle: It is very simple yet very useful. We achieve the simplicity by dropping the actual values of anything "mixed". For our practical purposes, that is a reasonable trade-off.

When trying to grasp this slightly peculiar behavior of SFont::Union(...), I find it helpful and illustrative to look at SFont in terms of an n-dimensional space (n==3 in our toy example), that is spanned by its aspects "name", "size" and "boldness". A fully defined font is equivalent to a point. If one aspect of the font is "mixed" or "undefined", the partially defined SFont object can be seen as a line in the space of all possible fonts. Remove another aspect, and you're left with a plane. When no aspects are left, the resulting SFont object represents the entire space. In this space, the operator<<(lhs, rhs) as introduced above (4) calculates the projection of lhs onto rhs.

If you have any feedback, I'd be glad to hear from you. Don't hesitate to let me know what you think about partially defined properties!

Next up: Applying the same ideas to the design of SLineFormat turns out to be less straight-forward than you may expect.

— by Volker Schöch

Do you have feedback? Send us a message at devblog@think-cell.com !

Sign up for blog updates

Don't miss out on new posts! Sign up to receive a notification whenever we publish a new article.

Just submit your email address below. Be assured that we will not forward your email address to any third party.

Please refer to our privacy policy on how we protect your personal data.

Share