Skip to content

UI Windowing

Mike Strobel edited this page Jun 26, 2026 · 1 revision

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.

The window manager

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.ActiveWindow and the ActiveWindowChanged event.
  • app.WindowManager.ScreenSize — the current viewport in cells.
  • FitAllWindowsToViewport() / DismissFitBadge() — drive the fit affordance (see below).

Window

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).

Key properties

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.

Lifecycle events

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.

Chrome interactions

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 vetoed Closing keeps 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.

Modal dialogs

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

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();
  • IsOpen is two-way by default and write-back: every close reason (light dismiss, Escape, host events) flips it back to false, so a {Binding} stays in sync. Open() / Close() are the imperative equivalents.
  • Placement (Bottom/Top/Left/Right/Center/Pointer) positions the surface relative to PlacementTarget (defaults to the logical parent). Placement flips-then-clamps to keep the popup on screen. HorizontalOffset / VerticalOffset nudge it after placement.
  • StaysOpen — when true, an outside press does not dismiss it (default false: light dismiss is on).
  • CloseOnEscape — Escape closes it (default true); the route reaches the popup through the logical parent.
  • KeepOpenOnAnchorPress — when true, a dismiss-triggering press that lands on the PlacementTarget doesn'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.

Shadows

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 shadow

Window.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.

The screen-resize policy and the fit badge

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.

Mouse subtree capture

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 dismisses

Capture here is routing policy, not OS capture — None is simply the absence of a holder, so it isn't a member of the enum.

See also

  • UI Overview — the framework, the frame loop, and UIApplication.
  • Input & focus — routed events, focus scopes, and mouse capture.
  • ControlsMenu, ContextMenu, ComboBox, and ToolTip build on Popup.
  • Styling & themes — replacing the window chrome with a control theme.

Clone this wiki locally