Repository navigation
Command Surfaces
Cursorial.UI.Bars is the command-surface suite: a Toolbar, a Ribbon, and floating/drop-down menus are three
surfaces over one shared set of command-driven bar controls. You define each command once as a BarCommand, bind
it to whichever surfaces you want, and every surface fills in its own label, icon, and gesture hint from the command —
"define once, surface anywhere". A toolbar button, a ribbon toggle, and a mini-toolbar entry that all invoke Save
are the same command with no per-surface duplication.
The controls are ordinary Cursorial.UI controls: the bar buttons derive from ButtonBase, the
surfaces from ItemsControl/TabControl, so the same styling,
data binding, and input and focus machinery applies. Looks come from a
self-registering theme in a box-drawing idiom that tracks the active palette and dark/light flips.
Namespace note. Every type here lives in
Cursorial.UI.Bars. The suite registers its own control themes at module load (no consumer dictionary merge needed). To address the bars from XAML, register the assembly once —XamlSchemaContext.Default.RegisterAssembly(typeof(Cursorial.UI.Bars.Toolbar))— after which the controls are available under the defaulthttps://cursorial.dev/uixmlns, unprefixed like every other Cursorial control.
BarCommand is the model every surface binds to — a UIObject (not a control) that pairs an ICommand with the
display metadata a command surface needs. Its headline properties are Command (ICommand? — the bindable inner
behavior, typically a view-model command), Text (the label), Icon (object? — an Icon, an
IconCarrier, or a glyph string), InputGestureText (the shortcut hint, display-only), IsCheckable, and
Description (rich hover-help body — see SuperTips). Delegate constructors wrap a plain action in the
inner command as sugar; an optional previewExecute/cancelPreviewExecute pair on the parameterized constructor makes
that inner command previewable (an IPreviewableCommand), which a value drop-down uses to dry-run a value live — see
Value commands and live preview.
BarCommand forwards every ICommand member to its inner Command; while that is null, CanExecute answers
false, so bound surfaces grey out until a command binds. A bar control auto-fills any unset Content/Icon/
InputGestureText from a bound BarCommand (via SetCurrentValue, so an explicit author or style value still
wins) — one declaration drives the face on every surface that hosts it.
var save = new BarCommand(() => document.Save())
{
Text = "_Save",
Icon = "💾",
InputGestureText = "Ctrl+S",
Description = "Save the current document to disk.",
};
// The same command on three surfaces — each fills its own label / icon / gesture:
var toolbarButton = new BarButton { Command = save };
var ribbonButton = new BarButton { Command = save };
var miniButton = new BarButton { Command = save };A raw ICommand still works on any bar control; the control then needs its own display properties set. Call
RaiseCanExecuteChanged() to re-query every bound surface (the inner command's own raises are forwarded
automatically, and Execute follows a run with a courtesy re-raise).
All of these are the same controls a Toolbar, a Ribbon, and a MiniToolbar host — build them once, host them
anywhere.
-
BarButton— a command button (ButtonBase). AddsIcon(object?) andInputGestureTextbeside the inheritedContentlabel andClick/Command/IsPressed. Auto-fills all three from a boundBarCommand. -
BarToggleButton— a checkable button (ToggleButton): checked = accent whole-cell fill,:checked. When itsCommandParameteris anICheckableCommandParameterthe checked state is command-owned (the command'sExecutemutates the parameter and the button re-reads it, and it no longer self-toggles on click), so one command drives the checked state on every surface without anIsCheckedbinding. Hand several toggles one command with aValueCommandParameter<T>each and they become a radio group with no extra wiring (see Value commands and live preview). -
BarSplitButton— two hit zones: the label runs the primary action (like aBarButton), and a tinted▾zone opens a dropdownPopupoverDropDownContent. -
BarPopupButton— a whole-control opener (a bare▾caret): a click / Space / Enter / Down toggles the dropdown overDropDownContent; it has no separate primary action. -
BarComboBox— a compact bar-faceComboBox([value ▾]): single selection, drop-down list, type-ahead, editable mode. Give it aCommandplus aValueCommandParameter<T>and the drop-down becomes a value picker — a commit gesture writes the chosen value and executes; a dismissal restores the pre-open selection. If the command is additionally anIPreviewableCommand, moving the highlight dry-runs the value live — see Value commands and live preview. -
BarGallery— aBarComboBoxwhose drop-down lays its choices out as a grid of preview cells (aWrapPanelitems host) rather than a vertical list; the same value-command and live-preview support. -
BarSeparator— a thin rule between clusters (an upright│on a horizontal bar; the toolbar flips it to a horizontal─in the overflow popup). -
BarLabel— a non-interactive caption (ContentControl) titling a cluster; not focusable, and its access-key (_) forwards focus into the cluster to its right.
The drop-openers (BarSplitButton/BarPopupButton, over the shared BarDropDownButton base) carry a
DropDownPlacement (PlacementMode) that picks their navigation model — Bottom opens parked on the face (a second
Down enters), side placements open and enter in one press — and FocusContentOnOpen opts a bottom opener into
enter-on-open. IsDropDownOpen is two-way with the popup.
var bold = new BarToggleButton { Command = boldCommand, CommandParameter = new CheckableCommandParameter() };
var paste = new BarSplitButton { Command = pasteCommand, DropDownContent = pasteSpecialMenu };
var align = new BarPopupButton { Content = "Align", DropDownContent = alignMenu };
var zoom = new BarComboBox
{
ItemsSource = new[] { 50, 75, 100, 150, 200 },
Command = zoomCommand,
CommandParameter = new ValueCommandParameter<int>(100),
};A value drop-down talks to a shared command through a value parameter — ValueCommandParameter<T> (in
Cursorial.UI, so several controls can bind one command, each with its own parameter instance). Give a BarComboBox
or BarGallery a Command plus a ValueCommandParameter<T> and its drop-down becomes a picker: a commit gesture
(Enter / Space / item click) writes the chosen item into the parameter's Value and executes the command once,
while a dismissal (Escape / light-dismiss / focus-out / toggle-close) restores the pre-open selection — so an unchosen
value never sticks to the model or the face. The value type T is the app's to choose, so no default parameter is
auto-provisioned; supply it explicitly. (This value channel works with any ICommand.)
Live preview. When the command is also an IPreviewableCommand (in Cursorial.UI) — a BarCommand built with the
previewExecute/cancelPreviewExecute delegate pair — moving the highlight dry-runs the value: the control writes
the highlighted item into the parameter's PreviewValue and calls Preview (a dry-run Execute that produces the
outcome but commits nothing). The close is atomic — it always calls CancelPreview first to unwind the dry-run
byte-exact, and only a real commit gesture then lets Execute do the thing for real on clean state, so an ordinary
Execute needs zero preview awareness. CancelPreview is a cleanup verb structurally outside the CanExecute gate, so
a command that greys itself mid-session still has its dry-run unwound and its commit refused. A command that is not
previewable never receives a preview verb — nothing tentative is ever shown, and a dry-run can never be mis-executed as
the real thing.
var applyFont = new BarCommand(
execute: p => model.Font = ((ValueCommandParameter<string>)p!).Value,
previewExecute: p => model.PreviewFont(((ValueCommandParameter<string>)p!).PreviewValue!),
cancelPreviewExecute: _ => model.EndFontPreview());
var fontPicker = new BarGallery
{
ItemsSource = installedFonts,
Command = applyFont,
CommandParameter = new ValueCommandParameter<string>(model.Font),
};Radio sets. ValueCommandParameter<T> derives from CheckableCommandParameter, so a set of BarToggleButtons that
share one command — each with its own parameter instance — becomes a radio group: on every re-query the command's
CanExecute compares each parameter's Value to the model's current value and writes the matching one's checked state;
Execute applies the clicked control's Value. Call the command's RaiseCanExecuteChanged() when the model moves and
the whole set re-syncs. A CheckableCommandParameter.Override(value) / Release() pair is the context-gate that
greys-and-locks a bound control's checked state while a value is inapplicable (pair Override with a false
CanExecute to grey and lock; Release gives the control back its own preference).
Toolbar (an ItemsControl) is a single horizontal row of bar controls with discrete overflow: when the row
is too narrow the trailing items fold into a » chevron Popup. The fold re-parents the same live control
instances between the row and the popup band as the bar resizes — no rebuild. Add controls to Items (or bind
ItemsSource); read HasOverflow/OverflowCount and toggle IsOverflowOpen. The per-item policy is the attached
OverflowMode (ToolbarOverflowMode): AsNeeded (the default — folds from the trailing edge), Always (always in
the popup), or Never (pinned to the row). The toolbar is a single Tab stop with arrow navigation within, and
returns focus to where it came from on Escape or after an invoke.
var toolbar = new Toolbar();
toolbar.Items.Add(new BarButton { Command = cut });
toolbar.Items.Add(new BarButton { Command = copy });
toolbar.Items.Add(new BarSeparator());
toolbar.Items.Add(new BarToggleButton { Command = bold });
Toolbar.SetOverflowMode(cutButton, ToolbarOverflowMode.Never); // a signature control never folds awayMiniToolbar is the right-click floating strip — a horizontal bar of the same controls shown at the pointer.
Attach one to any element with the Bar attached property; a right-click over that element opens it (and it wins the
right-click over a co-present ContextMenu).
var mini = new MiniToolbar();
mini.Items.Add(new BarButton { Command = bold });
mini.Items.Add(new BarButton { Command = italic });
MiniToolbar.SetBar(editorSurface, mini); // right-click over `editorSurface` floats the stripRibbon (a TabControl) is the full command surface: a tab strip over the selected tab's band of groups. Its
items are **RibbonTab**s, each hosting **RibbonGroup**s in its Groups collection (the XAML content). A group is
a HeaderedItemsControl whose Header is the bottom group name and whose items are the same bar controls — used
directly, no wrapper. Set HasDialogLauncher for the ⋰ corner launcher (raises DialogLauncherRequested).
var ribbon = new Ribbon();
var home = new RibbonTab { Header = "_Home" };
var clipboard = new RibbonGroup { Header = "Clipboard", HasDialogLauncher = true };
var paste = new BarSplitButton { Command = pasteCommand, DropDownContent = pasteMenu };
Ribbon.SetButtonSize(paste, RibbonButtonSize.Large); // the group's hero button
clipboard.Items.Add(paste);
home.Groups.Add(clipboard);
ribbon.Items.Add(home);Button sizes. The inherited attached Ribbon.ButtonSize (RibbonButtonSize.Medium/Large/Small) picks a
control's face — set it per-control or as a group default. Large is the tall glyph-over-label hero;
Medium is the single-row toolbar face; Small is the densest inline cell.
Density collapse. As the band narrows, each group demotes through three discrete tiers (RibbonGroupDensity):
Normal → Compact (every control drops to its small inline face) → Collapsed (the whole group becomes a
[name ▾] dropdown whose controls move into a flyout). The band owns the width budget; cap how far a group may
demote with the attached Ribbon.MinDensity (Normal pins a signature group full-size; Compact forbids the
dropdown).
Control-group stacking. RibbonControlGroup packs a handful of related small/medium controls into the band's
two rows beside a Large hero, so a tall band never wastes its second row. Its ItemSize (default
RibbonButtonSize.Medium) stamps the children's face; RowBehavior (Auto/SingleRow/TwoRow) chooses the row
count (Auto stacks only when the band is authored two rows tall); and the attached RibbonControlGroup.RowBreak
pins the split point when the width heuristic guesses wrong.
var small = new RibbonControlGroup { ItemSize = RibbonButtonSize.Small };
small.Items.Add(new BarButton { Command = cut });
small.Items.Add(new BarButton { Command = copy });
RibbonControlGroup.SetRowBreak(pasteToggle, true); // this child starts the second row
clipboard.Items.Add(small);Layout mode. Ribbon.LayoutMode (RibbonLayoutMode) is the user-directed density axis, distinct from the
width-driven fold: Classic (full presentation), Simplified (one labeled row — Large faces demote, group footers
collapse), or Compact (one icon-only row). Authored faces are never touched, so flipping back to Classic restores
them exactly.
Contextual tabs. Set RibbonTab.IsContextual for a conditional, purple-tinted tab (a "Table Tools"-style tab);
bind its Visibility so it appears only when relevant. Hiding the selected contextual tab redirects selection so the
band never blanks.
Backstage. Set RibbonTab.IsFileTab for the accent File tab — a click raises Ribbon.BackstageRequested instead
of selecting a band. Backstage (a TabControl) is the File view: a vertical rail of BackstageItem
destinations over a swapping detail pane, with a DisplayMode of FullScreen (a maximized surface) or Menu (a
File-anchored popup). The app opens the Backstage in response to the event and closes it on BackRequested (the ◂
back or Escape).
ribbon.Items.Add(new RibbonTab { Header = "_File", IsFileTab = true });
ribbon.BackstageRequested += (_, _) =>
{
var backstage = new Backstage { DisplayMode = BackstageDisplayMode.Menu };
backstage.Items.Add(new BackstageItem { Header = "_New", Content = newPane });
backstage.Items.Add(new BackstageItem { Header = "_Open", Content = openPane });
backstage.BackRequested += (_, _) => CloseBackstage();
ShowBackstage(backstage);
};Quick Access Toolbar. Ribbon.QuickAccessCommands is the ordered set of BarCommands on the QAT — the same
commands the groups bind. QuickAccessCandidates is the superset the customize ▾ checklist offers.
QuickAccessPlacement moves the QAT above the strip (default) or below the body.
ribbon.QuickAccessCommands.Add(save);
ribbon.QuickAccessCommands.Add(undo);
ribbon.QuickAccessCandidates.Add(redo); // offered in the customize ▾, not yet pinnedMinimizable band. Ribbon.IsMinimized collapses the body to a tabs-only strip (Ctrl+F1, a double-click on a tab,
or the pin chevron toggle it; clicking a content tab while minimized floats the band temporarily). Vertical
navigation crosses the strip ↔ body boundary, and the whole ribbon is one returning focus scope, so Escape returns to
wherever focus was before the ribbon was entered.
KeyTips are the Alt-overlay accelerator badges (the ribbon's multi-level drill-in: Alt → tab → group → control).
Arm them once per application with EnableKeyTips(); they are gated on the same negotiated keyboard capability as the
core AccessKeyManager. Each control's badge letter is auto-derived from its access-key / command text, or pinned
explicitly with the attached KeyTip.Key; suppress a badge with KeyTip.SetAutoAssign(control, false).
app.EnableKeyTips(); // arm the Alt-overlay badges
KeyTip.SetKey(pasteButton, "V"); // pin an explicit badge instead of auto-derivingSuperTip is a rich, titled hover tip (Title + InputGestureText shortcut + KeyTip hop sequence +
Description body + optional Footer). It is ordinary ToolTipService content, so it shows through the same hover
controller as a plain tooltip — no parallel machinery. A BarCommand with a Description auto-provisions one on
every bound bar control, so the rich help is identical wherever the command appears; attach one explicitly with
SuperTip.FromCommand.
ToolTipService.SetTip(button, SuperTip.FromCommand(save));Every bar control's look is a code-first control theme in CursorialBarsTheme, keyed by control Type and reusing
the core ThemeKeys palette spine — so the bars track the active palette and dark/light flips with the rest of
the framework. BarsThemeModule registers the whole dictionary into the framework's assembly
ThemeContributions tier at module load (a chain hop between the app theme and the built-in backstop), so the
suite renders self-contained and each template's {DynamicResource} reference resolves. Control themes resolve by
exact key; a subclass (e.g. a custom Ribbon) opts into the base theme by overriding Control.ControlThemeKey to
return the base type. See Styling and themes for the resolution chain and how to override a
control theme wholesale.
-
Controls — the base control catalog the bar controls extend (
ButtonBase,ComboBox,TabControl,Popup,Icon). - Input and focus — routed commands, access keys, focus scopes, and returning focus.
- Styling and themes — selectors, pseudo-classes, control themes, and theme contributions.
-
Data binding — wiring a
BarCommand.Commandto a view-modelICommand. - XAML — authoring surfaces declaratively (and registering the bars assembly).
Cursorial.Core
Cursorial.Rendering
Drawing & Animation
Cursorial.UI
- Overview
- Layout & panels
- Controls
- Styling & themes
- Data binding
- Input & focus
- Windowing
- Animation & transitions
Command & data surfaces
Declarative