Skip to main content

Desktop UI architecture

This page is the maintenance contract for the Avalonia UI layer. It explains where a resource belongs, how a shared component is consumed, and which layer owns a visual state.

Ownership model​

The Desktop UI is split into five layers.

LayerLocationOwns
FoundationUI/FoundationTheme templates, semantic brushes, primitive dimensions, and global selectors
ComponentsUI/Components and UI/ControlsReusable control themes, custom controls, layout primitives, and interaction behavior
ShellShellWindow chrome, navigation composition, startup surface, and shell-only resources
Screen resourcesScreens/<Screen>Data templates and styles that describe one screen or a deliberate cross-screen pattern
Screen viewsScreens/<Screen>/*View.axamlComposition, bindings, view-local event handlers, and screen-specific layout

The screen view owns the behavior. A reusable control owns the visual contract that can be consumed by more than one screen. A token owns a value that should change consistently across several consumers.

Resource pipeline​

App.axaml loads resources in this order.

OrderResourceContract
1UI/Foundation/Tokens/Primitives.axamlContext-free spacing, radius, size, and typography values
2UI/Foundation/Tokens/Semantic.axamlProduct colors, surfaces, interaction states, and status brushes
3UI/Components/ControlThemes.axamlNamed framework control themes such as TransparentPageButton and ThinScrollThumb
4Shell/WindowDecorations.axamlWindow decoration resources
5UI/Foundation/Themes/HyprismTheme.axamlApplication templates for framework controls
6UI/Components/Components.axamlShared control selectors and custom control templates
7UI/Foundation/GlobalStyles.axamlSelectors that apply across the application
8UI/Components/ManagerStyles.axamlShared manager rails, progress actions, detail toolbars, and loading placeholders

Keep this order stable. A theme template must exist before a selector customizes it, and a token dictionary must exist before a dynamic resource references it.

Semantic.axaml defines Dark and Light theme dictionaries with the same resource keys. DesktopTheme applies the persisted Theme preference (dark, light, or system) through Application.RequestedThemeVariant; the system option uses Avalonia's default variant. On a user initiated switch, DesktopTheme animates the semantic solid-color brushes from their current colors to the selected palette. The initial theme application during startup is immediate. Shared and screen styles use DynamicResource so existing views update when the variant changes. News article inline content rebuilds after theme and accent transitions because its runs and inline code controls are created in C#.

The AccentColor preference selects one of the preset colors for primary actions and news article links and code highlights. DesktopTheme updates the accent, hover, pressed, soft, and article brushes in both theme dictionaries. Article colors are adjusted separately for the dark and light palettes to keep text readable. Primary actions use white foreground content, and the selection persists across a theme change. Destructive actions keep separate danger brushes.

At the same priority, a later matching style wins. Class and pseudo-class selectors outrank plain type selectors, while a local XAML value outranks a style setter. Keep base rules before state rules and explain intentional local overrides of shared roles. See Avalonia property value precedence.

Component catalog​

Custom controls​

ComponentLocationUse it for
EmptyStateUI/Controls/Primitives/EmptyState.csA title, description, icon, and optional action for an empty screen surface
NoteCardUI/Controls/Primitives/NoteCard.csInformational or important callouts with a consistent icon and surface
FormRowUI/Controls/Primitives/FormRow.csA label, hint, optional error, and trailing editor with shared form-row structure
ModalFormUI/Controls/Primitives/ModalForm.csA modal heading, description, body, dismiss action, and action slot
OverlayModalUI/Controls/Primitives/OverlayModal.axaml and .axaml.csAnimated modal sheets, backdrop lifecycle, Escape handling, and focus restoration
FadingPopupUI/Controls/Primitives/FadingPopup.csPopups that need coordinated open and close transitions
FadingComboBoxUI/Controls/Primitives/FadingComboBox.csCombo box selection with the shared popup lifecycle

Layout and interaction primitives​

ComponentLocationUse it for
AdaptiveMasterDetailHostUI/Controls/InteractionWide master-detail presentation and compact detail navigation
WizardHostUI/Controls/InteractionWizard step ownership, transitions, and navigation
SmoothScrollViewerUI/Controls/PrimitivesApplication-owned scrolling with shared wheel and touchpad behavior
DeferredContentControlUI/Controls/LayoutDelayed creation of expensive screen content
ReorderableListControllerUI/Controls/InteractionDrag handles, previews, and drop position calculations
AspectRatioPanelUI/Controls/LayoutMedia surfaces that must retain a stable aspect ratio
StorageUsageBarPanelUI/Controls/LayoutStorage usage segments with shared measurement behavior

Use a component from this catalog before adding a new selector or helper to a screen. If the existing API cannot express the screen state, extend the component contract or record why the screen must stay local.

WizardHost handles Escape in the active wizard: it releases a focused text field, cancels a registered operation, returns to the previous step, or closes the wizard. Screens register their step commands and close action. When the master-detail layout crosses its compact breakpoint, screens synchronize the host with the current wizard state to finish an interrupted transition. A wizard with no active transition keeps its current step and reveal animation during the layout change. Opening a wizard or a step transition configured to replay the reveal starts playback.

The ownership chain is token -> theme or class -> component -> screen. Keep shared values, control appearance, reusable structure, and screen state in those respective layers.

FadingPopup preserves an explicitly configured placement. Popups with PlacementGap use adaptive placement above or below their target.

Component API and states​

ComponentSource and APIStates coveredExample
FormRowFormRow.cs, Label, Hint, Error, and trailing ContentRest, optional error, compact width, long textSettings rows
ModalFormModalForm.cs, Title, Description, DismissLabel, DismissCommand, body Content, and ActionsContentOpen, close, validation error, compact sheetJava argument form
ModalTextFieldModalTextField.cs, a TextBox content slot, IsError, and ErrorMessage; used by settings forms and profile and instance editorsRest, hover, focus, multiline input, and validation error with a warning tooltipJava argument form
OverlayModalOverlayModal.axaml.cs, IsOpen, DismissCommand, BackdropTarget, InitialFocusTarget, and ModalContentOpen, dismiss, Escape, blocked backdrop, focus restoreJava argument form
CheckBox.uiSelectionCheckHyprismTheme.axaml and Components.axamlRest, hover, pressed, keyboard focus, selected, disabledSelection controls
RadioButton.uiSelectionRadioComponents.axaml, native group behavior with a shared content templateRest, hover, pressed, keyboard focus, selected, disabledSelection controls

Keep FormRow's error line hidden while Error is empty. This keeps ordinary rows compact and centers the label and hint as one block.

Shared compositions​

PatternOwnerContract
Manager rail surfaceContentControl.managerRailSurface in UI/Components/Components.axamlA stretched, vertically scrolling surface for a list and its trailing action. The screen supplies item templates and the action.
Manager rail rows and compact actionsUI/Components/ManagerStyles.axamlShared row sizing, drag-handle spacing, selection, toolbar, menu, and progress-action states.
Selection checkboxCheckBox template in UI/Foundation/Themes/HyprismTheme.axaml; uiSelectionCheck and .row in UI/Components/Components.axamlOne check indicator with .row as the full-width list layout variant.
Selection radioRadioButton.uiSelectionRadio, Border.radioSelectionIndicator, and Ellipse.radioSelectionDot in UI/Components/Components.axamlNative RadioButton keeps group, keyboard, and accessible-name behavior. One shared template hosts the screen-provided indicator and label.

Button and action variants​

RoleOwnerUse
PrimaryUI/Foundation/GlobalStyles.axaml, Button.primaryMain confirmation or create action.
Secondary and dangerUI/Components/ManagerStyles.axaml, Button.managerActionDetail actions that need shared hierarchy, destructive state, and disabled state.
Icon, back, and menuUI/Components/ManagerStyles.axaml, detailBack, articleAction, and managerCompactMenuActionShared navigation and compact action menus. Keep a screen-specific action local when its behavior differs.
Quiet text actionUI/Components/Components.axaml, Button.quietActionFull-width manager add rows and the News load-more button, with muted text that brightens on hover.
WizardUI/Components/Components.axaml, Button.wizardActionWizard footer actions, including the create variant.
Progress and cancelUI/Components/ManagerStyles.axaml, managerAction.primary.activeLong-running manager actions that show progress and expose cancellation.

Keep these as Button variants so keyboard activation and commands stay on the native control. Add a wrapper only when it owns reusable behavior or structure.

Settings categories are separate typed views. SettingsView owns category navigation and compact master-detail behavior. The category views receive SettingsViewModel as their inherited data context and own their XAML and local interaction handlers. See SettingsGeneralView, SettingsDownloadsView, SettingsJavaView, SettingsVisualView, SettingsNetworkView, SettingsDataView, and SettingsAboutView.

Screen feature views​

ViewLocationOwnership and APIMain states
InstanceListViewInstanceListView.axaml and .axaml.csRenders the rail and item list. Exposes the rail and items to the host and raises selection, create, and drag-handle events.Empty, selected, managed, compact navigation, drag preview.
InstanceOverviewViewInstanceOverviewView.axaml and .axaml.csRenders instance status, summary, actions, and compact toolbar. Exposes its hub surface and toolbar; raises a back request.Overview, action progress or cancellation, menu open, compact layout.
InstanceModsViewInstanceModsView.axaml and .axaml.csOwns installed mods, catalog search and results, file-drop import, and catalog install transition. SetMaximumWidth receives responsive widths.Loading, empty, populated, selected, update available, incompatible, installing.
InstanceWorldsViewInstanceWorldsView.axaml and .axaml.csOwns the worlds list content.Loading, empty, populated.
InstanceLogsViewInstanceLogsView.axaml and .axaml.csOwns log search, level filters, selection, copy, auto-scroll, and its responsive width.Loading, empty, filtered, populated, compact layout.
InstanceCreatorViewInstanceCreatorView.axaml and .axaml.csOwns the branch selector, version picker, loading state, and form actions.Release or pre-release, loading, selected version.
ModCatalogPreviewView and ModCatalogInstallViewModCatalogPreviewView.axaml and ModCatalogInstallView.axamlOwn dialog content. The host keeps the OverlayModal instances and backdrop lifecycle.Preview, file loading, install confirmation, install progress.

InstancesView owns section navigation, adaptive layout, and transitions between the overview, feature views, and creator shell. Feature views inherit the typed InstancesViewModel; the host passes responsive widths and handles the small set of cross-view navigation events.

Asset catalog and audit report​

Generate a local UI inventory when reviewing styles or assets:

python3 Scripts/desktop_ui_inventory.py --output Build/ui-inventory.json

The report lists AXAML style and template definitions, class selectors and uses, repeated screen colors and numeric literals, asset resource keys, binary asset formats, native image dimensions, declared image sizes, references, and SPDX identifiers found in files. Class matching is lexical, so dynamic class assignment and selector-only template parts remain review candidates rather than build failures. Review candidates before moving values into a token or deleting a class. The report does not block a build.

Assets/Icons/MaterialSymbols.axaml records Apache-2.0 in its header. Brand dictionaries record their own SPDX identifiers or trademark terms. The report marks binary assets without license metadata in the asset file as unrecorded; check the source attribution before replacing or redistributing them. Vector geometry is colored by its consuming style when that style supplies a fill. Raster flags and illustrations retain their source colors. Instances and News use RemoteBitmapLoader for remote media. Profiles and Settings show initials when an avatar is unavailable.

Rendered component examples​

The deterministic screenshot test captures the shared selection states in English and Russian. The instance creation image shows the wizard shell and version picker. The Java view shows the radio option composition, and the Java argument image shows the shared modal form.

ContractEnglishRussian
Checkbox and radio statesSelected, disabled, and keyboard-focused selection controlsВыбранные, отключенные и сфокусированные элементы выбора
Instance creation wizardInstance creation wizard with a version selectedМастер создания экземпляра с выбранной версией
Settings form rows and radio optionsJava settings rows and runtime optionsСтроки настроек Java и варианты среды
Modal formJava argument modal formМодальная форма аргументов Java
Manager railInstances manager rail and selected detailРейка Instances и выбранная деталь

Placement rules​

If the change isPut it inDo not put it in
A raw value shared by several stylesUI/Foundation/Tokens/Primitives.axamlA view or screen style
A product color or state meaningUI/Foundation/Tokens/Semantic.axamlA hard-coded brush in a view
A complete control family templateUI/Components/Components.axaml as a ControlThemeRepeated templates in screen views
A selector for an application-wide classUI/Foundation/GlobalStyles.axamlA screen dictionary
A selector for one screenScreens/<Screen>/<Screen>Styles.axamlThe global foundation
A named visual pattern used by several screensUI/Components/ManagerStyles.axaml or a custom controlA screen-owned style dictionary
A data template for one screenScreens/<Screen>/<Screen>Templates.axamlA global resource dictionary
A handler that needs view code-behindThe owning *View.axaml resource sectionA standalone resource dictionary
A reusable behavior or controlUI/Controls/<category>A static helper inside a view
Window-only behaviorShellA screen folder

Each view includes its own style dictionary through UserControl.Styles. Shared manager rails, progress actions, detail toolbars, and loading placeholders use neutral class names and live in UI/Components/ManagerStyles.axaml.

Consuming a shared component​

The screen supplies content and state. The component supplies hierarchy, spacing, and the default visual states:

<controls:EmptyState IsVisible="{Binding !HasInstances}"
Title="{Binding CreateInstanceTitle}"
Description="{Binding CreateInstanceHint}">
<controls:EmptyState.Icon>
<Image Width="56"
Height="56"
Source="avares://Hyprism.Desktop/Assets/Fluent/puzzle.png" />
</controls:EmptyState.Icon>
<controls:EmptyState.ActionContent>
<Button Classes="primary" Click="OnOpenCreatorClicked">
<TextBlock Text="{Binding SelectVersionLabel}" />
</Button>
</controls:EmptyState.ActionContent>
</controls:EmptyState>

Keep screen strings and commands in the view model. Keep component properties small and typed around the visual contract. Prefer DynamicResource for tokens so theme changes propagate without copying values.

<controls:FormRow Label="{Binding JavaPathLabel}"
Hint="{Binding JavaPathHint}"
Error="{Binding JavaPathError}">
<TextBox Text="{Binding JavaPath}" />
</controls:FormRow>

Use FormRow.Error for a validation message that belongs to one row. ModalForm provides the shared form layout inside OverlayModal; the profile and instance editors compose their own sheets. BackdropTarget optionally blurs and blocks the underlying view. Settings modals omit it and use only the overlay dimming. InitialFocusTarget names the first input. Closing the modal restores the previous focus and any backdrop effect.

OverlayModal draws the bottom edge of an open sheet and updates the ancestor mainSceneFrame border while any sheet remains visible. The frame regains its bottom border after the final closing animation. New modals do not need a shell view model registration for this state.

<controls:OverlayModal IsOpen="{Binding IsEditorOpen}"
DismissCommand="{Binding CloseEditorCommand}"
InitialFocusTarget="{Binding ElementName=EditorTextBox}">
<controls:OverlayModal.ModalContent>
<controls:ModalForm Title="{Binding EditorTitle}"
Description="{Binding EditorHint}"
DismissLabel="{Binding CancelLabel}"
DismissCommand="{Binding CloseEditorCommand}">
<TextBox x:Name="EditorTextBox" Text="{Binding EditorValue}" />
<controls:ModalForm.ActionsContent>
<Button Classes="primary" Command="{Binding SaveEditorCommand}">
<TextBlock Text="{Binding SaveLabel}" />
</Button>
</controls:ModalForm.ActionsContent>
</controls:ModalForm>
</controls:OverlayModal.ModalContent>
</controls:OverlayModal>

Style and state contract​

Use a ControlTheme when a control family needs a complete template and named states. Use a plain Style for a selector, a class, or a narrow state adjustment. Use Classes.<name> bindings when a view model state changes a visual state.

Every interactive shared component should account for the states that apply to it.

StateExpected review
RestBaseline surface, text, icon, and spacing
Pointer overClear but restrained hover feedback
PressedImmediate pressed feedback without layout movement
Focus visibleKeyboard focus remains visible
DisabledContrast and cursor communicate unavailable action
SelectedSelection survives pointer movement and compact layouts
LoadingProgress replaces or accompanies the action without changing its meaning
Success, warning, errorStatus uses semantic brushes and localized text
EmptyThe user sees what is absent and the next useful action

Do not use animation as the only state signal. Keep transitions on render properties when a layout property would cause remeasure or scroll changes.

Screen templates and views​

Screen resource files follow the owning screen.

ScreenTemplate resourcesStyle resources
InstancesScreens/Instances/InstancesTemplates.axamlScreens/Instances/InstancesStyles.axaml
ProfilesScreens/Profiles/ProfilesTemplates.axamlScreens/Profiles/ProfilesStyles.axaml
NewsScreens/News/NewsArticleTemplates.axamlScreens/News/NewsStyles.axaml
SettingsSettingsView host, seven category views, and view-owned overlay instancesScreens/Settings/SettingsStyles.axaml

Templates are merged by the owning view when they are not application-wide. A template that references a view event handler stays in the view resource section because a standalone dictionary has no owner for that handler.

Views keep x:DataType and use compiled bindings. A view may contain layout and event wiring, but it should not define a second application-wide button or input template.

Responsive ownership​

AdaptiveMasterDetailHost owns the transition between wide and compact master-detail behavior. Screen view models own selected items and commands. The view owns size observation and visual host changes.

When adding a responsive state, review the wide and compact layouts together.

  • Keep the same command and localization contract.
  • Preserve keyboard focus or provide a deterministic focus target after navigation.
  • Keep destructive actions available through the compact action surface.
  • Test long localized labels and narrow widths.
  • Avoid duplicating a second state machine in code-behind.

Tests and review gates​

UI changes require a Desktop build and the relevant headless tests. Reusable controls should have focused tests for their public properties and rendered states. Screen changes should cover the state transition that consumes the component.

Useful existing test locations include Tests/Hyprism.Desktop.Tests/NoteCardTests.cs, OverlayModalAnimationTests.cs, MainWindowRenderTests.cs, and InstanceSectionRenderTests.cs.

Before merging a UI change, verify the resource path, the owner dictionary, the state matrix, English and Russian strings, keyboard behavior, compact layout, and the rendered result.

The higher-level composition rules remain in Desktop architecture.

Edit this page on GitHub