ClaFi FAQ

The question, the answer, and the reasoning folded away

Editors and tooling

Why is the source covered in red squiggles?

Because MS sucks.

Why

Visual Studio's own C++ IntelliSense does not understand C++20 modules - it has called that support experimental for years - so it paints the source red however healthy the code is. Nothing in the code it marks is wrong, and nothing you write there will make the marks go away.

Two editors read ClaFi properly. ReSharper C++ does, with no setup. So does clangd: configure the CMake build once and point your editor at the compile_commands.json it writes into the build directory.

Why does clang say a member is not yet bound in my constructor?

Because clang checks a constructor template without the default member initializers. Build the member where it is declared, and hand it the argument in the constructor body.

Why

A control holds its children as members, each built by its default member initializer, and members are built in the order they are declared. An entry of the constructor's initializer list that reads a member declared above it reads one that is already built, so the code is correct C++.

Clang's -Wuninitialized checks a constructor template as if those initializers were not there, and a control's constructor is a template, because it forwards the properties it is given. So every such read is reported: reference 'm_topBar' is not yet bound to a value when used here for a reference, field 'm_count' is uninitialized when used here for a pointer or a value. A constructor that is not a template is checked with the initializers, and nothing is reported.

The child that needs the argument is built where it is declared, like every other child, and the argument reaches it in the constructor body:

class TitledPage : public Panel
{
public:
    template <typename... Args>
    TitledPage(const CreateParams& params, std::wstring_view title, Args&&... args);
private:
    Panel& m_topBar{ createTopBar<Panel>(Padding{ 12.0f, 8.0f }) };
    Label& m_title{ m_topBar.createBody<Label>(VerticalTextAnchor::Center) };
};

template <typename... Args>
TitledPage::TitledPage(const CreateParams& params, std::wstring_view title, Args&&... args)
    :
    Panel{ params, std::forward<Args>(args)... }
{
    m_title.text() << TextStyleId::SubTitle << title;
}

Layout and scrolling

Why are scroll bars missing?

State WordWrap::No on the scrolled control.

Why

The symptom: a control inside a ScrollBox draws its content correctly, is visibly cut off at the edge of the box, and neither bar is on screen — including under ScrollBars::Both, where both bars were asked for by name.

Under Both the bars are up. A bar with nothing to carry reports disabled (SliderBase::getControlState answers maxPos() > 0), and a disabled bar paints its thumb and its buttons away whatever their own states say. So an empty view is not the box refusing to make a bar. It is the box saying the content is no larger than the view, and the question is why the content came out that size.

The body wraps

This is the usual answer, and it is silent. Control::wordWrap() is !(m_flags2 & cfNoWordWrap) — true for every control by default, including one that holds no text at all. Control::alignContent then reads it:

if (wordWrap())
{
    // Measured again against the slot it was granted, and the slot is the viewport.
}
else if (isOnScrollBox())
{
    // A scrolled body keeps the size it measured.
    newDimensions.y = std::max(calculatedContentHeight, newDimensions.y);
}

"A scrolled body keeps what it measured" lives in the second branch only. A wrapping body never reaches it: it is measured again against the slot, so it can never come out larger than the view. For a control that draws its own content and holds no text the re-measurement answers {0, 0}, which is discarded rather than taken, so nothing overrules the slot either and the body ends up exactly the viewport. max == page, nothing to carry, both bars paint away.

The flag belongs to the body, and there are three places to state it. As a construction property where the body is made, which is how a stock control such as a TextBox takes it:

BodyProps{ ReadOnly::Yes, WordWrap::No, Padding{ 12.0f } }

In the constructor of a control you are writing that can never wrap, ahead of the forwarded arguments so that Props still takes a stated one — it reads the last match:

template <typename... Args>
HexView::HexView(const CreateParams& params, Args&&... args)
    :
    Control{
        params,
        // A line is a fixed grid of characters and nothing about it can be broken to a box.
        WordWrap::No,
        std::forward<Args>(args)... }
{
}

Or through setWordWrap(WordWrap::No) on a control already built.

Do

  • State it where the body is made, in the properties that build it. That is the answer for any control whose source you are not editing.
  • A control that can never wrap says so in its own constructor instead, so that no caller has to remember. ComboBox and HexView both do.

Don't

  • Assume a control holding no text is unaffected. wordWrap() is true by default for every control, and one that draws its own content is exactly the case where the wrapping branch measures {0, 0} and the slot stands.
  • Reach for MinSize to force the body taller. The size it measured is already right; what is missing is permission to keep it.
The same flag decides the width. ScrollBox::adjustChildMetrics states the viewport as a wrapping body's maximum width, so such a body is never wider than the view and the horizontal bar has nothing to carry either. One flag, both bars.

ScrollBars::Auto on a box that does not measure itself

The other cause, and the one WordWrap::No does not answer. Auto is decided in ScrollBox::calculateContent, against the box's own maximum size — that is what lets the measuring pass answer it in one go, with no correction afterwards. So it reaches a box that measures itself: one under a MaxSize, or one that is an auto-fitting window's whole content. A box sized by whatever contains it has no maximum of its own and never shows an Auto bar. Such a box states the bars it wants instead, with Both, Vertical or Horizontal.

wordWrap is a text property standing in for a question about reflow — does this control's measurement depend on the width it is given? Anything whose answer is no has the trap waiting, not just a control that draws its own content: a container of naturally-sized items in a scroll box is the other shape it takes.

Colour and themes

Why doesn't the application follow the desktop's dark mode?

Choose Auto (Follows System) on the Settings page.

Why

The symptom: the desktop is dark and the application opens light, or the desktop changes mode and the application stays on the side it was.

The config states a mode outright

Auto is the default of a config that states nothing. A config stating ColorMode=Dark or ColorMode=Light keeps that side whatever the desktop does: a click on a mode button stores one, and an application may state its own first-run default - the TextEngine showcase states Light. Stating Auto is the whole of the fix:

applyColorMode(appContext(), ColorModeSetting::Auto);

Windows is asked for its app mode

Windows keeps two modes: one for Windows itself - the taskbar and the Start menu - and one for applications. Auto follows the second, Choose your default app mode in the colour settings. A dark taskbar over a Light app mode is a light desktop to every application.

A Linux session without a portal

On Wayland the mode comes from the XDG desktop portal, which GNOME, KDE Plasma and most desktops start with the session. A session without a portal, or with a portal whose backend serves no org.freedesktop.appearance, states no preference, and no preference reads as Light. What the portal says is one call away:

busctl --user call org.freedesktop.portal.Desktop /org/freedesktop/portal/desktop \
    org.freedesktop.portal.Settings ReadOne ss org.freedesktop.appearance color-scheme

It answers v u 1 for dark and v u 2 for light. v u 0 is no preference, and it is what GNOME answers for its default light look.

Do

  • Leave the choice to the user. Auto is the default because the desktop already says what the user wants.
  • State a first-run default only where the application needs one side, and only where configFolderExists() is false.
  • Ask wornColorMode for the side the application is drawn on.

Don't

  • Write a mode over a loaded config at start. The user's choice arrives with the load, and a mode written after it undoes the Settings page on every run.
  • Read the setting as a side. Under Auto it names none, and a test for ColorModeSetting::Dark is false on a dark desktop.

Text and links

Hold Ctrl while you click.

Why

The symptom: a link in a TextBox is drawn in the accent ink, but a click on it puts the caret there and nothing opens. The pointer stays an I-beam over it and no underline appears.

The box is one the user types into

This is the usual answer. In a box the user can edit, a plain click places the caret, the way it does anywhere else in the text - a link is a word the user may want to change. Ctrl+click follows it, and with Ctrl held, a link the pointer moves onto is underlined and the pointer becomes a hand. The tooltip over the link says so too: in an editable box it ends with a muted Ctrl+click to follow.

A box that is there to be read follows a link on a plain click. That is a property of the box:

BodyProps{ text, ReadOnly::Yes, Padding{ 16.0f } }

The press became a drag

A press that selects text between going down and coming up was a drag, and follows nothing - even when it starts and ends on the same link. A pointer that moves a few pixels while the button is down can select a character.

The box is disabled

A disabled box takes no input, so it follows nothing, underlines nothing, and shows the arrow over its links.

The underline and the hand are the test: they appear exactly when a click would follow the link. A link that shows both and still opens nothing is the next question.

Do

  • Make a box that is there to be read ReadOnly::Yes. Its links then follow on a plain click, and its caret and selection still work.

Don't

  • Follow a link on a plain click in a box the user types into. Every click that meant to place the caret inside a link would leave the page.

Answer it in OnLinkClick.

Why

The symptom: the link is underlined under the pointer and the pointer is a hand, so a click follows it - and nothing opens.

The box opens three schemes

A followed link raises OnLinkClick with its target. What no handler stops, the box opens through the shell - but only a target starting with http:, https: or mailto:. A text can come out of a file or off the clipboard and name anything, and the shell runs a program it is handed, so every other target - file:, a bare path, a scheme of the application's own - goes nowhere until the application answers it:

box.onLinkClick([](LinkClickEvent& event) {
    if (!event.target.starts_with(L"file:"))
        return;
    // Open what the application trusts, and only that.
    openDocument(event.target);
    event.stopPropagation();
    });

A # link to an anchor the box does not hold

A #name the box's own text holds is followed by the box, with no event raised. A name it does not hold is raised to OnLinkClick like any other target, which is how a link reaches an anchor on another page: the handler opens that page and calls goToAnchor on its box. With no handler, it goes nowhere.

Do

  • Stop the event once a handler has answered the target, so the box does not act on it as well.
  • Check a target before opening it. The text it came from may not be the application's own.

Don't

  • Hand every target to the shell from a handler. That undoes the one guard the box keeps, for every text that ever reaches it.
  • Leave the event running after opening an https: link yourself. The box opens it a second time.

The whole of what a followed link does is in the TextEngine manual, under Following a link in a TextBox.

Settings and documents

Why does the build say a type must define serializedFields?

Import the serializer's module where the value is read or written, or re-export it from the module that states the struct's fields.

Why

The symptom: a struct gains a Color member - or any other type with a ScalarSerializer - and the build stops in the Dom engine at static assertion failed: 'Type must define k_serializedFields, serializedFields member, or an ADL serializedFields() function.' The serializer exists, and the struct's fields are stated.

The serializer is out of sight

The engine decides whether a type is a scalar by looking for its ScalarSerializer in the file that reads or writes the value. Where none is in sight, the type is taken for a struct, and a struct whose fields are not stated stops the build. The file sees a serializer when it imports the serializer's module itself, or imports a module that re-exports it. A plain import in a module further up the chain does not carry it.

The instantiation context in the error names the file the import belongs in - the get<T>(), set() or Dt::Value that reads or writes the struct - and further down, the member type whose serializer it could not see.

The fields module re-exports what its members need

The module that states serializedFields for a struct is the one that knows which serializers its members need. Re-exported from there, they reach every file that can see the fields:

export module MyApp.Swatch;

export import ClaFi.Core.Dom_UiSerializers;

import ClaFi.Core.System.Serialization;
import ClaFi.Core.System.UiTypes;
import ClaFi.StdLib;

export struct Swatch
{
    Color fill{};
    Color stroke{};
};

export constexpr auto serializedFields(const Swatch&)
{
    return std::make_tuple(
        SerializedField{ L"Fill", &Swatch::fill },
        SerializedField{ L"Stroke", &Swatch::stroke }
    );
}

Do

  • Re-export the serializers a struct's members need from the module that states its fields.
  • Import ClaFi.Dom in application code that reads or writes values. It re-exports the standard and the UI serializers.

Don't

  • Import the serializer without export into a module further up the chain. The file that reads or writes the value still does not see it.
  • State fields for a scalar type to silence the error. Wherever its serializer is in sight the type is still a scalar, so one type would be written two ways.

Which module holds which serializer is in the Dom manual, under Where a Serializer Must Be Imported.

All manuals Top