-
Notifications
You must be signed in to change notification settings - Fork 0
UI Windowing
Cursorial.UI gives you real windows inside the terminal — draggable, resizable, maximizable Windows, modal
dialogs you await, and light-dismiss Popups for menus and tooltips. There are no OS windows involved: the
WindowManager is the window system, compositing every surface into the single cell grid the renderer paints.
Reach for this when one screen of content isn't enough — a tool palette over a document, a confirmation dialog, a
drop-down menu — or when you want overlapping, independently-activatable regions. If you've used WPF, the shapes are
familiar: Window, Window.ShowDialog, Popup, owner relationships, and WindowStartupLocation.
UIApplication owns a WindowManager once it's running (app.WindowManager). It maintains a surface stack —
the application root surface at the bottom, then windows in z-order, then a popup band above all windows, then a
fit-to-viewport badge on top — and concatenates them into one composite per frame. Each window or open popup wraps
its own render tree at a screen offset, so moving a window is a composite-only operation (no re-raster of its
content).
You rarely touch WindowManager directly: Window.Show() and Popup.IsOpen = true find the manager for you. Its
public surface is mostly observational:
-
app.WindowManager.Windows/.Popups/.Surfaces— the live collections (z-ordered). -
app.WindowManager.ActiveWindowand theActiveWindowChangedevent. -
app.WindowManager.ScreenSize— the current viewport in cells. -
FitAllWindowsToViewport()/DismissFitBadge()— drive the fit affordance (see below).
Window derives from ContentControl, so its Content is whatever you put in it (text, a panel, a control tree),
laid out and themed exactly like any other control. The chrome — title bar, ✕ button, maximize, resize grip — comes
from a control theme, so an app-level Window style can replace it wholesale.
var win = new Window
{
Title = "Inspector",
Content = new StackPanel
{
Children =
{
new TextBlock { Text = "Properties" },
new Button { Content = "_Close" },
},
},
WindowStartupLocation = WindowStartupLocation.CenterScreen,
};
win.Show(); // modeless; activates it (WPF semantics)Show() hosts the window on the application's manager (or its owner's, if it has an Owner) and activates it.
Show(WindowManager) targets a specific manager. Activate() brings an already-shown window to the top of its band
and returns false if no manager hosts it (or it's blocked behind a modal dialog).
| Property | Meaning |
|---|---|
Title |
The window title; the active main window mirrors it to the terminal's title (OSC 2). |
WindowStyle |
TitleBar (chrome) or None (chrome-less but still opaque/occluding). |
WindowState |
Normal or Maximized. Setting it directly behaves identically to the maximize chrome action. |
WindowStartupLocation |
Manual (use Left/Top), CenterScreen, or CenterOwner. |
Left / Top
|
Screen-space position in cells (signed). Moving is composite-only. |
SizeToContent |
Manual, Width, Height, or WidthAndHeight (the default — the window sizes to its content). |
CanMove / CanResize / CanClose
|
Gate the corresponding chrome interactions (all default true). |
IsActive |
Read-only; true when this is the focused top-level window. |
IsShown |
Read-only; true between Show and Close. |
Shadow |
A WindowShadow (see below). |
Owner |
The owning window; settable only before the first Show (throws after). Owned windows close when the owner closes. |
win.Closing += (_, e) =>
{
if (HasUnsavedWork && e.CanCancel)
e.Cancel = true; // veto a cancelable close
};
win.Closed += (_, _) => Log("inspector closed");
win.Activated += (_, _) => RefreshSelection();Closing fires before the window is removed and can veto cancelable closes (a programmatic Close() or the ✕
button — but not an owner-cascade or manager shutdown). Closed, Activated, and Deactivated are plain
EventHandlers. Window.Close() closes it; Close(object? dialogResult) sets DialogResult first.
The chrome is interpreted by role, not by part name, so a custom template just tags its parts and the behaviors follow:
-
Drag the title bar to move (honoring
CanMove). - The ✕ button closes (honoring
CanClose); a vetoedClosingkeeps it open. - The maximize action toggles
WindowState. - Drag the lower-right grip to resize (honoring
CanResize).
WindowStyle.None removes the title band but the window still occludes what's behind it.
ShowDialogAsync shows a window modally and returns a Task that completes when it closes — the running frame
loop is the pump, so awaiting it never blocks the UI thread (no nested dispatcher). While the dialog is up, its
owner (and everything behind it) is input-blocked; poking a blocked window flashes it for attention.
var dialog = new Window { Title = "Confirm", Content = BuildConfirmUI(), Owner = win };
// Typed overload: a normal close returns DialogResult as TResult (or default when null/mismatched).
bool? result = await dialog.ShowDialogAsync<bool?>();
if (result == true)
Commit();Inside the dialog, set DialogResult and close:
okButton.Click += (_, _) => dialog.Close(dialogResult: true);Cancellation throws. Pass a CancellationToken; if it fires, the dialog is force-closed on the UI thread and the
await raises OperationCanceledException — it never silently returns a default. A pre-canceled token returns a
canceled task with no side effects.
try
{
var choice = await dialog.ShowDialogAsync<string>(cancellationToken);
Apply(choice);
}
catch (OperationCanceledException)
{
// the dialog was canceled, not answered
}The non-generic ShowDialogAsync(CancellationToken) returns Task<object?> (the raw DialogResult); the generic
ShowDialogAsync<TResult> casts it for you.
Popup is the light-dismiss primitive the menu, combo-box, and tooltip controls build on. It lives in your logical
tree — so its Child inherits DataContext, resources, and styles, and Escape/key routing flows back to the host
— but contributes nothing to the host's visual layout. When opened, its Child roots a separate surface the manager
places in the band above all windows.
var popup = new Popup
{
PlacementTarget = anchorButton,
Placement = PlacementMode.Bottom,
Child = new Border { Child = new TextBlock { Text = "I'm floating" } },
};
anchorButton.Click += (_, _) => popup.IsOpen = true; // or popup.Open();-
IsOpenis two-way by default and write-back: every close reason (light dismiss, Escape, host events) flips it back tofalse, so a{Binding}stays in sync.Open()/Close()are the imperative equivalents. -
Placement(Bottom/Top/Left/Right/Center/Pointer) positions the surface relative toPlacementTarget(defaults to the logical parent). Placement flips-then-clamps to keep the popup on screen.HorizontalOffset/VerticalOffsetnudge it after placement. -
StaysOpen— whentrue, an outside press does not dismiss it (defaultfalse: light dismiss is on). -
CloseOnEscape— Escape closes it (defaulttrue); the route reaches the popup through the logical parent. -
KeepOpenOnAnchorPress— whentrue, a dismiss-triggering press that lands on thePlacementTargetdoesn't close the popup, so the anchor owns the open/close toggle. Drop-toggle controls (ComboBox, DatePicker) set this to avoid the dismiss-then-reopen race. -
IsHitTestTransparent— the surface is transparent to hit testing and never appears in the surface scan; the pointer falls through to the content beneath. This is the tooltip case: it floats over content but never steals hover or clicks.
Opened and Closed fire on open/close; Closed carries a PopupClosedEventArgs.Reason (Programmatic,
LightDismiss, EscapeKey, HostClosed, ScreenResized, …). A menu/combobox popup that held keyboard focus
returns focus to the element that opened it on close; a hover-only popup or tooltip leaves focus alone.
WindowShadow is a soft drop shadow drawn beneath the surface. It's a readonly record struct of a
ShadowGeometry plus a Color:
win.Shadow = WindowShadow.Default; // a soft drop from the bottom & right at half strength
win.Shadow = WindowShadow.None; // no shadowWindow.Shadow defaults to WindowShadow.Default; Popup.Shadow defaults to WindowShadow.None. The surface grows
by the shadow's margins so it has cells to paint into. Shadows are RGB-only — they're a no-op on palette-only color
tiers.
When the terminal shrinks, Cursorial does not auto-shrink your windows — it clamps each so a minimum number of
cells stays on screen, but the size you chose is your choice. A window that ends up overhanging the viewport sets its
read-only IsClippedByViewport flag, and the manager shows a fit badge in the top-right corner. Call
app.WindowManager.FitAllWindowsToViewport() to snap everything back inside (or DismissFitBadge() to clear the
affordance). app.WindowManager.IsFitBadgeVisible reports whether it's showing.
The same CaptureMouse / ReleaseMouseCapture an element uses (see Input & focus) takes a
CaptureMode that matters across surfaces:
-
CaptureMode.Element(the default) — every mouse event routes to the capture holder regardless of what the pointer is over. This is the slider/scrollbar-drag gesture: the holder owns all input until release. -
CaptureMode.SubTree— events route normally while the pointer is over the holder or an element in its subtree (so descendants stay interactive), and redirect to the holder only when the pointer is outside it. This is the menu/combo-box pattern: items inside the open popup stay live while an outside press still reaches the owner to dismiss it.
menuRoot.CaptureMouse(CaptureMode.SubTree); // items interactive; outside press dismissesCapture here is routing policy, not OS capture — None is simply the absence of a holder, so it isn't a member of
the enum.
-
UI Overview — the framework, the frame loop, and
UIApplication. - Input & focus — routed events, focus scopes, and mouse capture.
-
Controls —
Menu,ContextMenu,ComboBox, andToolTipbuild onPopup. - Styling & themes — replacing the window chrome with a control theme.
Cursorial.Core
Cursorial.Rendering
Drawing & Animation
Cursorial.UI
- Overview
- Layout & panels
- Controls
- Styling & themes
- Data binding
- Input & focus
- Windowing
- Animation & transitions
Declarative