Skip to content

Desktop UI

Valkerran edited this page Oct 2, 2026 · 5 revisions

Desktop UI

PCEdit.Desktop is the only UI head: Avalonia 12, running on Linux, Windows and macOS. It is deliberately thin — a set of views, a handful of converters, and the implementations of the platform interfaces App Core declares.

Package Role
Avalonia, Avalonia.Desktop, Avalonia.Themes.Fluent, Avalonia.Fonts.Inter The UI framework (kept in lockstep)
CommunityToolkit.Mvvm ObservableObject, [ObservableProperty], [RelayCommand]
Microsoft.Extensions.DependencyInjection The composition root
Microsoft.ICU.ICU4C.Runtime.linux-x64 App-local ICU — referenced for every build, but only a linux-x64 publish carries its libraries and loads them. See Building & Packaging
Avalonia.BuildServices Referenced only to exclude it — see below

Exact versions live in PCEdit.Desktop.csproj, and the resolved graph — every transitive package, with its content hash — in the committed packages.lock.json. Dependabot proposes updates weekly, with the Avalonia packages grouped so they move together.

No build-time telemetry

Avalonia pulls in Avalonia.BuildServices, whose targets run a task on every build that reports to avaloniaui.net (hashed project and machine name, a machine GUID, OS, IDE, CI detection). It is build-time only and never reaches a published PCEdit, but it fires on every contributor machine and CI run. Since v1.4.3 the csproj references it directly with ExcludeAssets="all", which drops its targets entirely, and CI fails if obj/*.nuget.g.targets ever imports it again. Its version is pinned deliberately: an Avalonia upgrade that needs a newer one fails restore with NU1605 instead of quietly re-enabling the reporting.

Startup

Program.cs → App.axaml.cs:

  1. Build the IServiceProvider — Core services as singletons, page ViewModels as singletons, and the Avalonia implementations of every platform interface.
  2. Apply the stored UI culture.
  3. Show MainWindow.

Program.BuildAvaloniaApp() calls .WithInterFont(), so Inter is the app-wide font.

<AssemblyName>PCEdit</AssemblyName> — the published executable is PCEdit (which is what the AppImage packaging expects) and asset URIs are avares://PCEdit/..., not PCEdit.Desktop.

Shell

MainWindow.axaml is a SplitView:

  • Nav pane — a ListBox Classes="nav" bound two-way to MainWindowViewModel.SelectedNavItem. The selected row shows the terraform-spectrum gradient on its leading edge over a faint tint. Below it, the 15-locale language ComboBox; in the footer, the loaded file's path.
  • Content — a Grid: row 0 is the header bar (page title, the primary Save button with HotKey="Ctrl+S", and the save/dirty state); row 1 is an error banner, shown only when a page's Load() threw, so a malformed save cannot take the app down with the file still open; row 2 is a ContentControl bound to CurrentPage.

MainWindow.axaml.cs handles two things itself:

  • Closing — if the workspace is dirty, the close is cancelled and a discard-confirmation dialog is shown.
  • First open — the disclaimer dialog appears when IDisclaimerGate.HasAcknowledged is false.

The other page views are pre-built at Background priority so the first navigation is not a stall.

ViewLocator

ViewLocator maps PCEdit.App.Core.ViewModels.XxxViewModel → PCEdit.Desktop.Views.XxxView and caches the view per ViewModel instance. Since page ViewModels are singletons, re-navigating does not rebuild the visual tree.

Theme and styles

App.axaml defines colour tokens (SurfacePage, SurfaceCard, BrandFill, Status*Text, …) in ResourceDictionary.ThemeDictionaries for Light and Dark — a Planet Crafter terraforming palette: rust world → blue sky → green biosphere, with a muted biosphere green as the brand accent. RequestedThemeVariant="Default", so the app follows the OS theme.

Important

The token keys are referenced by name from the styles and views, always as DynamicResource. Renaming a token silently breaks the status colours. Keep them stable.

Styles/Controls.axaml holds the type scale — h1 (hero), pageHeading, h2 (section), caption, micro — and Border.headingRule, the green bar to the left of every section heading.

XAML rules

Things that will cost you an afternoon if you get them wrong:

  • Avalonia 12 uses xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" — not 2009.
  • Compiled bindings are off (AvaloniaUseCompiledBindingsByDefault=false); bindings are reflection-based.
  • The Inventories list is a ListBox and must not be wrapped in a ScrollViewer — a wrapping scroll viewer gives it infinite height and defeats virtualization. On a real save that is the difference between instant and unusable.
  • TextBox placeholder text is PlaceholderText, not the obsolete Watermark.
  • Localized text uses the {m:Loc Key} / {m:LocFormat Key=…, Path=…} markup extensions, which bind ILocalizer.Current through a converter so text re-reads live on a language change. LocFormat takes a second argument through Path2 (v1.5.0). See Localization.

Platform implementations

Interface Implementation Backed by
IFilePickerService AvaloniaFilePickerService IStorageProvider
INavigationService AvaloniaNavigationService MainWindowViewModel + a modal Window
IDialogService AvaloniaDialogService MessageDialog window
IScreenReaderAnnouncer AvaloniaScreenReaderAnnouncer A hidden live-region TextBlock
IAppVersionInfo AvaloniaAppVersionInfo Assembly version
ILanguageStore, IDisclaimerGate JsonSettingsStore <ApplicationData>/PCEdit/settings.json
ISaveBackupService LocalFileSaveBackupService <LocalApplicationData>/PCEdit/backups

The backups deliberately sit under LocalApplicationData rather than the ApplicationData holding settings.json: on Windows that is the roaming profile, and megabytes of save copies have no business syncing between machines.

Converters/AppConverters.cs holds thin IValueConverter wrappers; the classification logic (Presentation/VitalStatus) stays in Core, where it is unit tested. Colour never comes from a converter that resolves a brush — that keeps the old theme's colour after a live light/dark switch (fixed in v1.7.0). Instead VitalLevelIsConverter, StatusKindIsConverter and EnumIsConverter set style classes, and Styles/Controls.axaml colours them with DynamicResource (TextBlock.vitalLow / .vitalCritical, TextBlock.status / .status.success / .status.error). When two matching styles set the same property the later one wins, so order them general first.

Accessibility

Not an afterthought, and worth preserving in any change:

  • Status messages are announced through IScreenReaderAnnouncer, errors with an error prefix.
  • Expanders carry a spoken hint describing their current state.
  • Controls that need one have an explicit accessible name (the *A11y string keys). An item's Move button names the item and its id, so identical items are distinguishable.
  • The type scale and the status palette are designed to stay legible in both themes.

Running it

dotnet run --project PCEdit.Desktop/PCEdit.Desktop.csproj

Packaging the three platform artifacts is covered in Building & Packaging.

Clone this wiki locally