facet_win32
facet's Win32 backend — the Windows counterpart of
facet_appkit, facet_gtk and
facet_android.
facet owns the description tree and all layout (the shared flex_layout
engine). This package answers the contract's verbs with user32/gdi32/comctl32,
or records in MANIFEST.md that Win32 cannot — and the second
half is the point: a verb that is neither implemented nor argued is a bug.
facet_win32 the registration surface: install(), and nothing else
views the Renderer's five verbs
controls one HWND body per node kind
input the READ half — WM_COMMAND / WM_NOTIFY, the gesture and key bands
subclass the messages a system control keeps to itself — Enter, the caret
paint the shared band — background / radius / opacity / enabled / tooltip
geometry computed frames -> SetWindowPos placements, and a scroll's extent
measure intrinsic size, in each control's own font
fonts the font cache, and which props are typography per kind
scheduler schedule + the Scheduler service, over the message queue
window the host HWND, the WNDPROC every window shares, and the menus
recycler list / collection / tree, virtualised over a scrolling panel — and the grid
imaging the bitmap decoder — GDI+, and a GIF's own frame delays
anim the two things that move on their own: a progress tween, a page slide
dnd receiving a drop, without COM
darkmode the appearance the system controls wear, and the ordinal it needs
field_furniture the magnifier and the clear button, drawn over an EDIT
shadow the band's drop shadow — a blurred DIB the PARENT blits
scroller a `scroll` that scrolls: the document's extent, the wheel, the bars
swipe swipe-to-reveal: a drag, and the strip of actions under the row
menus HMENU, and the one command-id table
dialogs alert / choose / prompt as facet trees, and the file chooser
sys constants, and the UTF-8 <-> UTF-16 door every string goes through
observers size observation, for the resource tier
What makes this backend different
One fact, and three consequences that shape every file: a Win32 child control is a WINDOW, not a view.
- The parent owns the event. A
BUTTONdoes not call you back; it sendsWM_COMMANDto its parent, and a trackbar sendsWM_HSCROLL. So the read half is a WNDPROC on the HOST rather than a controller per control — the one place this package's shape genuinely departs from facet_gtk's. - A control HWND is not paintable by us. It draws itself. The shared band's decorative half is answered on the container BEHIND it. Where a verb needs the control's own pixels, this package owner-draws — and where it cannot, MANIFEST.md says so rather than faking it.
- Z-order IS the child order. There is no child list to splice:
insertisSetParentplus aSetWindowPosnaming the sibling to follow. Paint order andcomponent::raiseare the same mechanism.
Origin is top-left with y growing downward — as in flex and facet_gtk, and unlike facet_appkit — so there is no flip anywhere in this package.
The same fact is why adopting a native view takes code here when it takes
none on AppKit. adopt_native hands facet a view the application built, and
the mount walk deliberately skips create for it — so the window arrives never
having been through the one place that makes a window a facet child. An NSView
is an NSView and needs nothing; an HWND that is not WS_CHILD becomes owned
rather than contained by SetParent, keeps its own frame, floats above
everything and ignores every SetWindowPos the layout makes, which reads as a
layout bug and is not one. views::insert forces the style and views::apply
binds the node, and between those two moments a mark keeps the window visible
to its siblings' slot arithmetic. camera's live preview is the worked example.
Status is a measurement
$ python3 tools/parity.py win32 # a shim onto the one measurement in facet_gtk/tools
363 prop bits declared across facet's kind modules
gtk 359 / 363 98%
appkit 354 / 363 97%
win32 315 / 363 87% <-- this package
68 declared handlers
gtk 68 / 68 100%
win32 62 / 68 91% <-- this package
21 shared-band bits
win32 19 / 21 90% <-- this package
1 unanswered and UNRECORDED: canvas.redraw — and it is a false negative.
That closing line is the number that matters more than the percentage. Every
kind reads either n/n or decided absent: — and the one not yet: row left
is wrong. canvas.redraw IS answered: apply_canvas repaints on any apply and
never reads dirty, so it never names the bit, and the tool counts a prop as
answered when the backend names its constant. MANIFEST's opening has the
argument, including the three ways of closing it that are all worse than leaving
it open.
The tool itself was wrong until 2026-09-06, in four ways that all flattered the number — among them, kinds a backend answered NOTHING for were left off the report entirely. The figures above are after the fix, which is why they are not the ones an older commit message quotes.
The band number went DOWN on 2026-09-01, and that is the point. It read
16/21 because C_OPACITY, C_SHADOW and C_CLIP were named in
paint::band_bits() and acted on nowhere — the tool reads a named C_* as
answered, so three verbs that did nothing reported as covered. Two are built
now and the third is measured impossible (MANIFEST §1), which leaves 15 real
ones where there were 13. facet_gtk shipped the identical mistake with
C_SHADOW and records it.
Read MANIFEST.md before trusting any adjective here, including
that one. And run the tool rather than quoting it: four separate times this
package's own §1 prose was what made a verb read as absent. The tool takes any
backticked identifier in §1 as "decided absent" and keeps only the leaf, so a
true sentence about a themed button also condemned text_button, which is
owner-drawn and can simply be given a border. The rule that came out of it:
name a verb in §1 only where the sentence decides that verb across the whole
backend; when a kind is the exception, describe it without backticks.
Five ways to check it
cd vendor/facet_win32 && ../../target/release/cpc test --filter facet_win32
# running 139 of 571 tests matching "facet_win32"
# test result: 139 passed; 0 failed
cd playground/win32_probe && ../../target/release/cpc build # the SEAM
cd playground/win32_runtime_probe && ../../target/release/cpc build # the FACADE
The suite pins what the backend DECIDES — the mappings, the style bits, the arithmetic, the encoders — and opens no window, because a suite that needs a desktop session is a suite that stops running.
--filter is not optional on Windows, it is the only way in. A package's
driver carries every dependency's tests — 571 here for 139 of this package's —
and one of stdlib's hangs on this host, which used to make everything ordered
after it unreachable. bugs/cpc-test-hangs-on-windows-in-facets-own-suite.md
has the measurement and the reproduction.
And examples/facet_gallery runs, which MANIFEST §4 said it could not:
the two symbols that blocked the link are gone, and the four bugs its first
launch found — an unsized scroll document, a wheel that skipped scroll, dead
scroll bars, and a tree that drew nothing without a row builder — are fixed.
It is the best test this package has, because it is the only one written
without Win32 in mind.
The probes are the other half. win32_probe calls the backend directly and
proves the seam; win32_runtime_probe goes through runtime::App and proves
the facade. Between them they cover what a test cannot: whether the slider
reported while it was dragged, whether the field kept its caret, whether a
carousel's page slide reads as a flick rather than a jump.
...and the third: ask it
Both probes serve the agent surface behind FACET_PROBE_AGENT, which is the
answer to "an agent has no eyes":
FACET_PROBE_AGENT=1 ./target/debug/win32_probe
# facet: agent surface on http://127.0.0.1:9352/ — 25 verbs (11 core, 14 armed)
describe_tree then answers what was actually built — keys, kinds and frames —
so a claim like "the grouped collection has two section headers" is a query
rather than a squint. That is how the grouping arithmetic in recycler was
confirmed on live data, and how a bug that made collections never rebuild was
found.
It does not return list ROWS, and that is facet's shape rather than this backend's — a virtualised row is realised without becoming a child of the list node, so the tree walk never reaches it. MANIFEST §3 has the detail.
...and the fourth: send it the messages a mouse would
An agent has no hands either, and the gesture kinds are the ones a test cannot
reach. PostMessage can: WM_LBUTTONDOWN, a run of WM_MOUSEMOVE and
WM_LBUTTONUP, posted to the deepest child under the point, go through the real
WNDPROC and the real input::on_mouse. That is how the swipe was verified —
the row's content window measured 176 points to the left afterwards, and the
application's on_open_requested label had changed.
Two things make the synthesis match a mouse rather than merely resemble one.
Descend from the TOP-LEVEL window with ChildWindowFromPointEx, not
WindowFromPoint, which answers about the whole desktop and hands back the
terminal that happens to be in front. And stop the descent at a system control:
a Static answers WM_NCHITTEST with HTTRANSPARENT so a real press falls
through to the panel behind it, while a POSTED message is eaten by the
static's own WNDPROC and never reaches facet at all.
...and the fifth: look at it
PrintWindow(hwnd, hdc, PW_RENDERFULLCONTENT) renders a window into a bitmap
of your own, which is how the shadow and the clip above were checked: the
falloff under a card is four numbers, and a circular clip is either a circle or
it is not.
Two things make it work where a screenshot does not. It asks the WINDOW to
paint, so z-order and occlusion stop mattering — a screen capture with a
terminal in front samples the terminal, which is how a pixel test lies. And the
PW_RENDERFULLCONTENT flag (2) is what includes CHILD windows: without it a
window made of child controls — which is every window this backend builds —
prints as an empty client area.
Diagnostics
FACET_WIN32_ROWS=1 ./target/debug/win32_probe # what the recycler is holding
# [rows] model=20000 visible=0..9 cells=10 live=10
Ten cells for a twenty-thousand-row model, and the number does not move with
the model. That is the property recycler.cplus exists to have, and it is a
number rather than an adjective because facet_gtk's handoff ended on exactly
this measurement — "GtkListView keeps 205 cells for a viewport that holds
seven".
FACET_WIN32_THEME=1 ./target/debug/win32_probe # which comctl32 actually loaded
IsAppThemed() is NOT the signal — it answers 0 on a process demonstrably
running comctl32 6.0, and 0 on a C program drawing themed controls at the time.
The loaded module's PATH is, which is what this prints. Believing the API cost
an afternoon.
The rules worth knowing before editing
One code path for create and apply. create configures a new control with
paint::all_bits(), the same call apply makes with the node's real dirty
word. A prop honoured on update is therefore honoured on create, and the two
cannot drift. Every write is gated on its bit.
The bits are PER KIND. A generated module numbers its P_ constants from 1
in declaration order, so the same verb is a different bit on each kind —
list::P_COUNT is exactly collection::P_SELECTED_INDEX. This has been hit
three times here: fonts, then the recycler, then the sequence applier. A table
that reads one kind's numbers for another compiles, runs, and quietly does
nothing.
Text goes out WIDE. The *A entry points decode the process ANSI codepage
and a C+ string is UTF-8. sys::to_wide / sys::from_wide are the door; a
direct *A call with anything above ASCII is mojibake, and it showed up in a
window title before it was fixed.
A child window owns its pixels. The parent does not paint underneath it, so
a control that skips its own erase does not become transparent — it keeps
whatever it drew last. That is one bug (a label reading unche…ed beside its
own ghost) and one absent verb (icon_button.is_opaque), from one fact.