Platform and Ownership
ClaFi Framework Developer Manual
Introduction
The platform is the object an application is built against. It is the one place the framework meets the operating system, and it is where everything that is process-wide lives: the connection to the display server, the timers, the clipboard, the directory watches. An application holds exactly one, builds it first and destroys it last, and everything else in the process - the context, the forms, every control - stands on it.
import ClaFi.Platform.Windows; // Win32Platform, Win32Application import ClaFi.Platform.Wayland; // WaylandPlatform, WaylandApplication
Most code never names the platform. A control reaches what it needs through its context - the
clipboard through formContext().clipboard(), the animation controller through
appContext().animator() - and the framework's own services reach their owner through one
of two seams described in Reaching the Platform From Below. This manual is
for the reader who wants to know what stands where, in what order it is built and torn down, and how
to add a service of their own without breaking that order.
One rule is worth carrying from the start: nothing framework-wide is a static. Every object that serves the whole process has an owner, the owner is the platform or the application's context, and the object is built and destroyed at the moment its owner's member order says. A service that cannot hold a reference to its owner - a hook with no object in hand - reaches it through a standing pointer the owned object sets and clears, and a hook that finds none standing has found a broken order and says so.
Basic Usage Example
An application is Application<Platform, GpuBackend>, or a class derived from
it, built from the platform's Params. The second argument names the GPU backend the
application offers and may be left out, which offers none. The entry point is the whole of what a
program knows about its platform:
// Win32 - Direct2D beside the CPU backend int __stdcall wWinMain(HINSTANCE hInstance, HINSTANCE, wchar_t*, int) { using namespace ClaFi; Themes_App::ThemesApplication<Win32Platform, Direct2DBackend> app{ { hInstance } }; return app.run(); } // Wayland - no GPU backend is named, because this platform has none int main() { using namespace ClaFi; Themes_App::ThemesApplication<WaylandPlatform> app{ WaylandPlatform::Params{} }; return app.run(); }
Two aliases name the common pairings: Win32Application is
Application<Win32Platform, Direct2DBackend> and WaylandApplication is
Application<WaylandPlatform>. The CPU backend is the core's own and every
application has it, so naming no GPU backend is an answer rather than a gap:
Application<Win32Platform> draws on the CPU and offers nothing to turn on. One that
names Direct2D carries both and starts on whichever its config holds - see
The Backend.
WaylandPlatform's
constructor connects to the compositor, and where there is none it throws a
std::runtime_error naming what WAYLAND_DISPLAY held. Left uncaught, that
ends the process with the message on stderr, which is what a client started outside a session should
do. There is no probe to call first: building the application is the attempt.The Two Halves of a Platform
A platform is two classes in a base-derived pair. Platform, declared in the Context
module, is what the rest of the framework sees and is the same on every platform.
Win32Platform and WaylandPlatform derive from it, one per build, and are what
the entry point names. The base is the contract; the derived class is the owner.
Platform, the Neutral Half
Platform lives in ClaFi.Core.Context.FormContext and carries three kinds of
member:
| Kind | Members | Who answers |
|---|---|---|
| Statics | setApplicationId, createFormsConfigSchema,
restoreFormPlacement, storeFormPlacement, addFormToSession,
keyName, keyModifiers, beep, globalScaleFactor,
caretBlinkTime, processCpuTime, setCursor, cursor,
getCursorInfo, shellExecute, appDataPath,
documentsPath, executableDirectory, debugOutput, systemColorMode, events |
Each platform supplies the bodies in its own unit - Wayland.Platform.cpp, an
implementation unit of the Context module, and Windows_Facade.cppm. A static that
needs the display server reaches it through a standing pointer - see
The Standing Pointer. |
| Two virtuals | createWindow(IForm&, WindowRole, IForm* parentForm),
diagnosticLines() |
The derived platform, which holds what a window is made from. A form calls
createWindow through appContext().platform() when it builds its window.
diagnosticLines answers what the platform found the display server offering, one
line each, and nothing unless a platform says otherwise; the application writes them into the
diagnostic log as it starts. Wayland names the optional protocols present and missing. |
| One owned object, by reference | clipboard() |
The derived platform owns the Transfer::Clipboard as a member and names it to
the base at construction: Platform(Transfer::Clipboard&). |
The base holds a reference rather than the clipboard itself because of order: a base is built before any member of the derived class, and on Win32 the clipboard has to come after the OLE apartment the windows manager opens. So the derived class declares the clipboard where the order needs it and hands the base its address - before the clipboard is built, which a reference may be, nothing reading through it until the platform stands whole.
The Platform Built for the Display Server
Win32Platform and WaylandPlatform are the owners. Each derives from
Platform and from one further interface that says what its platform layer offers to
services standing below the context - the subject of The Empty Name and What
Stands Behind It:
export struct Win32Platform : public Platform, public PlatformImplementation::Windows::IMessageWindowFactory { ... }; export struct WaylandPlatform : public Platform, public PlatformImplementation::Wayland::IDisplayAccess { ... };
What each owns, and in what order, is the next section. The rule they share: a platform member is declared after everything it needs and before everything that needs it, and what the platform owns is reached through the platform - or, where a hook has no object in hand, through the standing pointer the owned object publishes.
Params
Each platform declares a Params struct, and IsPlatform asks for nothing
more than that a platform is constructible from one. It is what the entry point has and the platform
wants:
| Platform | Params | Meaning |
|---|---|---|
Win32Platform | HINSTANCE appInstance |
The instance handle wWinMain is given, which every window class and window is
registered under. |
WaylandPlatform | bool watchesClipboard{ false } |
Whether the application watches the clipboard rather than pastes from it. A compositor announces a selection to the client holding the keyboard focus and to nobody else, so a viewer of the clipboard asks here for the clipboard manager's channel, where the compositor offers one. |
What the Platform Owns
C++ builds members in declaration order and destroys them in reverse, and that is the whole of the lifetime machinery here. Each platform lists its members in the order they depend on each other, the comment on the list says why, and nothing else states an order anywhere.
Win32Platform
private: Params m_params; PlatformImplementation::Windows::WindowsManager m_manager{ m_params.appInstance }; PlatformImplementation::Windows::TimerManager m_timers{ m_manager }; Transfer::Clipboard m_clipboard{ *this };
| Member | What it is | Why here |
|---|---|---|
WindowsManager | Registers the window classes, opens the OLE apartment, and closes both on the way out. Every window is made from it. | First: everything below is made from it or lives inside what it opened. |
TimerManager | A message-only window and its WM_TIMER, which
every UiTimer ticks through. |
After the manager, whose class it is made from. |
Transfer::Clipboard | The clipboard, with its Windows half behind it - the
IDataObject OLE holds and the listener window. |
Last: its destructor renders the package onto the clipboard, which wants the OLE apartment still open. |
WaylandPlatform
private: PlatformImplementation::Wayland::DisplayManager m_display{}; PlatformImplementation::Linux::TimerManager m_timers{}; PlatformImplementation::Linux::DirWatchManager m_dirWatches{}; PlatformImplementation::Wayland::PollSource m_timerSource{ m_display, m_timers.eventFd(), ... }; PlatformImplementation::Wayland::PollSource m_dirWatchSource{ m_display, m_dirWatches.notifyFd(), ... }; Transfer::Clipboard m_clipboard{ *this };
| Member | What it is | Why here |
|---|---|---|
DisplayManager | The connection, the globals and the event loop. Its constructor connects and throws where it cannot. | First: it is the loop everything else is dispatched from. |
TimerManager | A thread that waits out the delays and an eventfd it
raises, which every UiTimer ticks through. |
Before its poll source, which names its descriptor. |
DirWatchManager | One inotify descriptor serving every
DirWatch. |
Same. |
Two PollSources | Each puts a descriptor in the display's loop with the call to make when it is readable, and takes it out again in its destructor. | After the display and the manager each polls for, so the descriptor is in the loop exactly while both stand. |
Transfer::Clipboard | The clipboard, with its Wayland half - the
wl_data_source behind the selection. |
Last: on the way out it hands the selection and its change handler back to a display that is still up. |
Order Is the Design
Two things follow from letting member order carry the lifetime, and both are deliberate.
First, there is no initialize() or finalize() on a platform. A member's
constructor is its initialisation and its destructor its finalisation, and a member declared in the
right place needs neither called. A separate finalisation step is for the case where something must
happen while a later member is still standing, and no member of either platform has that
case: the Win32 clipboard flushes in its own destructor, which runs before the manager's, and that is
the order it wants.
Second, the loop names none of what it polls. On Wayland the display would otherwise have to know
the timer and the watch managers, which would put them under it in the module graph and above it in
the ownership order at the same time. Instead the display offers addPollSource and
removePollSource, and the platform - which owns both the display and the managers -
registers each descriptor through a PollSource whose lifetime is the registration.
Where the Platform Stands in an Application
First Up, Last Down
Application<Platform, GpuBackend> holds its platform in a base, not a member:
template <IsPlatform PlatformType> class PlatformHolder { protected: PlatformType m_platform; }; export template <IsPlatform PlatformType, IsOptionalGpuBackend GpuBackend = void> class Application : private PlatformHolder<PlatformType>, public ApplicationBase { ... };
Bases are built in declaration order and destroyed in reverse, so the platform is built before
ApplicationBase - and with it the AppContext, the config and the
ThemesManager with its directory watch - and destroyed after them. Everything an
application owns is built on a standing platform and goes down on one. A UiTimer stopped
by a dying member finds the timer manager up; a directory watch removed by a dying themes manager
finds its manager up; a form's window is destroyed while the windows manager still holds its class.
ApplicationBase would be built after the context and
destroyed before it, and every one of the sentences above would be false.The Whole Lifetime
From the entry point's braces to their close, in order:
| Step | What happens |
|---|---|
| 1 | PlatformHolder builds the platform from its Params: the
platform's members in their declared order, each on the ones before it. The platform stands
whole from here. |
| 2 | ApplicationBase is built on it: the config path, the
AppContext holding a Platform& and the GPU backend's factory where
the application named one, the ThemesManager. |
| 3 | Application's constructor body calls
ApplicationBase::initialize(): the application id is stated to the platform, the
standard actions and the transfer formats are registered - the paste action connecting to the
platform's clipboard - the app menu is connected, the diagnostic log is built and given the
platform's diagnosticLines, the themes are connected, the config is read. |
| 4 | The application's own members and its run(): forms are built, each
asking the platform for its window and the context for a backend to draw it, and the main form's
loop runs on the platform's event loop. |
| 5 | The application's own members go. A form's root stops its animations while it can still name its form; a container stops its subtree's before detaching it - see Teardown. |
| 6 | ~Application calls finalize(): the config is saved where
the user allowed it, the diagnostic log is released. It runs once, however many times it is
called, so an application whose own members the save reaches calls it from its own destructor
first. |
| 7 | ApplicationBase's members go: the themes manager and its watch, then
the context, whose destructor stops the theme crossing. |
| 8 | The platform goes, its members in reverse: the clipboard flushes or hands its selection back, the timers stop, the display or the windows manager closes. |
AppContext
AppContext is what a form or a control means by "the application". It holds the platform
by reference and hands it out as platform(), and it owns what the application runs for
itself rather than what the platform makes from the operating system: the
AnimationController, as animator(), and the UpdateCheck, as
updateCheck(). The animation controller belongs here rather than to the platform because it is the application's -
every control's state fades, every glide, the theme crossing - stepped by one UiTimer
that the platform's timer manager serves. Its owner has to stand below the platform and above every
control, which is exactly where the context stands.
The update check holds a WebFetch while it asks: one request on a thread of its own,
answered on the UI thread through a UiTimer. A fetch cancels its request and joins its
thread in its destructor, and the context goes before the platform, so the timer it stops on the way
out finds the timer manager up.
A FormContext holds a const Platform& and hands out the one thing a
control asks the platform for directly, the clipboard, as clipboard(). It is not
const: the clipboard is the process's state rather than the context's, and a copy made
from a const context is still a copy.
The Backend
The platform makes a form's window; a backend draws into it. A form asks the context
for one as it is built - AppContext::createBackend(window, size) - and is handed the GPU
backend the application named while GPU acceleration is on, the CPU backend otherwise. The two kinds
are built from different things, and a concept states each:
| Concept | Built from | Why |
|---|---|---|
IsGpuBackend | (IPlatformWindow&, IntSize) |
It draws into a window the platform owns, and reads the native object it needs out of that window. |
IsCpuBackend | (IntSize) |
It owns its pixel buffer, which the window presents. |
Besides its size, a backend is told one thing about its window: whether the window carries an alpha channel, so that a frame starts from nothing where the pixels outside a rounded corner are read.
The choice is made in the application's type. Application is a template and a form is
not, so it travels down as a function pointer - gpuBackendFactory<GpuBackend>(),
null where none was named - and AppContext::gpuAvailable() is that pointer's answer.
Where there is one, the config's Performance section holds a GpuAcceleration value, on by default, and the
Settings page shows the check that writes it; a change reaches every window at the top of its next
frame, and the window's canvas lets go of everything the old backend held. Where there is none,
neither the section nor the check exists.
Nothing else in the platform depends on which backend draws, and nothing in a control does. What the two draw, and where they still differ, is the Graphics manual's.
Reaching the Platform From Above
Code above the context - forms, controls, an application's own pages - never names
Win32Platform or WaylandPlatform. It reaches what it needs through the
context it already has, and the context knows only Platform.
Windows
A form asks for its window in its own member initialiser, through the context's platform:
std::unique_ptr<IPlatformWindow> m_window{ m_appContext.platform().createWindow(*this, m_windowRole, popupTargetForm()) };
createWindow is the virtual every platform answers. The derived platform answers with a
FormWindow made from what it holds - the windows manager on Win32, the display on
Wayland - and hands that to the window, so no window reaches for it by any other route. A popup is
handed its parent's window the same way, through the parent form. WindowRole says what
kind of window: Dialog, Menu or Tooltip, and nothing else.
Window Opacity
A window's opacity is a byte on its IPlatformWindow - alpha() and
setAlpha(ColorByte) - applied to the whole window at once. A tooltip fades in and out
this way, stepping the byte on AnimationSlots::formAlpha. Each platform applies the
value where it costs least:
| Platform | What applies it |
|---|---|
| Win32 | The window's composition visual. The shadow window the form casts its shadow from takes the same value, so the two fade together. Nothing is repainted. |
Wayland, where the compositor offers wp_alpha_modifier_v1 | The compositor, as a multiplier stated on the surface. A change is one commit - no paint, no pixel pass, no new buffer - and it applies to a buffer without an alpha channel too, so an opaque window fades the same way. While the window is off the screen the value waits in the surface's pending state for the commit that maps it. |
| Wayland, where it offers none | The frame, where the window's buffer carries an alpha channel. Every pixel is written at the window's opacity, and a change repaints the window. Premultiplied colour scales with its own alpha, so the four channels take one factor, the result is still premultiplied, and a rounded corner keeps the coverage it was drawn with. |
Zero takes a Wayland window off the screen on either path: a frame nobody can see is not worth drawing, and an unmapped window is how a tooltip says it is not there.
The Clipboard
The clipboard is formContext().clipboard(), a Transfer::Clipboard&. A
copy puts a package on it with the input event that asked; a paste asks it for an offer:
// a copy, from a handler that has the click's stamp Transfer::Source source{}; source.add<Transfer::PlainText>(selectedText()); formContext().clipboard().set(std::move(source), stamp); // a paste if (Transfer::Offer* offer = formContext().clipboard().offer()) { if (const std::optional<std::wstring> text = Transfer::take<Transfer::PlainText>(*offer)) insert(*text); } // what to connect to instead of polling - scoped, because the clipboard outlives the listener ScopedEventConnection watch{ formContext().clipboard().events() .connect<Transfer::ClipboardChangeEvent>([&](Transfer::ClipboardChangeEvent&) { refresh(); }) };
Where only a form is at hand, form->context().clipboard() is the same object. The
clipboard's own manual is the Transfer footnote under Source/.seedocs/footnotes; what matters here is its
place: one per process, the platform's member, alive from before the first window to after the
last.
Animations
A control runs its animations through Control::animate, which reaches the application's
controller through the control's form:
animate(AnimationSlots::expander, m_expandedFactor, target, [this](AnimateParams& params) { m_expandedFactor = params.value; invalidateChevron(); }); stopAnimation(AnimationSlots::scroll); // one slot stopAnimations(); // every slot of this control
animator() answers the controller, or null while the control stands in no form - there
being no application to reach then. animate on a control in no form applies the end
value at once through the same callback: nobody sees an unattached control, and the end value is what
it is drawn from once attached. A form reaches the controller as
appContext().animator(); something that is not a control - the grid's cell highlights -
takes it from the control it belongs to and answers the null the same way.
What the Platform Reports
The platform reports what the user does to the form whose window it happened in, through
IForm: a pointer move, a press, a wheel, a key, a side button. The form turns each report
into the framework's input state, which lives in Input - one per application rather
than one per form, since the focus leaves one window as it arrives in another.
The Input Controllers
The mouse and the keyboard are each an InputController: whether it drives the input,
and a factor that follows that through a fade of its own. Code outside Input reads
them and never writes them:
const InputController& mouse = Input::mouse(); const InputController& keyboard = Input::keyboard(); bool byKey = !mouse.active(); // whether a key did what is being handled float ringShare = keyboard.factor(); // active() through its fade, from 0 to 1
What the form is told switches them:
| Report | Mouse | Keyboard |
|---|---|---|
| A pointer move, a press or a release, a wheel, a side button, a menu asked for at a point | On | Off |
| A key, or a menu asked for from the keyboard | Off | On |
| A bare modifier - Shift, Ctrl, Alt | As it is | On |
So both are on only after a modifier, until the next pointer report or key, and the one state that cannot occur is both off. A modifier names no destination - it qualifies the click or the key that follows, and the pointer is still where the user left it. Two reports switch nothing: a held key's repeats, and the first move a form hears since the pointer came in, which only hit-tests - Win32 sends a window that appears under a still pointer a move, and a Wayland enter arrives as one.
A click, or a menu the pointer raised, always finds the mouse on. A key other than a modifier finds it off, unless the pointer moved while that key was held down and repeating.
The fades run on the application's animator, reached through the AppContext the form
reporting the input hands in, so Input owns nothing that has to be released. The two
slots, AnimationSlots::keyboardActive and mouseActive, mirror each other, so
a switch from one device to the other is one crossfade. Each step of either fade raises
InputSwitchEvent on AppContext::events(), and every visible form repaints
whole on it: the colour rules read the two factors as RuleInput::Keyboard and
RuleInput::Mouse, and nothing knows which controls of which windows do. The
Colour manual says what the rules do with them.
Side Buttons
The two side buttons most mice carry are reported as SideButton::Back and
SideButton::Forward - named by what the system takes each to mean, not by where it
sits on the mouse:
| Platform | Back | Forward |
|---|---|---|
| Win32 | XBUTTON1 | XBUTTON2 |
| Linux | BTN_SIDE, or BTN_BACK on the few mice that
report it | BTN_EXTRA, or BTN_FORWARD |
A side button acts as it goes down, and a second press inside the double-click time is one more
press. On Win32 the window answers every side button message itself, so the system raises no
WM_APPCOMMAND for the same press.
The form hands the press to the control under the pointer as a SideClickEvent,
carrying the button, and walks it up the parents until one stops it. A control answers with an
OnSideClick handler, or, where it answers for everything inside it, by overriding
nestedSideClick:
// Whatever the pointer stands on inside the browser, a side button is its Back or Forward. void BrowserControl::nestedSideClick(SideClickEvent& event) { Panel::nestedSideClick(event); // emits; a handler may stop it if (event.propagationStopped()) return; event.stopPropagation(); const std::ptrdiff_t offset = event.button == SideButton::Back ? -1 : 1; travel(offset, historyButton(offset)); }
Reaching the Platform From Below
Some of the framework's own services stand below the context in the module graph - the
clipboard in Transfer, the timer hooks in System - and cannot import the module that declares
Platform, because Context imports them. They still need the platform: the clipboard
wants a listener window on Win32 and the display on Wayland, and a UiTimer wants the
timer manager. Two seams serve them, and which one applies depends on whether the service has an
object in hand.
The Empty Name and What Stands Behind It
IPlatformServices, declared in ClaFi.Core.System.UiTypes, is a class
with a virtual destructor and nothing else. It is the platform as a service below the context can
name it, and a service that is handed the platform holds it under this name:
export class Clipboard { public: explicit Clipboard(IPlatformServices&); ... private: IPlatformServices& m_platform; };
What stands behind the name is each platform layer's own statement, in its own module, and the platform built for the display server derives from it:
| Platform | Interface | Declared in | What it offers |
|---|---|---|---|
| Win32 | IMessageWindowFactory | ClaFi.Platform.Windows.Window |
createMessageWindow() - a MessageWindow made from the windows
manager, for a service that needs an HWND to listen on. |
| Wayland | IDisplayAccess | ClaFi.Platform.Wayland.Display |
display() - the DisplayManager, for the data device, the
selection and the change handler. |
The service's platform half - the unit compiled for that platform alone - casts the name to what its platform stands behind it:
// Windows.Transfer.cpp IMessageWindowFactory& platform = static_cast<IMessageWindowFactory&>(owner.m_platform); listener = platform.createMessageWindow(); // Wayland.Transfer.cpp return static_cast<IDisplayAccess&>(owner.m_platform).display();
The cast is sound because a build has one platform: the object behind an
IPlatformServices& in a Windows build is a Win32Platform, which derives
from IMessageWindowFactory, and there is no other. The cast is also what keeps the neutral
name empty. Nothing is declared on IPlatformServices that only one platform can answer,
so no platform is made to answer for a thing it has no notion of - Wayland has no message window, and
Win32 has no display object - and the neutral module carries no method that half the builds would
stub.
Platform itself does not derive from IPlatformServices; the
derived platform does, beside Platform. That keeps the conversion from either platform to
the name unambiguous, and keeps Context out of the business of naming the seam at all.The Standing Pointer
A hook has no object in hand. PlatformTimer::start and stop are free
functions a UiTimer calls; a DirWatch's platform half is built with nothing
but the watch; a Platform static is a static. These reach the platform's object through
a pointer that object sets as it is built and clears as it goes:
export class TimerManager { public: [[nodiscard]] static TimerManager* standing() { return s_standing; } ... private: inline static TimerManager* s_standing{ nullptr }; }; // the hook void start(UiTimer* timer, MilliSeconds delay) { TimerManager* manager = TimerManager::standing(); if (manager == nullptr) unreachable("PlatformTimer: no timer manager stands - a UiTimer outlives the platform"); ... }
| Pointer | Set by | Read by |
|---|---|---|
TimerManager::standing() (Win32 and Linux) | The platform's timer manager | PlatformTimer::start and stop, the two hooks
ClaFi.Core.System.Timer declares and each platform defines. |
DirWatchManager::standing() (Linux) | The platform's watch manager | DirWatch::Impl, the Linux half of a directory watch. |
DisplayManager::standing() (Wayland) | The platform's display | The Platform statics that need the display server: the cursor, the modifiers,
the application id, the session. |
This is not a singleton. The object is a platform member with a declared place and a declared
lifetime; the pointer is a non-owning view of it, and it is null exactly while the platform holds no
such object. That null is the whole of the difference: a singleton built on first use would answer a
hook called after the platform has gone by building a new manager, silently and at process exit. A
standing pointer answers it with unreachable, because the platform stands before anything
that owns a timer or a watch and after it - see First Up, Last Down -
and a hook finding none standing has found a broken order.
Do:
- Hand the platform's object to whatever you build from it - a window its manager, a
buffer its
wl_shm- and hold it as a reference member. - Use the empty name and a cast when the caller is a service below the context that is handed the platform once.
- Use a standing pointer only for a hook with no object in hand, and make none standing
unreachable.
Don't:
- Don't add a method to
IPlatformServices. A method there is one every platform must answer, and the name exists to be empty. - Don't write a
static X& instance()or a function-local static holding framework state. It has no place in the order and dies at process exit, after everything that would have released it. - Don't answer a missing standing pointer by doing nothing. The silence hides the order that is wrong.
The Platform's Own Objects
What each platform layer builds for its owner, and the two or three rules each carries.
On Win32
WindowsManager is the root of the Windows layer. Its constructor sets the
process DPI awareness, opens the OLE apartment the clipboard lives in, and registers the four window
classes; its destructor closes the apartment, posts the quit message and unregisters the classes. Every
window is handed the manager by whoever makes it - the platform to a FormWindow, the
platform to the timer's and the clipboard's message windows - and the manager has no static accessor.
The one static state it keeps is the desktop's colour mode as last read, which a broadcast is compared
against.
MessageWindow is a message-only HWND on WindowBase alone -
no frame, no placement, no alpha, none of what a platform window has. Whoever holds one names itself
as its IMessageSink and receives every message the window does, exactly as a
Window subclass would without being one; what the sink leaves unhandled goes to the
system's default. IMessageSink is also what the manager's window procedure delivers to for
every window it made, by the pointer in the window's user data - a Window and a
MessageWindow alike.
TimerManager owns a MessageWindow and is its sink for
WM_TIMER. It publishes standing() for the two hooks.
On Wayland
DisplayManager is the connection, the bound globals and the event loop.
Its constructor connects and throws where it cannot; waitForEvent sleeps on the
connection's descriptor, every registered poll source and the appearance bus, and
dispatchPending hands every sink what arrived. It publishes standing() for the
Platform statics, and nothing else reaches it by name: windows are handed it, and the
clipboard casts to IDisplayAccess.
PollSource is a descriptor's place in the loop, held for as long as
the object stands: addPollSource in its constructor, removePollSource in its
destructor, with the call to make when the descriptor is readable. The platform declares one per
manager, after the display and the manager it polls for.
Linux::TimerManager is a thread that waits out the delays and an
eventfd it raises; Linux::DirWatchManager is one inotify descriptor. Both
live in Platform/Linux because they are the operating system's, not the display server's;
both publish standing(); neither knows the loop that polls it.
The Clipboard's Two Halves
Transfer::Clipboard is the worked example of a service that is one object with a
neutral half and a platform half. The neutral half - the package this process stands behind, the
held reader, the change dispatcher, the generation - is the class's own members, declared in
ClaFi.Core.Transfer.Clipboard. The platform half is NativeClipboard: a
struct the interface declares without a body and each platform's implementation unit of the same
module defines, held through a unique_ptr so the interface names no platform type.
| Win32 | Wayland | |
|---|---|---|
| NativeClipboard holds | The IDataObject OLE keeps a reference to, the
clipboard sequence numbers, the retry flag, the listener MessageWindow. |
The wl_data_source behind the selection and the offer handle the reader was
built for. |
| Reaches the platform by | Casting to IMessageWindowFactory on the first
watch, for the listener window; it is the window's sink. |
Casting to IDisplayAccess, for the data device, the selection and the change
handler it installs on the first watch. |
| On the way out | Renders the package onto the clipboard where
OleIsCurrentClipboard says it is still there, so a copy outlives the process;
removes the format listener. |
Destroys the standing source and clears the display's change handler. |
The constructor and destructor of Clipboard are defined in the platform unit, where
NativeClipboard is complete. Nothing above the platform layer sees the difference.
Teardown
Construction order is the easy half; the framework's own objects are built by whoever owns them, in the order the owner declares. Teardown is where a design of this shape earns its keep, because the things going down still reach for the platform, and the platform has to be there.
| What goes | What it reaches for | Why it finds it |
|---|---|---|
| A control destroyed in place | The animation controller, to stop what it was running
(Control::~Control calls stopAnimations). |
It is still attached, so its form and the context are reachable. |
| A container's children | The same. | They go down detached, so releaseChildren stops the whole subtree's animations
first, while the container can still reach the controller. |
| A form's root | The same. | A root has no parent, so FormControlBase's destructor stops its animations
while getForm still names its form - by the time Control's destructor
runs, getForm walks the parents and finds none. |
Any UiTimer | The platform's timer manager, through
PlatformTimer::stop. |
Every UiTimer is owned by something the application or its context owns - a
control, a form's tooltip, the animation controller, a directory watch, a web fetch - and the platform
outlives all of them. The hook finding no manager is unreachable. |
The themes manager's DirWatch | The platform's watch manager. | ApplicationBase's members go before the platform. |
The update check's WebFetch | Its own thread, and the timer manager for the timer it answers through. | Its destructor cancels the request and joins the thread while the timer manager stands: the check is the context's member, and the context goes before the platform. |
AppContext | Its own animation controller, to stop the theme crossing. | The controller is its member, stopped in its destructor body before the members go. |
| The clipboard | The OLE apartment, or the display. | Declared last in the platform, so it goes first. |
What Stays Static, and Why
The rule is not "no statics"; it is that no static holds anything that must be released in an order. A timer, a handle, a thread, a protocol object, a listener on something else - each of those has an owner with a declared place. What remains static holds none of them:
| Static | What it holds |
|---|---|
The Platform statics | Functions, and one value: the cursor last set. A
static that needs the display server on Wayland reaches it through
DisplayManager::standing(). |
Platform::events() | One dispatcher for the process, where
SystemColorModeEvent is raised. It stands before the first window and after the
last, which is the safe direction for a scoped connection. |
Input | Which control has the focus, which is hovered, whether a button
is down, and the two input controllers: pointers to controls that clear themselves on the way
out, and values. Each controller's fade runs on the application's animator, reached through
the AppContext the form reporting the input hands in - see
The Input Controllers. |
| The Transfer tables | Registrations filled by calls from
ApplicationBase::initialize, never at static initialisation - a static library
drops a translation unit nothing references. |
| The text engine's caches | Layouts, glyphs, draw buffers, the spare buffers on Linux. They are caches: what they hold is rebuilt on demand and released in no order. |
| The Wayland appearance portal | A lazily connected session bus, read for the desktop's colour mode; its descriptor is polled by the display's loop while it is open. |
| libcurl, on Linux | Nothing a variable holds: the library is opened with
dlopen when a fetch runs and never closed. A transfer aborted while a name resolves
leaves libcurl's resolver thread running its code, and unloading the library would take that
code from under it. |
WindowsManager::appsColorMode | The desktop's mode as last read, which a broadcast is compared against. |
| The diagnostic log | A form, held in a static unique_ptr in the Diagnostic
module. It is the one static whose contents need an order, and ApplicationBase::finalize
is what keeps it: the log is released there, ahead of the context and the platform going. |
Adding a Platform Service
The questions to answer, in order:
- Who owns it? A service the platform layer makes from the operating system - a
window, a descriptor, an apartment - belongs to the platform. A service the application runs for
itself - the animations - belongs to
AppContext. Nothing belongs to a namespace. - Where in the list? After everything it is made from, before everything that
is made from it. On Wayland, a descriptor the loop must poll gets a
PollSourcedeclared after both the display and the service. - Who reaches it, and from where? Code above the context: give the context an
accessor, as
clipboard()andanimator(). A service below the context that is handed the platform once: add a method to the platform layer'sIPlatformServicesderivative -IMessageWindowFactoryorIDisplayAccess- never to the neutral name. A hook with no object in hand: astanding()pointer on the service, set and cleared by the service,unreachablewhen null. - Where do the bodies go? A class member is defined in the module that declared
its class, so a platform-neutral interface with platform bodies gets one implementation unit per
platform of the same module -
Windows.Transfer.cppandWayland.Transfer.cppare bothmodule ClaFi.Core.Transfer.Clipboard;. Its platform state goes behind a struct the interface declares and the platform unit defines, held through aunique_ptr. - What does it do on the way out? Its destructor, and only that. If it must act while a later platform member still stands, the order is wrong; move it.
Reference
The platform's neutral half, ClaFi.Core.Context.FormContext:
| Member | What it is |
|---|---|
Platform(Transfer::Clipboard&) | Names the clipboard the derived platform owns as a member. |
clipboard() | The one clipboard, a Transfer::Clipboard&. |
createWindow(IForm&, WindowRole, IForm* parentForm) | A form's window, answered by the derived platform. |
diagnosticLines() | What the platform found the display server offering, one diagnostic log line each. Empty unless the derived platform answers. |
| The statics | See Platform, the Neutral Half. |
The two platforms and their facades:
| Name | Module | Notes |
|---|---|---|
Win32Platform, Win32Application | ClaFi.Platform.Windows |
Params{ HINSTANCE appInstance }. Derives from IMessageWindowFactory.
Win32Application is Application<Win32Platform, Direct2DBackend>. |
WaylandPlatform, WaylandApplication | ClaFi.Platform.Wayland |
Params{ bool watchesClipboard }. Derives from IDisplayAccess. The
constructor throws std::runtime_error with no compositor.
WaylandApplication is Application<WaylandPlatform>. |
The seams:
| Name | Module | What it is |
|---|---|---|
IPlatformServices | ClaFi.Core.System.UiTypes |
The platform as a service below the context names it. Empty. |
IMessageWindowFactory, MessageWindow, IMessageSink |
ClaFi.Platform.Windows.Window |
What Win32 stands behind the name: a message-only window and the sink it delivers to. |
IDisplayAccess, PollSource | ClaFi.Platform.Wayland.Display |
What Wayland stands behind the name: the display, and a descriptor's place in its loop. |
TimerManager::standing() | ClaFi.Platform.Windows.Timer,
ClaFi.Platform.Linux.Timer | For PlatformTimer::start and
stop. |
DirWatchManager::standing() | ClaFi.Platform.Linux.DirWatch |
For the Linux DirWatch::Impl. |
DisplayManager::standing() | ClaFi.Platform.Wayland.Display |
For the Platform statics. |
What the context hands out:
| Accessor | Answers |
|---|---|
AppContext::platform() | The Platform&. |
AppContext::animator() | The application's AnimationController. |
AppContext::updateCheck() | The application's UpdateCheck. |
AppContext::createBackend(window, size) | A form's backend: the GPU backend while one was named and GPU acceleration is on, the CPU backend otherwise. |
AppContext::gpuAvailable(), usingGpu() | Whether the application named a GPU backend, and whether the windows draw through it now. |
FormContext::clipboard() | The platform's Transfer::Clipboard. |
Control::animator() | The controller through the form, or null in no form. |
Control::animate(slot, current, end, onAnimate) | Runs an animation through it, or applies the end value in no form. |
Control::stopAnimation(slot), stopAnimations() | The stops, by the same route. |
AppContext::events() | Where InputSwitchEvent is raised, at
every step of either input controller's fade. |
What the platform reports, as the framework holds it:
| Name | Module | What it is |
|---|---|---|
Input::mouse(), Input::keyboard() | ClaFi.Core.Foundation |
The two input controllers, read-only. |
InputController::active(), factor() | ClaFi.Core.Foundation |
Whether the device drives the input, and that through its fade. |
InputSwitchEvent | ClaFi.Core.Context.AppContext |
The controllers' factors have moved. Carries nothing. |
SideButton | ClaFi.Core.System.UiTypes |
Back or Forward. |
SideClickEvent, Control::OnSideClick, nestedSideClick |
ClaFi.Core.Foundation | A side button pressed over a control, walked up its parents. |
IPlatformWindow::alpha(), setAlpha(ColorByte) |
ClaFi.Core.Context.FormContext | The window's opacity - see Window Opacity. |