Platform Seams¶
Every OS service a control needs sits behind an interface with a test double. A control calls
the interface; the Win32 host installs the real implementation; a test installs a recording or
scripted one. That is how a TextBox can copy, paste and show spelling squiggles in a unit test
with no clipboard and no dictionary.
The Seam Pattern¶
Each seam has the same four parts:
- An interface in
include/winui/render/(orui/for the window-level ones): a small abstract class with only the calls controls actually make. - A test double in
include/winui/testing/, namedTest<Seam>(TestClipboard,TestNativeHost, ...), part of thewinui::testinglibrary. It records what a control asked for and lets a test script the answer. It never touches the OS. - A Win32 implementation under
src/platform/windows/, header included: the backend is private and never installed. - An installer: on
Windowfor a per-window service (set_compositor(),set_spell_checker(), and so on), reached by a test throughui::testing::seams(window). The pointer is optional and non-owning. The Win32 host creates the real object, owns it for the window's lifetime and installs it onWM_CREATE; a test installs its double; a headless host usually installs nothing. The clipboard and the launcher belong to the process instead: the backend makes them on first use, and a test swaps them withui::testing::set_clipboard()andui::testing::set_launcher().
A missing per-window seam degrades gracefully. With no compositor, controls repaint without the fade; with no native host, a NativeControl
never creates its window. So the pure layout and focus tests need no seams at all.
How a seam reaches a control depends on who needs it:
| Delivery | Seams | Why |
|---|---|---|
Tree-wide, after every layout and input dispatch (sync_composition(), sync_native()) |
Compositor, NativeHost | Every visual needs them, focused or not (hover state, a native control's position). |
To the focused element only, right before a key or char (sync_spell_checker()) |
SpellChecker | Only the focused field edits text. It is also synced as a field gains focus, so it is checked before the first keystroke. |
Passed into the one call that may use it (handle_context_menu(position, host)) |
ContextMenuHost | A menu may only be shown during that call; a control never caches the host. |
Process-wide (ui::clipboard(), ui::launch_uri(), ui::message_box(), ui::notify(), ui::set_jump_list(), set_text_analyzer()) |
Clipboard, Launcher, MessageBoxHost, Notifier, JumpListHost, TextAnalyzer | There is one of each for the whole OS session, so no window has its own, and a control needs no path to its window to use them. |
| Seam | Interface | Test double | Win32 implementation | Installed with |
|---|---|---|---|---|
| Text measurement | TextMeasurer (render/Text.hpp) |
HeadlessTextMeasurer |
DWriteMeasurer (private) |
passed to Window::layout() |
| Drawing | Renderer (render/Renderer.hpp) |
TestRenderer |
D2DRenderer (private) |
passed to Window::render() |
| Composition | Compositor (src/render/Compositor.hpp, private) |
TestCompositor (src/testing/) |
DCompCompositor (private) |
Window::set_compositor() |
| Native controls | NativeHost (render/NativeHost.hpp) |
TestNativeHost |
Win32NativeHost |
Window::set_native_host() |
| Clipboard | Clipboard (ui/Clipboard.hpp) |
TestClipboard |
Win32Clipboard |
ui::testing::set_clipboard() |
| Spell checking | SpellChecker (render/SpellChecker.hpp) |
TestSpellChecker |
Win32SpellChecker |
Window::set_spell_checker() |
| Context menus | ContextMenuHost (render/ContextMenuHost.hpp) |
TestContextMenuHost |
Win32ContextMenuHost |
Window::set_context_menu_host() |
| Launching URIs and files | Launcher (ui/Launcher.hpp) |
TestLauncher |
Win32Launcher (private) |
ui::testing::set_launcher() |
| Message boxes | MessageBoxHost (ui/MessageBox.hpp) |
TestMessageBoxHost |
the task dialog in Win32MessageBox.cpp |
ui::testing::set_message_box_host() |
| Notifications | Notifier (ui/Notification.hpp) |
TestNotifier |
the toast notifier in Win32Notifier.cpp |
ui::testing::set_notifier() |
| Jump list | JumpListHost (ui/JumpList.hpp) |
TestJumpListHost |
the shell's destination list in Win32JumpList.cpp |
ui::testing::set_jump_list_host() |
| Window state and frame | WindowHost (src/ui/WindowHost.hpp, private) |
HeadlessWindowHost, through an adapter |
the HWND host in Win32WindowHost.cpp |
Window::set_window_host() |
| Text segmentation | TextAnalyzer (render/TextAnalyzer.hpp) |
the built-in base class | Win32TextAnalyzer (system ICU) |
set_text_analyzer() |
The step-by-step for a new one is on Add a Platform Seam.
The Backend Is Private¶
Every Win32 implementation (Win32Clipboard, Win32NativeHost, Win32ThemeSource,
Win32ContextMenuHost, Win32SpellChecker, Win32TextAnalyzer, D2DRenderer, DWriteMeasurer,
the DirectComposition compositor, Win32Launcher) lives in src/platform/windows/ and is not
installed. An app never names one: ui::run() and Window::show() install them, and what an app
used to ask a backend class directly has a portable function, such as ui::os_theme() for what
ThemeMode::System resolves to right now. The tests of the backend itself include these headers
from src/.
The seams are installed on a Window through ui::testing::seams(window)
(winui/testing/WindowSeams.hpp): the setters are private members of Window, so an app cannot
swap a service out from under a running window, and a test can still hand it a test double.
None of these headers includes <windows.h>. They name a window as ui::NativeHandle
(core/NativeHandle.hpp), which on Windows is HWND__*. Window::native_handle() and
NativeControl::native_handle() return it. Under STRICT, which <windows.h>
turns on unless NO_STRICT is defined, HWND is struct HWND__*, so forward-declaring
HWND__ gives the identical type: a caller with a real HWND passes it unchanged, and a caller
that never touches Win32 never pays for the header. Inside src/platform/windows/,
Win32String.hpp's widen() and narrow() are the only places a string crosses between the
framework's UTF-8 and the ...W APIs' UTF-16.
Clipboard¶
Clipboard is the system clipboard as an app sees it, trimmed to the three standard
formats an app actually puts there: text, a bitmap and storage items (file paths). It adds the
history and roaming switches of SetContentWithOptions, available_formats(), clear() and
content_changed(), a Property<std::uint64_t> counter that advances on every change so an app
can observe it. Reading is per format, so pasting text never decodes a bitmap that
happens to be there too. HTML, RTF, links, delayed rendering and custom formats are not covered.
TextBox uses set_text()/text() for Ctrl+C, Ctrl+X and Ctrl+V, through ui::clipboard(),
the one clipboard of the process. App code calls the same function, before ui::run() too. The
Win32 backend makes its Win32Clipboard on the first call and keeps it for the life of the
process; a backend without a clipboard returns one that lives in memory. HeadlessUI installs a
TestClipboard unless the test installed one first, so a test never writes to the real
clipboard.
Win32Clipboard writes these formats:
| Content | Formats written | Windows synthesizes |
|---|---|---|
| Text | CF_UNICODETEXT |
CF_TEXT, CF_OEMTEXT, CF_LOCALE |
| Bitmap | "PNG" first (alpha intact, what current apps paste first), then CF_DIBV5: 32 bpp, BI_BITFIELDS with an alpha mask, bottom-up |
CF_DIB, CF_BITMAP (without alpha) |
| Storage items | CF_HDROP (a wide DROPFILES list) plus "Preferred DropEffect" holding the requested operation as a DROPEFFECT |
|
| History and roaming switched off | "CanIncludeInClipboardHistory" / "CanUploadToCloudClipboard" set to 0 |
Pasting a bitmap prefers PNG, then CF_DIBV5, then CF_DIB, decoded by WIC. TestClipboard
holds one ClipboardContent in plain members a test can seed and read, and reports the same
format names the Win32 backend would.
Spell Checker¶
SpellChecker is the same shape: available(), language(), check(text) returning
SpellingError ranges with a suggested action, suggest(word), add_to_dictionary(),
ignore_for_session(), add_autocorrection(), and two user settings, highlight_enabled() and
autocorrect_enabled(). generation() moves whenever anything that changes results changes, so
a field re-checks without being told why.
Win32SpellChecker wraps the Windows Spell Checking API (ISpellChecker, Windows 8+) and owns
the policy that API leaves to its caller:
- The language is the current keyboard layout's tag (
GetKeyboardLayout, thenLCIDToLocaleName), re-derived onWM_INPUTLANGCHANGEandWM_SETFOCUS, since the layout may change while another app has focus. OneISpellCheckeris created per language on first use and kept for the process. A layout with no installed dictionary makes the checker unavailable rather than falling back to another language, and is cached as a null checker so it is not probed again on every keystroke. - The two Typing settings ("Highlight misspelled words", "Autocorrect misspelled words") are
undocumented registry values,
EnableSpellcheckingandEnableAutocorrectionunderHKCU\Software\Microsoft\TabletTip\1.7, read onWM_SETTINGCHANGEand when the window gets focus back. A missing value reads as on, Windows' own default, so an OS that drops them fails open. - Autocorrect has a small built-in table of classic English typos ("teh"), applied only when
the OS dictionary flags the word. App entries from
add_autocorrection()are keyed lowercase, and a correction takes the typed word's capitalization ("Teh" becomes "The", "TEH" becomes "THE"). - Offsets:
ISpellCheckerreports UTF-16 ranges, which are mapped back to UTF-8 byte offsets.
A caller just reads the two flags and never learns where they came from. COM is initialized lazily on the UI thread, which is normally in OLE's single-threaded apartment by then (the drop target needs it, see Drag and Drop). The spell-checker factory is free-threaded, so either apartment works; the class owns the initialization only when it made it. Every call must come from the UI thread.
A field is checked from the moment it first gains focus (the window syncs the checker in
set_focus() as well as before each key). It keeps the pointer after losing focus, so its
squiggles stay and it re-checks when generation() moves. A field that was never focused is
never checked, the same rule browsers apply to pre-filled text. Only TextBox checks; the
composites that own one do not forward it, since none of them hold prose.
TestSpellChecker takes the misspelled words and their suggestions up front, lets a test flip
the two settings, and counts what the control asked for. The Win32SpellChecker tests never
call add_to_dictionary() or autocorrect through the OS, since both write into the user's own
dictionary.
Text Analyzer¶
TextAnalyzer answers three questions a text field needs: where the grapheme clusters are
(caret movement, Backspace), which characters count as word characters (the run a double-click selects) and
how ui::CharacterCasing changes typed text. It is a process-wide seam, installed with
set_text_analyzer().
The base class is the fallback and what headless tests use. It is small and deterministic: a cluster is a code point plus the combining marks, variation selectors, emoji modifiers and ZWJ sequences after it; casing covers Latin, Greek and Cyrillic; anything past Latin-1 that is not in a punctuation or symbol block counts as a letter.
Win32TextAnalyzer loads the system ICU (icu.dll, Windows 10 1903 and later) at run time. It
runs a character break iterator over a UTF-8 UText, so no offset crosses to UTF-16, uses
u_isalnum plus the mark categories for word characters, and ICU's locale-aware case mapping.
The ICU functions are declared by hand, because the SDK's <icu.h> links against icu.lib, a
load-time dependency Windows before 1903 cannot satisfy. The break iterator holds the text it was
last given, so calls are serialized with a mutex. Without ICU, clusters and word characters fall
back to the base class and casing to LCMapStringEx linguistic casing in the user's locale, so
older Windows still edits text, just less precisely.
Native Host¶
NativeControl (ui/hosting/NativeControl.hpp) is the deliberate exception to "a control has no
HWND": a leaf that wraps a real Win32 child window (a RichEdit, a shell control, WebView2) for
things nothing in the framework can or should reimplement. NativeHost is the seam between that
leaf and whatever owns the real window. The control never calls Win32; it calls:
| Call | When |
|---|---|
ensure_created(id, desc) |
every sync_native(); idempotent, like Compositor::sync_visual() |
set_owner(id, control) |
right after creation, so the host can route a child's WM_CONTEXTMENU and notifications back |
place(id, bounds, visible) / set_bounds() / set_clip() |
whenever the arranged bounds or the visible part change |
set_visible(id, bool) |
Collapsed, scrolled wholly out of view, or no longer reached by the sync walk |
destroy(id) |
from the control's destructor |
native_handle(id) |
for control-specific messaging (WM_SETTEXT to a RichEdit); null in a headless host |
Everything is keyed by the control's VisualId and in DIPs; Win32NativeHost converts to device
pixels itself. Window::sync_native() wraps each pass in begin_sync()/end_sync(), so a control
the walk did not reach (a Tabs page that is no longer selected) gets hidden. The same pass
hands the window's spell checker to every native control, creation pass included, so a control
several levels down gets it after one sync. The control decides whether that means re-checking,
since only it knows whether its content changed.
TestNativeHost records the lifecycle (create once, move on every changed arrange, hide, show,
clip, destroy) as plain state, since that part is ordinary C++ behavior. It must outlive every
control synced against it.
Known limits, stated in the header: the native window is not theme-driven, it is not in the tab
ring (it takes real keyboard focus from Windows when clicked), and it is clipped only by
scrolling ancestors (through SetWindowRgn), not by rounded clips or render transforms. The
host window has WS_CLIPCHILDREN, so Direct2D never paints over a child. Hosting one is walked
through in Hosting Native Controls.
Moving a Child Window¶
After it moves a child, Win32NativeHost repaints it synchronously (RedrawWindow with
RDW_UPDATENOW) instead of trusting the WM_PAINT that MoveWindow queued. WM_PAINT is
synthesized, not posted: GetMessage only makes one once the queue holds nothing else, and a
parent's is served before its children's. While any animation runs, the host window is
invalidated after every message (see Message loop), so the child's paint could
starve for as long as the animation runs, and a scrolled control would show stale pixels.
MoveWindow's blit is kept (no SWP_NOCOPYBITS), so the redraw only refreshes what changed. One
extra paint per move is fine for an escape hatch used a few times per window.
The clip region is set before the move and without its own repaint, so the synchronous repaint
already paints inside it and a control scrolling out of a viewport never flashes its old region.
Region edges are rounded the same way set_bounds() rounds the window, so the clip meets the
viewport edge exactly. The system owns an HRGN only once SetWindowRgn succeeds, so a failed
call deletes it.
EditBox¶
native_controls::EditBox (examples/native_controls/edit_box.hpp) wraps the stock EDIT class,
the least capable native control, to show that the NativeControl escape hatch works with no
special handling anywhere. It is example code, not part of the library: the library ships the
extension point (ui::Native, ui::NativeControl), the gallery, tutorial 12 and the tests use
the example. It maps TextBox/PasswordBox properties onto EDIT messages and styles:
| Property | EDIT |
|---|---|
set_placeholder_text() |
EM_SETCUEBANNER |
set_max_length() |
EM_SETLIMITTEXT (0 restores the class limit) |
set_character_casing() |
ES_LOWERCASE / ES_UPPERCASE |
set_foreground() / set_background() |
answered from WM_CTLCOLOREDIT / WM_CTLCOLORSTATIC through NativeControl::handle_native_notification() |
set_number_only() |
ES_NUMBER, which TextBox has no counterpart for |
Multi-line, wrapping, alignment and password are creation-only styles and live in
EditBoxOptions; only ES_LOWERCASE, ES_UPPERCASE and ES_NUMBER can change after creation.
What the class cannot do, EditBox does not offer: character spacing, a selection color (it is
always the system Highlight), per-range formatting, Redo (EDIT keeps a single undo step) and spell
checking.
The class has a few traps the wrapper deals with:
- Border. It uses
WS_EX_CLIENTEDGE, notWS_BORDER. A themed EDIT draws its visual-style border into the client edge, and aWS_BORDERbox under the light visual style draws no border at all (dialog templates convertWS_BORDERfor the same reason). - Font. A stock EDIT with no font draws in
SYSTEM_FONT, the Windows 3.1 bitmap face. EditBox sets the message font at the window's DPI and rebuilds it when the window moves to a monitor with another scale. Before the window and font exist, a box with no height estimates 23 DIPs, one line of the 9 pt message font at 96 DPI, as a dialog lays an EDIT out. - Change notification. A multi-line EDIT sends no
EN_CHANGEforWM_SETTEXT(a single-line one does), so EditBox calls itson_text_changed()handler itself for text set from code. - Password mask.
EM_SETPASSWORDCHARtakes one UTF-16 unit, so a mask outside the BMP falls back to the default. Revealing the text sets a zero mask, but the box stays a password box with no Cut or Copy. - Colors. An enabled, editable EDIT asks for
WM_CTLCOLOREDIT, a read-only or disabled one forWM_CTLCOLORSTATIC, whose system answer is the dialog color. In a dark theme the EDIT gets theDarkMode_Explorervisual style (dark scroll bars) and answers both with the palette's text on the FluentTextBoxrest fill over the window's base background, made opaque because GDI paints with a solid brush. In a light theme it is the plain system EDIT. The rest of theTheme(era packs, high contrast colors) does not reach it. - Context menu. An EDIT answers
WM_CONTEXTMENUitself with its own system-themed popup and never tells the parent. EditBox subclasses it (SetWindowSubclass, hence thecomctl32dependency) so it forwards the message like RichEdit does, and the menu comes from the framework's context menu host in the window's light or dark palette. The items are RichEditBox's minus Redo and the spelling section. EDIT has noEM_CANPASTE, so Paste is enabled fromIsClipboardFormatAvailable(CF_UNICODETEXT).
PlaceholderText and the dark visual style need comctl32 version 6, which an app opts into with
a manifest dependency on Microsoft.Windows.Common-Controls 6.0 (the gallery has one). Without it
the EDIT is comctl32 5's and shows no placeholder.
RichEditBox¶
native_controls::RichEditBox (examples/native_controls/rich_edit_box.hpp, example code like
EditBox) wraps RICHEDIT50W from
MSFTEDIT.DLL, the version current Win32 apps ship and the same RichEdit WinUI's own
RichEditBox runs windowless, not the legacy RICHEDIT20W from RICHED20.DLL. MSFTEDIT.DLL
registers the class as a side effect of loading and has no init call (unlike
InitCommonControlsEx), so it is loaded once, lazily, before the first RichEditBox is created.
EDIT, by contrast, is registered by user32 for every process.
Its API follows RichEditTextDocument where the gallery samples need it:
| Text document API | RichEditBox |
|---|---|
ITextCharacterFormat Bold, Italic, Underline, ForegroundColor, BackgroundColor |
the character format setters; FormatEffect.Toggle is toggle_bold() / toggle_italic() |
ITextRange::FindText |
find_all() |
Document.LoadFromStream / SaveToStream with TextSetOptions.FormatRtf |
load_rtf() / save_rtf() |
CommandBarFlyout customization |
app context-menu items |
Offsets are UTF-8 byte offsets over the control's text, with each paragraph mark counted as one
\r, and are mapped to the control's UTF-16 positions on every message.
The context menu has TextBox's items plus Undo, Redo and Delete. Paste asks EM_CANPASTE with
format 0 ("can anything be pasted"), which also covers the read-only state. The right-clicked
word, for the spelling section, is found with EM_CHARFROMPOS, converting window-client DIPs to
the control's client pixels through bounds() and the window scale. A menu opened from the
keyboard (Shift+F10, the Menu key) anchors 8 DIPs inside the control's top-left corner instead of
at the caret, because GetCaretPos returns device pixels with no DPI scale at hand.
The in-tree ui::RichEditBoxControl is a different control: it is drawn by the framework and never
touches RichEdit. It reads and writes a subset of RTF 1.9 through a private parser
(src/ui/text/Rtf.hpp, never installed), enough for what a RichTextFormat carries: the font and
color tables, \b, \i, \ul and its variants, \strike, \cf, \highlight, \cb,
\chcbpat, \f, \fs, \plain, \par, \line, \tab, \uN with \ucN, \'hh in code page
1252, and the control symbols. Destinations it does not understand (the \* ones, the
stylesheet, info, pictures, objects, headers) are skipped whole, so a WordPad or Word file loads
as its text in those formats.
Context Menu¶
A right-click menu is shown by the OS, not drawn in the tree. Every native Windows text field pops a real USER32 menu for its spelling suggestions, and that brings OS theming, keyboard navigation and screen-reader support for free.
ContextMenuHost::show(position, items) takes a flat list of ContextMenuItems (text, enabled,
separator) and returns the index picked, or nothing for a dismissal. It is deliberately flat:
no submenus, icons or check marks, since a spelling menu needs none. The flow:
- The host receives
WM_CONTEXTMENU(a right-click, Shift+F10 or the Menu key) and callsWindow::handle_context_menu(position). - A positioned request first focuses the focusable element under the pointer, as a press would.
on_context_requested()handlers get the first chance, innermost first. Then the focused element'shandle_context_menu(position, host)may callhost.show(), and only during that call.
Win32ContextMenuHost turns it into TrackPopupMenuEx with TPM_RETURNCMD (escaping & so
text is not read as a mnemonic). show() runs the menu's own modal loop and returns only when
the menu closes; the window keeps repainting, since that loop still dispatches WM_PAINT. A
right-click inside a native child window never reaches the framework's hit testing, so the host
resolves the owning NativeControl itself and calls Window::handle_native_context_menu().
Dark Menus¶
USER32 draws popup menus in the system menu colors, which follow neither the libwinui Theme
nor "Apps use dark mode". The only lever is uxtheme's undocumented immersive app mode, wrapped
by set_app_dark_mode() in src/platform/windows/DarkMode.hpp. Its entry points are exported by
ordinal only, so each is resolved at run time and the call degrades to a no-op (light menus)
where it is missing:
| Ordinal | Function | Windows |
|---|---|---|
| 135 | SetPreferredAppMode(PreferredAppMode) |
10 1903 and later (build 18362) |
| 135 | AllowDarkModeForApp(bool) |
10 1809 (build 17763) |
| 104 | RefreshImmersiveColorPolicyState() |
|
| 136 | FlushMenuThemes() |
Ordinal 135 changed its signature between the two builds, so the build number is checked
through ntdll's RtlGetNtVersionNumbers (GetVersionExW lies without a compatibility manifest)
instead of just testing for the export. Below build 17763 there is no dark mode. On 1809 dark can
only be allowed, not forced, so a dark-themed window still gets a light menu unless the OS is
dark too. From 1903 on it uses ForceDark/ForceLight, so the menu follows the app's theme and
not the OS setting.
The switch is binary: dark or light, never an era theme's own colors (that would need
owner-draw). It is also per process while a Theme is per window, so the last window to show a
menu wins. The host is told dark or light through set_dark() rather than reading the Theme,
because nothing below Window may depend on the theme's owner. EditBox and the tray menu use
the same switch.
TestContextMenuHost records every menu and answers with a scripted choice:
host.pick("Add to dictionary") by visible text, pick_index(), or the default dismissal. A
script applies to the next show() only.
In-tree menus (MenuFlyout, CommandBarFlyout) are ordinary popups, not this seam. An app's own
right-click menu is a MenuFlyout opened from on_context_requested().
Compositor¶
The seam for compositor-owned animation: sync_visual(), animate_opacity() and
remove_visual(), keyed by VisualId. It reaches controls
through sync_composition() after layout, input and focus changes. DCompCompositor layers
DirectComposition overlay visuals over the Direct2D content; TestCompositor records each call
as a struct (SyncVisual, AnimateOpacity, ...). The layer calls (supports_layers(),
sync_layer(), paint_layer(), animate_layer(), stop_layer_animations(), remove_layer(),
now()) carry an element's continuous motion; Window makes them from take_damage(), and each
has a default body, so a compositor without layers implements none of them. When
DirectComposition is unavailable the window simply has no compositor. The full picture is on
Rendering and Compositor Layers.
Launcher and Shell Dialogs¶
Launcher¶
ui::launch_uri() hands a URI to whatever app the OS has registered for it, and
ui::launch_file() opens a file with the app registered for its type. A hyperlink Button's and
a hyperlink inline's navigate_uri call launch_uri() after the click handler and the command
have run. Both go to the process's one Launcher.
Win32Launcher calls Windows.System.Launcher::LaunchUriAsync through C++/WinRT for a URI and
ShellExecuteEx with SEE_MASK_ASYNCOK for a file, fire and forget, so the UI thread never
blocks on the shell. TestLauncher records the URIs and files; HeadlessUI installs one unless
the test installed its own.
Message Box¶
ui::message_box() shows the OS's own message box and returns the button the user chose. It goes
to the process's one MessageBoxHost. The Win32 one is the task dialog (TaskDialogIndirect),
which has a heading line the plain MessageBox lacks. The task dialog is in version 6 of the
common controls, which a process only gets through a manifest; an executable without one borrows
shell32's manifest (its resource 124) in an activation context for the length of the call. Where
even that fails, MessageBoxW shows the box with the heading as its first paragraph.
The call runs the dialog's own modal loop on the UI thread, which keeps the app's windows
painting; no second thread is needed, as the task dialog works in either apartment.
TestMessageBoxHost records each box with its owner and answers with a value the test sets;
HeadlessUI installs one unless the test installed its own, so a test never blocks on a real
box. A backend without message boxes answers the choice that changes nothing: Cancel, No when
there is no Cancel, else OK.
Notifications¶
ui::notify() shows a toast through the process's one Notifier. The Win32 one uses
Windows.UI.Notifications through C++/WinRT and needs neither a package nor a tray icon:
- Toasts go out under an AppUserModelID: the one the app set for itself
(
SetCurrentProcessExplicitAppUserModelID), elselibwinui.<exe name>.<hash of its path>, so two copies of a program in different folders stay apart. - The first call writes
HKCU\Software\Classes\AppUserModelId\<id>with aDisplayName(the executable's FileDescription, else its file name) and anIconUri(its first icon, saved as a PNG under%LOCALAPPDATA%\libwinui\notifications). That is what the toast's header shows. NotificationIcon::Info,WarningandErrorbecome the toast's app logo, the system's stock icon saved as a PNG beside it.AppandNoneadd no picture, since the header already shows the program's icon.- The notifier of an unpackaged program can't report its setting (
Setting()fails with "Element not found") but its toasts show, and the system applies the user's notification settings and do-not-disturb to them. - A click raises
Activatedon a worker thread; it is posted to the UI thread through a hidden message-only window (Win32MessageWindow) and runson_click. Without a COM activator, a click after the app has quit opens nothing.
TrayIcon::notify() takes the same Notification and shows it as the tray icon's balloon, which
Windows 10 and later turn into a toast with the tray icon as its source. TestNotifier keeps each
notification and can click one; HeadlessUI installs one unless the test installed its own.
Jump List¶
ui::set_jump_list() and ui::add_recent_file() go to the process's one JumpListHost. The Win32
one builds the list with the shell's ICustomDestinationList:
- Each
JumpTaskbecomes anIShellLinkto the running executable with the task's arguments, its description as the tooltip and its title in the link'sSystem.Titleproperty, added withAddUserTasks(). The list also keeps the Recent section (AppendKnownCategory(KDC_RECENT)), and an empty list deletes the app's list. - The shell's list objects are made for a single-threaded apartment, so each call runs them on a short-lived thread of its own and waits for it, whatever apartment the UI thread is in.
- The list belongs to an AppUserModelID: the one the app set for itself, which its toasts use too
(
Win32AppIdentity.cppreads it for both), else none, which leaves it to the ID the shell derives from the executable, the same one its taskbar button has. - An empty
iconuses the executable's first icon and a"#101"resource names it in place. Any other icon is drawn at 32 pixels and saved as an icon file under%LOCALAPPDATA%\libwinui\jumplist, since a shell link only takes an icon from a file. add_recent_file()callsSHAddToRecentDocs, under the app's own ID when it set one. The shell shows a recent file in the jump list only for a file type the app is registered to open.
TestJumpListHost keeps the last tasks and every recent file; HeadlessUI installs one unless
the test installed its own.
Single Instance¶
ui::claim_single_instance(id, on_activated) is not a seam either: a free function with a Win32
implementation in Win32SingleInstance.cpp and one in NoBackend.cpp that always answers true.
- The claim is a named mutex,
Local\libwinui.instance.<id>, so it holds per user session. The first process to create it keeps it open until it exits. - The holder titles its message-only window (
Win32MessageWindow, the one that also carries a toast's click)libwinui.instance.<id>. A later start finds that window withFindWindowEx(HWND_MESSAGE, ...), retrying for up to five seconds while the holder is still starting, and sendsWM_COPYDATAwith its working directory and arguments, each UTF-8 and ended by a NUL. - The holder copies the data, answers at once and posts the rest, so
on_activatedruns on its UI thread from the message loop after the sender has been released. The sender callsAllowSetForegroundWindowfor the holder first, soon_activatedmay bring a window forward. - The message window lets
WM_COPYDATAthrough UIPI, so an elevated holder still hears a start that is not elevated.
The tests run both sides in one process: the claim on the test thread, the forwarding on a second thread, with the test thread pumping messages.
Shell Dialogs¶
The file and folder pickers (include/winui/ui/Pickers.hpp) are not a seam. They are free
functions, ui::pick_file(), pick_files(), pick_save_file(), pick_folder() and
pick_folders(), each taking the owner Window and an options aggregate, kept outside the
element tree on purpose: what sits in the tree is a spec, what you do is a function. The Win32
implementation (src/platform/windows/ShellDialogs.cpp) keeps the Windows App SDK's picker
shapes as private types underneath; a platform without a backend returns nothing. A caller wires the result into a
control from a handler, like any other side effect.
They use the common item dialog (IFileOpenDialog/IFileSaveDialog), not the legacy
GetOpenFileName/GetSaveFileName. Titles, names and filters are UTF-8, paths are
std::filesystem::path both ways. The owner is passed as a Window and the host resolves its
HWND, so an app never includes <windows.h> for this header.
Each call blocks until the dialog closes, but the dialog runs on its own thread in the single-threaded apartment the common item dialog needs:
- The UI thread may be in the MTA (an app may have put it there before its first window, which
also keeps OLE's drag and drop off).
CoInitializeExcannot change that afterwards, andIFileDialog::Show()called from the MTA never returns instead of failing. - The calling thread keeps pumping messages rather than joining. An owned window on another
thread shares its owner's input queue and sends it messages, so a plain join would hang the
dialog too. The window keeps painting meanwhile, and the dialog stays modal because
Show()disables the owner. - A
WM_QUITthat arrives while the dialog is up is posted again once it has closed. - The focus the dialog thread hands back on close is useless, since that thread does not own the
window, which would then take no keys until clicked. So the focused window is recorded first
and focused again on the calling thread afterwards, mirroring WinAppSDK's
PickerCommon::DialogFocusRestorer.
The storage pickers are ported from the Windows App SDK's own implementation
(dev/Interop/StoragePickers): filter validation and configuration follow PickerCommon's
ValidateSingleFileTypeFilterElement, ValidateFolderPath, TryParseFolderItem,
CaptureFilterSpecData and ConfigureDialog. A picker's folder wins over its
start_location. With remember_last on (the default) either one only applies when the dialog
has no remembered folder, and with it off the dialog opens there every time. A known folder that
is unavailable falls back to Documents. The folder dialog sets FOS_FORCEFILESYSTEM, since a
library or "This PC" has no path to hand back.
There is no headless double: a real modal OS dialog has no useful headless form, so only the
filter-building helper (picker_file_types(), in the backend's ShellDialogs.hpp) is unit-tested. If you need a testable file
choice in an app, put your own interface in front of these functions.
Window Host¶
WindowHost carries window state across the boundary in both directions: the user maximizes
from the caption, the app goes full screen from a button. The host is the only writer of what
the window is (Window::state(), active(), visible(), caption_insets()); the app only
asks (request_state(), restore(), close()), and a request may be refused or complete on
the OS's schedule. Everything the app configures on Window (title, style, backdrop, icon, system
menu, taskbar state) is remembered there and pulled by the host when it is ready, so the order
of setting it and installing the host does not matter. WindowHost is private to libwinui. HeadlessWindowHost implements the same
rules in memory, and HeadlessUI::simulate_state() plays the user changing the state from
outside.
The Win32 Host¶
src/platform/windows/Win32WindowHost.cpp is the one place that turns a Window into an HWND. It
keeps one heap-allocated HostState per window: the Direct2D target, the measurer, the seam
implementations above, the frame state and the Window itself.
Message Loop¶
ui::run() handles one message, then ticks every open window with real elapsed time, and
blocks in GetMessageW only when the queue is empty. The reasons:
WM_TIMERis low priority and only synthesized once the queue is empty, so continuousWM_MOUSEMOVEstarves it. Antick_override()animation driven by the timer alone (a spinningProgressRing) freezes while the mouse moves.- Draining the queue with
PeekMessagebefore ticking does not help, since the drain never ends while fresh mouse moves keep arriving, and throttling ticks to 60 Hz makes the repaints follow the bursty arrival of input. - The clock is
QueryPerformanceCounter;GetTickCount64()'s roughly 15.6 ms steps are too coarse for smooth motion. - It also ticks right before dispatching. Otherwise an animation a click starts gets all the idle time since the previous message on its first tick and skips up to 100 ms of its curve.
The 100 ms WM_TIMER wakes the loop while a window is busy with time and nothing else arrives.
After each tick the host asks Window::wants_ticks(): whether the tick changed something on a
shown window, an element called request_tick() (a wait of unknown length that changes nothing
until it ends), work was posted, a drag or connected animation runs, the app set an on_tick()
handler. Three seconds (IdleGraceMs) after the last such tick of any window, the
timers stop, so an idle app takes no CPU at all until a message arrives; the next busy tick starts
them again.
A pending after()/every() timer does not make a window busy, and neither does an element that
called request_tick_in(ms) (a wait of known length, such as the next frame of an animated image).
After each tick the host also asks Window::next_wake(), the earlier of the two, and sets the
WM_TIMER period to that wait when it is sooner than the 100 ms tick, or when the window is idle,
so a one-hour after() wakes the process once. An element's request holds for one tick: it asks
again from its next tick_override(), and a hidden or minimized window keeps none.
What such an element changes when its wake falls due is a timed step, and a timed step does not
make the window busy either: a caret blink or the next frame of a GIF is drawn, and the window goes
back to sleep until the next one. Window::tick() tells it from other changes with a count of
mark_dirty() calls per thread:
Element::tick()notes, per element and without its children, whether itstick_override()marked anything and whether it asked forrequest_tick_in(). A mark without a wake is busy, and so is any mark from the element's own animations.- In a tick where no element's wake was due, every mark an element makes is busy. So an animation that also waits for something later (a Carousel sliding while its buttons wait to fade) keeps the window busy in all the ticks between.
- Posted work, timers and the
on_tick()hook are never a timed step. - Dirt the last tick's timed steps left undrawn does not count at the next tick, unless anything marked the tree since. Input, layout or a setter in between makes the tick busy, which is what starts the grace for a hover fade.
The timer is re-armed only when it has to go off sooner or after it has gone off, since re-arming
after every message would keep postponing it under a stream of mouse moves. Windows rounds the
period up to its timer resolution (about 15.6 ms), so every(20ms) runs on a 20 ms schedule with
that much jitter, and nothing runs more often than every 10 ms (USER_TIMER_MINIMUM). Every quiet wait in the controls asks for its end with request_tick_in() and costs one wake-up:
- a caret blink (530 ms each way, so a focused text box wakes its window twice per 1.06 s)
- a tip's delay, the window's own for
set_tool_tip()(800 ms) and aToolTip'sdelay() - a submenu's hover delay to open or close (400 ms)
- the spell check after the last keystroke (250 ms)
- an overlay scroll bar's 2 s before it fades (
ScrollBarOverlay::fade_wait()), and Carousel's three seconds before its buttons hide
The fades and slides that follow are animations and keep the window busy while they run. A test runs a real window past the grace and counts the timer messages of the next second: none.
An animated Image asks for its next frame with request_tick_in(), so a GIF with 30 ms frames
plays at its own pace however rarely the window ticks otherwise. It also pauses while nobody can
see it, through the measuring pass of each frame (On Screen). The scroll
that brings it back produces a frame and so a measuring pass, which tells it at once, and the
request_tick_in() it makes from there wakes the window too.
An Image whose source is still decoding (ImageSource::load()) costs no wake-ups either. Its
first tick hands the decode a callback that posts empty work to the window through a WindowRef,
and the decoding thread runs it when it is done. The post wakes the window, and that tick shows the
picture. The callback holds no pointer to the Image, so the Image may be destroyed first, and a
post to a closed or destroyed window is dropped.
Extra ticks are cheap tree walks, because drawing is paced by WM_PAINT. A paint that finds
nothing on screen changed still calls DwmFlush() before it returns, so it waits for the next
composition the way a presented frame does. Without that wait, an animation scrolled out of view
(dirty on every tick, drawing nothing) ran the loop at full speed and took a whole core. While a window is
minimized its time keeps moving, so a tooltip delay or animation is in the right place on restore,
but no repaint is asked for, and a change on a hidden or minimized window does not keep it busy.
After each WM_PAINT the host calls RedrawWindow(RDW_ALLCHILDREN | RDW_UPDATENOW). Child
WM_PAINT has the same starvation problem as Win32NativeHost's moves: during an animation the
host window is invalidated after every message, and a hosted RichEdit's paint never came, so
typing showed nothing. RDW_UPDATENOW without RDW_INVALIDATE costs nothing when no child owes a
paint, and WS_CLIPCHILDREN keeps the Direct2D drawing out of the child rectangles.
Closing and Destruction¶
Every close path (the caption button, Alt+F4, the taskbar, Window::close()) goes through
WM_CLOSE, where handle_close_request() may refuse. Window::close() posts WM_CLOSE rather
than sending it, because it is usually called from a click handler deep inside the window's own
dispatch. For the same reason a HostState is destroyed only at the loop's next safe point after
its WM_DESTROY, never inside a message dispatched to it.
At WM_DESTROY the host clears the automation lifetime flag and calls
UiaReturnRawElementProvider(hwnd, 0, 0, nullptr) to tell UI Automation the window is going (see
Input and Focus). Inside HostState, window is declared last so
it is destroyed first: NativeControl destructors in the tree call into the native host, which
must still be alive.
More Than One Window¶
Window::show() runs another window on the existing message loop, next to the main one. The
host only borrows a Window (HostState::window is a plain pointer): the app owns every window
it shows and decides when the object dies. Closing destroys the OS window and leaves the Window
a valid object that show() can open again; ~Window() destroys the OS window if it is still
there (detail::destroy_native_window(), src/platform/Backend.hpp), which is why a window
must not be destroyed from inside one of its own handlers. Nothing in a window's content may
capture another window's state by reference unless that state outlives it.
An owned modal window disables its owner while it is up. The owner is enabled again before
DefWindowProc destroys the window, or the still-disabled owner cannot take the activation and
Windows gives it to another app.
Frame and Window State¶
The style is WS_OVERLAPPEDWINDOW minus what WindowStyle turns off, plus WS_CLIPCHILDREN
(without it every Direct2D repaint, one per mouse move, paints over native child windows and they
flicker).
Frame |
Style | Why |
|---|---|---|
Standard |
overlapped | the OS draws the caption and borders |
Borderless |
overlapped, caption removed in WM_NCCALCSIZE |
the caption bits make the shell treat it as a real window: snap, Win+arrows, the minimize animation, the shadow |
None |
WS_POPUP, plus WS_SYSMENU and the minimize and maximize boxes |
the taskbar button can still minimize and restore it |
A few rules the frame code follows:
WS_SYSMENUis what puts caption buttons on a caption at all. There is no style bit for the close button alone; graying the system menu's Close disables it (CS_NOCLOSEwould too, but it is class-wide).- Style changes need
SWP_FRAMECHANGED, or the frame keeps cached metrics and the caption buttons do not update. Topmost can only change throughSetWindowPos(HWND_TOPMOST/HWND_NOTOPMOST), never throughGWL_EXSTYLE.WS_EX_TOOLWINDOWis only re-read by the shell when a window is shown, so a visible window is hidden around that change. - Hiding the caption icon needs
WTNCA_NOSYSMENUnext toWTNCA_NODRAWICON, because the icon is also the system-menu button. That drops only click-the-icon; Alt+Space and a caption right-click still open the menu. - Custom system-menu ids stay below
SC_*(0xF000) in steps of 16, because the system keeps the low four bits ofWM_SYSCOMMAND's wParam for itself. The menu is rebuilt fromGetSystemMenu(TRUE)each time, with app items placed above Close. - Windows are created cloaked (
DWMWA_CLOAK), so there is no white flash before the first frame.
Extended title bar and borderless. With the title bar extended, only the caption leaves the
non-client area; the side and bottom borders stay native. The caption band left to DWM is the
largest of the OS caption (plus the top sizing band while restored), the caption buttons and
WindowStyle::title_bar_height. WM_NCHITTEST asks DWM first (DwmDefWindowProc, which gives
the caption buttons their hover, press and Windows 11 Snap Layouts on HTMAXBUTTON), then the
resize band, then the logical Window. The creation-time WM_NCCALCSIZE comes with wParam
FALSE, so a frame change is forced in WM_CREATE. A maximized overlapped window overshoots its
monitor by its sizing border, so with the non-client area collapsed the client is inset by the
frame thickness. A maximized window covering the whole work area would block an auto-hide
taskbar's reveal edge, so that edge gives up one pixel.
TitleBar never draws minimize, maximize or close buttons.
With content extended into the caption, DWM keeps drawing the OS buttons over it, and they keep
their UI Automation providers, high contrast colors and Snap Layouts, which a self-drawn set
would lose. Window::caption_insets() tells the title bar how much room they take.
DWM attributes. DwmExtendFrameIntoClientArea is decided in one function, because the last
call wins: a system backdrop needs the whole client (Mica shows wherever paint leaves
transparency), an extended title bar needs the caption band, and a borderless frame needs a
token 1 px so DWM keeps the shadow. Turning a backdrop off must also reset the margins, since
DWMSBT_NONE alone leaves the frame extended and any transparent pixel shows the desktop.
DWMWA_SYSTEMBACKDROP_TYPE exists from Windows 11 22H2; the first refusal is latched, so theme
changes do not retry it forever. DWM takes Mica's light or dark tint from
DWMWA_USE_IMMERSIVE_DARK_MODE, not from what the app draws, so that flag has to match the
theme. Frame colors are pushed on every theme change, with DWMWA_COLOR_DEFAULT for "follow the
OS", or a previous theme's caption color (a Windows 9x navy) outlives a switch. DWM rounds
nothing on a popup by default, so Frame::None with round_popup_corners asks for
DWMWCP_ROUND.
Full screen follows Raymond Chen's "How do I switch a window between normal and fullscreen?":
save the placement (restoring first, so the saved one is the windowed state), switch to
WS_POPUP, and drop WS_MAXIMIZE so the monitor rectangle becomes the normal position. A
minimize from full screen then restores to full screen for free. WS_MINIMIZEBOX and
WS_SYSMENU stay for the taskbar button, WS_MAXIMIZEBOX goes so Win+Up cannot maximize it,
and the taskbar is told explicitly (MarkFullscreenWindow). It covers the monitor the window
mostly sits on and re-fits on display changes. Leaving re-derives the windowed style from
WindowStyle, so a set_style() made meanwhile lands. Style bits gate the user, not the API:
SW_MAXIMIZE works without a maximize button, and resizable = false does not block full
screen.
Placement. WindowPlacement normal bounds are converted at the DPI of the monitor they sit on,
not the window's current one, so they can be applied without knowing where the window was.
Applying takes two passes: the system DPI finds the monitor, whose DPI then gives the real size.
Size limits are client DIPs; WM_GETMINMAXINFO adds the current frame back. Resizing the client
measures the frame instead of using AdjustWindowRectEx, which knows nothing of a caption the
content took over. The first window of the process shows with SW_SHOWDEFAULT, which honors a
shortcut's "Run: Maximized/Minimized".
Taskbar and Icons¶
Win32Taskbar (src/platform/windows/Win32Taskbar.hpp) wraps ITaskbarList3 for progress, the
overlay badge, the thumbnail toolbar and MarkFullscreenWindow. Calls about the taskbar button
fail until the shell sends the registered "TaskbarButtonCreated" message, and it sends it again
after an Explorer restart. So the class keeps the last value of everything and replays it from
button_created(); a setter before that only records. SetProgressValue also switches an
indeterminate bar back to normal, so it is only sent for states that show a value. The thumbnail
button set is fixed by the first ThumbBarAddButtons, which cannot be undone, and is capped at
the shell's seven. ChangeWindowMessageFilterEx lets an elevated process still hear
"TaskbarButtonCreated" and the thumbnail buttons' WM_COMMAND from the unelevated shell.
The caption icon, the overlay badge and the thumbnail buttons each need an HICON at a size only
the host knows. Win32Icon.hpp builds one from a WindowIcon: an RGBA image is resampled
bilinearly on premultiplied values into the square (uniform, centered); a glyph is drawn through a
Direct2D DC target at 96 DPI with grayscale antialiasing (nothing under it is opaque, so
ClearType has nothing to blend against) and then un-premultiplied. The 32 bpp color bitmap
carries the alpha, but an all-zero monochrome mask still has to exist.
Tray¶
ui::TrayIcon (include/winui/ui/TrayIcon.hpp) is a notification-area icon and nothing more:
a hover text, a right-click menu of MenuItems and notifications. A flyout is an ordinary
Window the app owns, made with the ui::tray_flyout_options() preset and attached through
TrayIconOptions::flyout: a left click (or Enter or Space on the focused icon) opens it next to
the icon, and clicking anywhere else closes it. The app runs its own loop, ui::run() with no
main window, and ends it with ui::quit(), which is an item it writes into the menu itself.
It has three pieces:
- A hidden icon window owns the icon and hears the shell. It is a top-level zero-size
WS_EX_TOOLWINDOWpopup that is never shown, so it has no taskbar button or Alt+Tab entry. It is top-level rather than message-only because a message-only window never receives the registered "TaskbarCreated" broadcast, which says Explorer restarted and every icon must be added again. Adding the icon also fails while there is no taskbar yet, and TaskbarCreated retries it.ChangeWindowMessageFilterExlets an elevated app still hear it. - The flyout is the app's
Window. Its OS window is created hidden the first time it is needed, withWindowOptions::close_hidesfrom the preset, so every control, binding, theme and popup works in it unchanged. The icon only keeps a pointer to it, so the window has to outlive the icon: declare the flyout first. - The loop is the app's:
ui::run()without a main window, so onlyui::quit()ends it.
The icon uses NOTIFYICON_VERSION_4: NIN_SELECT, NIN_KEYSELECT and WM_CONTEXTMENU with the
anchor point in wParam and the event in LOWORD(lParam), and the shell's own tooltip
(NIF_SHOWTIP). Two events need debouncing: Enter on the focused icon can deliver NIN_KEYSELECT
twice, and a click on the icon while the flyout is open first light-dismisses the flyout (it
loses activation) before NIN_SELECT arrives, which would reopen it. The second case is filtered
within the double-click time.
The icon follows Explorer's own taskbar light or dark setting (SystemUsesLightTheme, changed
through WM_SETTINGCHANGE "ImmersiveColorSet"), not "Apps use light theme"; the flyout follows
the app theme. It is drawn at the primary monitor's DPI, because the notification area lives on
the primary taskbar, and redrawn rather than cached, since DPI and light or dark can change.
The native tray menu needs SetForegroundWindow before TrackPopupMenu, or a click elsewhere
does not close it, and a posted WM_NULL after, or the next right click opens and closes it at
once (both per the Shell_NotifyIcon docs). It follows the flyout's theme through
set_app_dark_mode().
Placement. Along the taskbar the flyout is centered on the icon and kept clear of both
corners; one too big for the work area sticks to the leading corner. With a bottom taskbar, or an
anchor inside the work area (the overflow flyout, or an auto-hide taskbar overlapping while
shown), it goes above the anchor. Before sizing, the hidden flyout is moved onto the icon's
monitor so a DPI change (WM_DPICHANGED) arrives before the content is measured, and it stays
DWM-cloaked across the show until the frame at the new size has painted.
Look. The flyout is a bare Frame::None popup with Windows 11's rounded corners and rim and
no shadow, like the shell's own flyouts, which DWM does not shadow either. TrayFlyoutOptions::shadow
switches it to Borderless, an overlapped window DWM shadows and rounds itself. Windows 11 would
otherwise give a tool window a tooltip's small corner radius, while the shell's flyouts use
8 px (OverlayCornerRadius); an era pack that asks for square corners keeps them. There is no
system menu, since DWM would draw a tool window's close button over the backdrop. The default
Fluent 2 background is #242424 in dark (sampled from Quick Settings) and #F3F3F3 in light (the
same step between #F9F9F9 and #EEEEEE); themes that do not allow a system backdrop (era
packs, high contrast) use their menu surface. Light dismiss is posted from the flyout's
WM_ACTIVATE rather than handled inside it.
Process-Wide OS State¶
Two small pieces of OS state live outside any window.
The command line. ui::command_line() (core/CommandLine.hpp) does not use main()'s
argv: a plain main(int, char**) on Windows gets its arguments in the ANSI code page, so a
folder name with an umlaut dropped on the exe arrives with a ? unless the manifest opts into
UTF-8. The function re-reads what Windows stored (GetCommandLineW) and splits it like the shell
and CRT do (CommandLineToArgvW), which is what lets an app pair it with winui::main and write
int main(). Elsewhere it reads /proc/self/cmdline, which is already UTF-8.
The regional number format. A DecimalFormatter that names no separators follows the
process's regional format, like WinRT's DecimalFormatter() and NumberBox's regional-settings
aware formatter ("1,5" in de-DE, "1.5" in en-US). The regional format stays the invariant one
(., and , in groups of three) until a host sets it with set_regional_number_format(), so a
headless run formats the same everywhere. The Win32 host reads LOCALE_SDECIMAL,
LOCALE_STHOUSAND and LOCALE_SGROUPING of the user locale (with LOCALE_NOUSEROVERRIDE, the
locale's own data) at startup and again on WM_SETTINGCHANGE "intl". Digits are always ASCII and
the sign is always -.
Drag and Drop
Drag and drop (ui/DragDrop.hpp) needs no seam. A drag started in a window stays inside it
and runs without OLE. Files and text dragged in from other apps reach the window through the
Win32 host's OLE drop target, which a test replaces by calling the WindowSeams external-drag
functions directly (see Drag and Drop). Dragging out to
other apps is not implemented.