App Window

FluereAppWindow provides the chrome of a window: corner radius, border and shadow, the title bar (icon, title, subtitle, left header, center content, right header, back and pane-toggle buttons), the caption buttons in the top-right corner, and a content area that only owns the visible region.

It maps to WinUI 3 / Windows App SDK's Microsoft.UI.Xaml.Controls.TitleBar (microsoft-ui-xaml) — a composite control — so the template, states and metrics follow it item by item:

  • 12-column template: 2 | Auto(back) | Auto(pane toggle) | Auto(left header) | 14(left header spacing) | Auto(icon) | Auto(title) | Auto(subtitle) | *(content) | Auto(right header) | 48(min drag region) | Auto(caption area).
  • Two heights: 32 only when Content / LeftHeader / RightHeader are all empty (TitleBarCompactHeight); otherwise 48 (TitleBarExpandedHeight, TitleBar.cpp#UpdateHeight).
  • Zero-duration state changes: the source calls GoToState(..., false) and has no Storyboard / VisualTransition at all, so title-bar internals never animate.
  • Dragging = non-client area plus punched holes: the whole title bar is non-client, interactive elements become NonClientRegionKind::Passthrough holes, and the Icon uses its own Icon region — it is not in the passthrough list, so the icon area stays draggable.

Version and evidence baseline

ItemValue
Behavior baselinetag winui3/release/2.0.1 (commit 978ab63) — the WindowsAppSdkPackageVersion=2.0.1 pinned by WinUI3 Gallery v2.9.3
Template / brusheswinui3/release/2.5.1 (commit ba3a8d5) — the directory drifts between tags (TitleBar.cpp +382 lines, AutoRefreshDragRegions / IsDragRegion are MUX_PUBLIC_V11), but TitleBar.xaml and TitleBar_themeresources.xaml are byte-identical
Key filessrc/controls/dev/TitleBar/{TitleBar.xaml,TitleBar_themeresources.xaml,TitleBar.idl,TitleBar.h,TitleBar.cpp,TitleBarAutomationPeer.cpp}

perf2026 is not adopted: TitleBar_themeresources_perf2026.xaml (new in 2.5.1) contains two real bugs (a renamed resource key whose reference was not updated, and the pane-toggle Disabled state painting the background onto the 16×16 icon instead of the button). This component follows the base file.

Title bar: six slots and three buttons

SlotWinUI equivalentNotes
#iconIconSourceLives in a 16×16 Viewbox (TitleBarIconMaxWidth/Height)
#left-headerLeftHeaderCounts towards the 32 → 48 height switch; not a drag region
#titlebar-contentContentTitle-bar center (star column, centered by default); Compact switches it to left + 16 end margin
#right-headerRightHeaderRight-aligned; not a drag region
#titlebarnone (escape hatch)Replaces the whole title bar; caption buttons are still rendered here (showCaptionButtons can turn them off)
#content (alias #default)none (window body)Application content: only the viewport (overflow: clip), scrolling belongs to the consumer

Any of the latter four pushes the title bar to 48. When exactly one of back / pane-toggle is visible, the left-header spacing drops from 14 to 2 (NegativeInsetSpacing).

Header slots
(nothing yet)
Header slots + back / pane-toggle buttons

Inputs and buttons placed in the title bar are not drag regions — pressing them never starts a window drag (the equivalent of WinUI punching that area into the passthrough list).

Content area: viewport only

The content area is "window height minus title-bar height" with overflow: clip and min-block-size: 0: it only defines the visible boundary; scrolling is up to the consumer (put a FluereScrollView, or your own scroller, inside). Sizing is up to the consumer too — the component never writes position / inset / transform.

Settings21RU0000CD
The content area only owns the visible region: what goes inside is up to the consumer. Put a FluereScrollView in the slot when scrolling is needed.
Basic: icon + title + subtitle + content

Caption buttons: pure logic proxying

FluereCaptionButtons renders three native <button>s (minimize, maximize/restore, close) and only emits: minimize / maximize / restore / close, plus v-model:maximized to hand the "maximize ⇄ restore" intent back. The component itself never changes any window state — that is exactly the 0.5.2 "logic proxy" contract.

Capability flagBehaviorBasis
isMinimizablefalse ⇒ button hiddenNo A-level source for the system behavior; recorded as an assumption
isMaximizablefalse ⇒ button hidden, glyph no longer swapssame
isClosablefalse ⇒ button kept but disabled (native disabled)same
(nothing yet)
Caption buttons + capability flags + event log

Caption-button evidence gaps

WinUI 3's TitleBar has no caption buttons in its template (all 12 columns read; no minimize / maximize / close). They are drawn by the system (DWM) and have no XAML implementation in microsoft-ui-xaml:

  • CWindowChrome.cpp only makes the button background fully transparent and maintains the caption-region hit-test rectangles — no drawing code; enum class CaptionButtonState in CWindowChrome.h has no usage anywhere.
  • Microsoft's own specs/TitleBar/titlebar-functional-spec.md:47-48 states "WinUI TitleBar does not handle Caption Buttons … handled by the AppWindow TitleBar".
  • The only caption visual in the tree is the legacy system-chrome x:Key="WindowCaptionButton" style (src/dxaml/xcp/dxaml/themes/generic.xaml:15697-15786, a 40×28 button, a 9×9 stroked Path, and the two WindowStateNormal/Maximized Path.Data strings). This component only reuses its glyph geometry.

Everything below is therefore a system metric, cross-checked against 1:1 Windows 11 screenshots and exposed as overridable component-local variables:

QuantityValueSource and verification
Button width × height46 × 32TitleBarRightPaddingWidth is overridden at runtime by AppWindowTitleBar.RightInset() (A-level lead); measured glyph-center pitch 46.0 / 46.0 in the reference screenshot
Glyph box / line width10 / 1px (close cross 1.1px)Measured: minimize = 10×1 bar, maximize = 10×10 stroked square, close = 10×10 cross; the 1.1px close value is justified in "Glyph calibration" below
Glyph ink#000000 / #FFFFFF (light / dark)Legacy WindowCaptionForeground → SystemControlForegroundBaseHighBrush → SystemBaseHighColor (generic.xaml:263 + :4132); no matching token (colorNeutralForeground1 is #242424 in light), so it becomes --fui-caption-buttons-glyph-color
Vertical placementtop-alignedWith a 48-high title bar the back-arrow ink center is y≈23.5 (= 48/2) while the caption glyph center is y≈15.5 (= 32/2) ⇒ the buttons occupy only the top 32
Hover / pressed fillcolorSubtleBackgroundHover / …PressedThe same A-level brush family as the title-bar buttons (SubtleFillColorSecondary/Tertiary)
Close hover red#C42B1CWinUI system color SystemFillColorCritical (Common_themeresources_any.xaml:282), identical to Windows 11; the legacy #e81123 is defined but never referenced and cannot be used as evidence
Close pressed redcolorStatusDangerBackground3Hover (#B10E1C)No source at all; nearest semantic token

Glyph calibration (1:1 frozen frames vs. the Windows 11 reference screenshot, scored by per-pixel mean absolute ink difference Δ, lower = closer; scripts temp/glyph-lab.mjs + temp/glyph-lab-compare.py stay in temp/, the conclusions live in packages/ui/src/app-window/constants.ts and the component comments):

GlyphSettled valueΔRejected candidates
MinimizeM0 5.5H10 (butt; ink lands on the 6th pixel row, 0.5px below center)0.00M0.5 5H9.5 → 16.11
Maximize10×10 box, centerline inset 0.5, corner r0.25 (butt)2.51square corners → 3.38; legacy r≈1.67px → 9.72
Closeendpoints ±0.55 (= width÷2 so the round cap fills the box), width 1.1, round3.19width 1.0 → 15.74; width 1.2 → 11.08

Two systematic fixes (the root cause of the former "clunky / broken" look): ① dropping shape-rendering: crispEdges — it disables anti-aliasing, turning 45° strokes into hard stair-steps and quantizing the rounded corners into blocks (Δ15.7 vs Δ3.2); ② moving line width and line cap onto the per-glyph path presentation attributes (minimize / maximize 1px + butt, close 1.1px + round) — CSS rules outrank presentation attributes, so keeping them in <style scoped> would silently override the per-glyph values.

To match a real machine exactly, override these local variables:

.my-caption-buttons {
  --fui-caption-buttons-button-width: 46px;
  --fui-caption-buttons-glyph-size: 10px;
  --fui-caption-buttons-glyph-color: #000000; /* #ffffff in dark */
  --fui-caption-buttons-close-hover: #c42b1c;
  --fui-caption-buttons-close-pressed: #b22b26;
}

Glyph line width / line cap / path are deliberately not local variables: they interlock with the path geometry (endpoint = width ÷ 2) and differ per glyph, so overriding one of them breaks the box alignment — use the minimize-icon / maximize-icon / restore-icon / close-icon slots to swap glyphs instead.

Dragging: events only

With draggable (default true), pressing the title bar outside interactive regions emits:

EventPayload
dragStart{ pointerId, clientX, clientY }
dragMove{ pointerId, clientX, clientY, deltaX, deltaY, totalDeltaX, totalDeltaY }
dragEnd{ pointerId, clientX, clientY, totalDeltaX, totalDeltaY, canceled }
titlebarDoubleClick{ clientX, clientY } (the system behavior is toggling maximize; the component does not change state)
  • Per-event deltas (deltaX/deltaY) suit incremental updates; totals (totalDeltaX/totalDeltaY) can be applied straight to the drag origin.
  • Esc / pointercancel / losing pointer capture ⇒ dragEnd.canceled = true.
  • Pressing inside left header / center / right header / back / pane toggle / caption buttons / any native interactive element, or inside an element carrying data-fui-app-window-drag-exempt ⇒ no drag starts.
  • Title-bar text is not selectable while dragging (it is a non-client area in WinUI).
  • With roundToInteger (default false), dragMove / dragEnd totals round to whole numbers and delta is the difference between two consecutive rounded totals — so an outer CSS translate stays integer (no sub-pixel blur) with no accumulated rounding error.
Draggable window
Position: (24, 24)Drag distance: 0pxPress and drag the title bar; double-click the title bar to toggle maximize (the shell reacts to the events).
Position owned by the shell (component only emits), carried by translate

Why translate instead of left / top: translate runs on the compositor and does not trigger reflow / repaint, keeping the drag smooth; with :round-to-integer="true" the offset stays a whole number, so there is no blur or jitter from sub-pixel values.

States and integrated styling

StateExpressed byImplementation
rest / hover / pressedback, pane toggle, caption buttonscolorSubtleBackground / …Hover / …Pressed (SubtleFillColor*)
disabledisBackButtonEnabled=false / isClosable=falsecolorNeutralBackgroundDisabled + colorNeutralForegroundDisabled (ControlFillColorDisabledBrush / TextFillColorDisabledBrush)
focusTab onto a buttonWinUI double-layer focus rectangle: 2px colorStrokeFocus2 outside, 1px colorStrokeFocus1 inside
inactiveactive=falsetitle / subtitle switch to colorNeutralForeground3 (TitleBarDeactivatedForegroundBrush); icon / left header / center / right header get Opacity=0.5 (TitleBarDeactivatedOpacity)
maximizedmaximized=trueradius borderRadiusXLarge → borderRadiusNone, shadow removed, caption glyph switches to "restore"
Active / inactiveDriven by active
When inactive the title and subtitle switch foreground, and the icon / left header / content / right header drop to 50% opacity.
Zero-duration states + focus
Right header
motion="none" turns off the component-owned transitions; press Tab to see the WinUI double-layer focus rectangle.
Inactive / maximized / zero-duration states + focus

Stacking stays outside: the component writes no z-index at all. The window layer is the shell's decision (existing docs tiers: ContentDialog / Combobox popups 1000, Tooltip 1100).

Maximize / minimize animation: DWM's window open, minimize and maximize animations have no readable keyframes (they are not in microsoft-ui-xaml), so this library makes no frame-accurate claim. The component only offers controllable hooks: with motion="auto" the window frame transitions radius / shadow / background with design tokens (--durationNormal + --curveDecelerateMid, honoring prefers-reduced-motion); geometry animation is up to the shell using the same tokens (see the drag demo above).

API

FluereAppWindow

PropTypeDefaultMapping / notes
titlestring''Title: 12px / Normal; an empty string collapses the block
subtitlestring''Subtitle: 12px / Normal, weaker foreground than the title
isBackButtonVisiblebooleanfalseIsBackButtonVisible
isBackButtonEnabledbooleantrueIsBackButtonEnabled (really disables the button and removes it from the interactive set)
isPaneToggleButtonVisiblebooleanfalseIsPaneToggleButtonVisible
showCaptionButtonsbooleantrueLibrary extension: WinUI draws caption buttons in the system
isMinimizable / isMaximizable / isClosablebooleantrueForwarded to the caption buttons
maximizedbooleanfalsev-model:maximized; affects radius / shadow / glyph only, never geometry
activebooleantrueActive state; false renders the deactivated look (WinUI reads InputActivationListener)
draggablebooleantrueWhether the title bar drags (only gates the drag events)
roundToIntegerbooleanfalseRound deltas via consecutive rounded totals so a translate stays integer (see Dragging above)
labelstring—AutomationProperties.Name, falls back to title (peer GetNameCore)
motion'auto' | 'none''auto'Component-owned transitions (auto honors prefers-reduced-motion)
labelsPartial<CaptionLabels>—Overrides caption-button accessible names
localestring—Built-in copy locale (Back / Toggle Navigation), beats FluereConfigProvider
EmitPayloadNotes
backRequested—Matches WinUI BackRequested
paneToggleRequested—Matches WinUI PaneToggleRequested
update:maximizedbooleanCaption maximize / restore intent
minimize / maximize / restore / close—Caption semantic events (no window state touched)
dragStart / dragMove / dragEndsee aboveTitle-bar dragging (events only)
titlebarDoubleClick{ clientX, clientY }Double click (system behavior: toggle maximize)
SlotNotes
iconIconSource; 16×16 box
left-headerLeftHeader; participates in height
titlebar-contentContent (title-bar center)
right-headerRightHeader
titlebarReplaces the whole title bar (escape hatch)
content / defaultContent area (viewport only)
back-icon / pane-toggle-iconReplace the back / pane-toggle glyphs

FluereCaptionButtons

PropTypeDefaultNotes
maximizedbooleanfalsev-model:maximized; drives glyph and name
isMinimizablebooleantruefalse ⇒ minimize button hidden
isMaximizablebooleantruefalse ⇒ maximize / restore button hidden
isClosablebooleantruefalse ⇒ close button kept but disabled
activebooleantruefalse ⇒ dimmed glyph (inactive)
labelsPartial<CaptionLabels>—Overrides accessible names (beats locale)
localestring—Built-in copy locale
EmitPayloadNotes
update:maximizedbooleanToggle intent
minimize—Minimize intent
maximize / restore—Picked from the current maximized
close—Close intent
SlotNotes
minimize-icon / maximize-icon / restore-icon / close-iconReplace the built-in glyphs

Measured in a real browser (reproducible)

Computed styles measured with Playwright + system Chrome (1280×900 / DPR 1) on this very page (the script and screenshots are process artifacts under temp/; the conclusions are below):

QuantityMeasured
Title-bar height32 (no header slots) / 48 (with header slots)
Window radius / border / content8px / 1px / overflow: clip
Title / subtitlefont-size: 12px / foreground rgb(66, 66, 66)
Back button40 × 44, radius 4px, left offset 5 (1 border + 2 padding + 2 margin)
Caption button / area / pitch46 × 32 / 138 / 46
Caption glyph10 × 10, centered, stroke-width attribute 1 (close cross 1.1), shape-rendering: auto, top-aligned in a 48-high bar (offsetTop: 0)
Caption ink / fidelitylight rgb(0, 0, 0) / dark rgb(255, 255, 255); per-pixel mean ink difference vs. the Windows 11 reference 2.09/255 (minimize 0.00 / maximize 2.73 / close 3.59)
Minimize hover / close hoverrgb(245, 245, 245) / rgb(196, 43, 28) (= #C42B1C) + white glyph
Focus rectangleinset 0 0 0 2px rgb(0,0,0), inset 0 0 0 3px rgb(255,255,255)
Inactive titlergb(97, 97, 97)
Maximizedradius 0px, no shadow, caption glyph restore
motionnone ⇒ transition-duration: 0s; auto ⇒ border-radius, box-shadow, background-color
Dragging (real pointer)carried by CSS translate; a +60 / +40 px drag yields translate: 60px 40px (integer with roundToInteger); data-dragging=true while dragging

Implementation notes

  • Height logic mirrors UpdateHeight(): only LeftHeader / Content / RightHeader count; Title / Subtitle do not affect the height tier.
  • Three deliberate departures from the legacy system chrome for the caption glyphs (calibrated 2026-10-16): ① dropping shape-rendering: crispEdges (it disables anti-aliasing, turning 45° strokes into hard stair-steps); ② moving line width / line cap onto the per-glyph path presentation attributes (CSS outranks presentation attributes, so keeping them in the style block would silently override the per-glyph values); ③ switching the ink to the local variable light-dark(#000000, #ffffff) (= WindowCaptionForeground → SystemBaseHighColor; no token matches it). The geometry candidate scan is in "Caption buttons: the evidence gap" above.
  • Compact folding is "last declared group wins": both TitleTextGroup and DisplayModeGroup write PART_TitleText.Visibility, and the latter is declared later ⇒ Compact hides title / subtitle and moves the center content from centered to left with a 16 end margin. The criterion is not a breakpoint but "desired width of the center ≥ its available width" (contentArea.DesiredSize().Width >= contentAreaGrid.ActualWidth()), with the threshold remembered from the moment folding started; the Web side measures desired width through a hidden mirror (visibility: hidden + aria-hidden + inert).
  • Web equivalent of punching passthrough holes: pointerdown does not start a drag when it lands in [data-fui-app-window-nodrag], a native interactive element, or [data-fui-app-window-drag-exempt]. Esc cancels; pointercancel and lost capture end as canceled=true.
  • Back / pane-toggle buttons skip the AnimatedIcon animation for now: WinUI uses AnimatedBackVisualSource / AnimatedGlobalNavigationButtonVisualSource (LottieGen output, c_durationTicks = 13333333 ≈ 1.3333 s, 48×48 canvas, 12 markers, Foreground theme property). This library ships static glyphs for now; the animation lands with 0.5.4 NavigationView (the slots are already in place).
  • Known gaps: high-contrast is not implemented (WinUI HC switches the title bar to SystemControl* / SystemColor*); the material tokens landed in 0.4.4 (micaBackgroundFillColorBase / acrylicInAppFillColorDefault / layerFillColorDefault and friends — see sources.materials in packages/designs/data/repo-tokens.json), but material composition (gradient + translucent layers + noise) belongs to 0.9.1 / 0.9.2, so the window surface still uses colorNeutralBackground2 (reusing the existing SolidBackgroundFillColorBase mapping) and no backdrop-filter is written.
  • Title never writes document.title: WinUI really renames the window and restores the default title when cleared; the Web side leaves the document title alone (no docs-site or SSR pollution).
v0.5.0-alpha.1