Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions ConfigFiles/MBE/ui/grid_style.yaml
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
# Grid Style Configuration
# Color formats supported:
# - #RGB (e.g., #FFF)
# - #RRGGBB (e.g., #FFFFFF)
# - #ARGB (e.g., #0FFF for transparent white)
# - #AARRGGBB (e.g., #00FFFFFF for fully transparent white)
# Alpha: 00 = fully transparent, FF = fully opaque

Expand Down
2 changes: 0 additions & 2 deletions ConfigFiles/MOCVD/ui/grid_style.yaml
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
# Grid Style Configuration
# Color formats supported:
# - #RGB (e.g., #FFF)
# - #RRGGBB (e.g., #FFFFFF)
# - #ARGB (e.g., #0FFF for transparent white)
# - #AARRGGBB (e.g., #00FFFFFF for fully transparent white)
# Alpha: 00 = fully transparent, FF = fully opaque

Expand Down
4 changes: 2 additions & 2 deletions Docs/architecture/error-reporting.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ type. Its two `Result`-join sites (`LoadAsync`, `Save`) route each error through
rather than joining raw `.Message` strings, so both the validation failures and the file-I/O failures render
in the current culture. Eight typed producers back it (all in `SemiStep.Core/Configuration/`):

- **Validation** (`GridStyleValidator`): `GridStyleConfigMissingError`, `GridStyleSectionMissingError`,
- **Validation** (`GridStyleMapper`): `GridStyleConfigMissingError`, `GridStyleSectionMissingError`,
`GridStyleOrientationInvalidError`, `GridStyleKeyMissingError`, `GridStyleHexColorInvalidError`. The
orientation error carries its two expected values as properties (`ExpectedRows`/`ExpectedColumns`) rather
than referencing the `internal GridOrientationValues` constants, which `ReasonLocalizer` (in `SemiStep.UI`)
Expand All @@ -184,7 +184,7 @@ the dropped `ex.Message` is logged at each consumer, and the `ExceptionalError`

- **Editor consumer** — `GridStyleEditorViewModel.LoadAsync`/`Save` log each nested `ExceptionalError` at
`LogWarning` after setting the localized `ErrorMessage`.
- **Startup consumer** — `GridStyleLoader`/`GridStyleValidator` are also on the startup config-load path
- **Startup consumer** — `GridStyleLoader`/`GridStyleMapper` are also on the startup config-load path
(`ConfigFacade.LoadAndValidateAsync`, consumed by `Program.cs`), which logs the same nested
`ExceptionalError` so a YAML parse detail is not lost at launch.

Expand Down
42 changes: 25 additions & 17 deletions Docs/architecture/grid-style-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,37 +41,42 @@ are the deliberate exception: `GridStyleLoader.LoadAsync` reads exactly one file
unambiguous: one config-dir maps to one style file. If styles were ever split across `ui/*.yaml`,
write-back would have no single target, so single-file is intentional.

The file header documents the accepted hex formats (`#RGB`, `#RRGGBB`, `#ARGB`, `#AARRGGBB`); the
The file header documents the accepted hex formats (`#RRGGBB`, `#AARRGGBB`); the
writer preserves that header on save.

## The load pipeline

```
grid_style.yaml
→ GridStyleOptionsDto (internal DTOs, snake_case [YamlMember], one per section)
→ GridStyleValidator.Validate (hex-format check; see "Known gap" below)
→ GridStyleMapper.Map (DTO → record, applies defaults)
→ GridStyleMapper.Map (DTO → record, aggregated per-key validation, applies defaults)
→ GridStyleOptions (immutable record, SemiStep.Core, Avalonia-free)
→ AppConfiguration.GridStyle (bundled by ConfigFacade)
→ DI singleton (UiDi.AddUi: AppConfiguration.GridStyle registered as GridStyleOptions)
```

`ConfigFacade` loads the DTO, runs `GridStyleValidator`, calls `GridStyleMapper.Map`, and bundles the
resulting `GridStyleOptions` record into `AppConfiguration`. `UiDi.AddUi` registers
`ConfigFacade` loads the DTO, calls `GridStyleMapper.Map`, which validates and maps in one pass, and
bundles the resulting `GridStyleOptions` record into `AppConfiguration`. `UiDi.AddUi` registers
`AppConfiguration.GridStyle` as a `GridStyleOptions` singleton, so every consumer (the installers, the
column factories, `ColumnWidthCalculator`) injects the same typed record.

`GridStyleOptions` is an immutable record with a `Default` fallback used only by tests; the `#000000`
cell-palette placeholders in `Default` are never rendered in production.

**Interim (slice 4).** Colors are now the typed `StyleColor` value (channels `A`/`R`/`G`/`B`) end to
end in the record; hex is a string only at the two I/O edges. Validation lives inside the load mapper —
`GridStyleMapper.Map` returns a `Result<GridStyleOptions>` and accumulates every per-key error in one
pass. The standalone `GridStyleValidator` and the editor's `HexColor` helper are gone. The full doc
rewrite (and removal of the now-obsolete "Known gap" section) lands in slice 5.

## Record shape: nested per group (interim, slice 3)

`GridStyleOptions` is no longer a flat 78-field record. It is a one-level nested root that composes ~10
per-group records (`Fonts`, `Layout`, `Selection`, `ChangedCells`, `ReadOnlyCells`, `DisabledCells`,
`Execution`, `StatusBar`, `ValidationPanel`, `Chrome`) plus the root-level `Orientation`. `ReadOnlyCells`
and `DisabledCells` share one `DepthPalette` type. Consumers read short nested paths — `gridStyle.Fonts.CellFontSize`,
`gridStyle.ReadOnlyCells.Depth1`, `gridStyle.Chrome.GridLine` — instead of the old flat fields. Colors stay
`string` in this shape; typing them as `StyleColor` is slice 4.
`gridStyle.ReadOnlyCells.Depth1`, `gridStyle.Chrome.GridLine` — instead of the old flat fields. Colors were
`string` at slice 3; slice 4 typed them as `StyleColor` (see the interim note above).

**The YAML file, the DTOs, and `GridStyleValidator` are unchanged** by this slice: the file on disk is
byte-identical, the DTO layer keeps its nested snake_case shape and per-key error reporting, and the load
Expand All @@ -83,8 +88,8 @@ show a flat path (for example `GridStyleOptions.FontFamily`) now live under thei
**Note for slice 4.** The error-path key `colors.grid_line` and the record path `Chrome.GridLine`
deliberately diverge: `grid_line` is a loose field in the `colors:` DTO section (beside `grid_border` /
`grid_background`), but the one-level record folds it into `Chrome` so the root carries no lone `string`.
Slice 4 moves `GridStyleValidator`'s per-key checks into the load mapper; that mapper-resident validation
**must keep emitting the `colors.grid_line` key**, not rename it to `chrome.grid_line`. The key names the
Slice 4 moved those per-key checks into the load mapper (`GridStyleMapper.Map`); that mapper-resident
validation keeps emitting the `colors.grid_line` key, not `chrome.grid_line`. The key names the
YAML path the operator edits, not the record path.

## Resources projection
Expand Down Expand Up @@ -128,8 +133,8 @@ orientation: rows_as_steps # canonical (rows = steps); or columns_as_steps (tr
```

- **Values.** `rows_as_steps` (canonical) or `columns_as_steps` (transposed). An absent key
defaults to `rows_as_steps`, so existing files load unchanged. `GridStyleValidator` rejects
any other string with a config error naming both accepted values.
defaults to `rows_as_steps`, so existing files load unchanged. The load mapper
(`GridStyleMapper.Map`) rejects any other string with a config error naming both accepted values.
- **Typed model.** The record carries a Core enum, `GridStyleOptions.Orientation`
(`GridOrientation.RowsAsSteps | ColumnsAsSteps`), not the raw string. Parsing and
serialization go through `GridOrientationValues` (`Configuration/Dto/`); an absent DTO value
Expand Down Expand Up @@ -237,9 +242,10 @@ GridStyleEditorWindow

- **ViewModel.** Seeds a mutable draft from the loaded record (a separate copy, never the DI
singleton). Colors are exposed as `Color` for the `ColorPicker`; sizes as `decimal?` for
`NumericUpDown`. Hex↔`Color` goes through an explicit `HexColor.ToHex` / `HexColor.Parse`, not
`Color.ToString()`. `CanSave` is gated by **both** the facade's color validation **and** VM-side
numeric range checks (font/padding/row-height/spacing/panel-height bounds). The editor surfaces
`NumericUpDown`. Channel↔`Color` conversion goes through `StyleColorConversions`
(`ToMediaColor` / `ToStyleColor`, in `SemiStep.UI/Styles`), not `Color.ToString()`. `CanSave` is gated
by VM-side numeric range checks alone (font/padding/row-height/spacing/panel-height bounds); an invalid
color is unrepresentable in the typed record, so the facade no longer color-validates on save. The editor surfaces
effectively the whole record: all ~54 colors, 13 numerics, plus the font controls — one global
font-family `ComboBox`, and a per-role weight `ComboBox` (curated 300–900 list) and italic
`CheckBox`. The window groups these into two cards: **Fonts** (the family row plus a size / weight /
Expand All @@ -254,9 +260,11 @@ GridStyleEditorWindow
- **Facade.** `GridStyleEditorFacade` is the only public Core seam for the editor:
`Load(configDir)` → `Result<GridStyleOptions>`, `Validate(GridStyleOptions)` → `Result`,
`Save(configDir, GridStyleOptions)` → `Task<Result>`. `Save` is async and runs the file write off the
UI thread (mirroring `Load`); the editor's `SaveCommand` awaits it. `Validate` stays synchronous —
`RecomputeCanSave` calls it per keystroke, and it is an in-memory pass with no I/O. The loader,
validator, writer, and the ~12 DTOs stay `internal`. The UI never touches Core internals directly.
UI thread (mirroring `Load`); the editor's `SaveCommand` awaits it. `Validate` is a vacuous
`Result.Ok()` pass-through — color validation now lives in the load mapper, so it validates nothing;
`RecomputeCanSave` still calls it per keystroke but `CanSave` turns on the numeric-range checks alone.
The method stays on the interface until slice 5 trims it. The loader, writer, and the ~12 DTOs stay
`internal`. The UI never touches Core internals directly.
(Layering: the config stays in `SemiStep.Core`, settled by review; the editor reaches it only through
this facade.)
- **Writer.** `GridStyleWriter` maps the record back to the DTO (`GridStyleDtoMapper`), serializes with
Expand Down
4 changes: 2 additions & 2 deletions SemiStep/SemiStep.Core/Configuration/ChangedCellColors.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
namespace SemiStep.Core.Configuration;

public sealed record ChangedCellColors(
string Changed,
string ChangedSelected);
StyleColor Changed,
StyleColor ChangedSelected);
28 changes: 14 additions & 14 deletions SemiStep/SemiStep.Core/Configuration/ChromeColors.cs
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
namespace SemiStep.Core.Configuration;

public sealed record ChromeColors(
string Info,
string Connected,
string Disconnected,
string LocalMode,
string Connecting,
string PanelBackground,
string PanelHeaderBackground,
string SubtleBorder,
string Separator,
string SecondaryForeground,
string GridBorder,
string GridBackground,
string HeaderForeground,
string GridLine);
StyleColor Info,
StyleColor Connected,
StyleColor Disconnected,
StyleColor LocalMode,
StyleColor Connecting,
StyleColor PanelBackground,
StyleColor PanelHeaderBackground,
StyleColor SubtleBorder,
StyleColor Separator,
StyleColor SecondaryForeground,
StyleColor GridBorder,
StyleColor GridBackground,
StyleColor HeaderForeground,
StyleColor GridLine);
20 changes: 10 additions & 10 deletions SemiStep/SemiStep.Core/Configuration/DepthPalette.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
namespace SemiStep.Core.Configuration;

public sealed record DepthPalette(
string Depth0,
string Depth1,
string Depth2,
string Depth3,
string Depth0Past,
string Depth1Past,
string Depth2Past,
string Depth3Past,
string Selected,
string Foreground);
StyleColor Depth0,
StyleColor Depth1,
StyleColor Depth2,
StyleColor Depth3,
StyleColor Depth0Past,
StyleColor Depth1Past,
StyleColor Depth2Past,
StyleColor Depth3Past,
StyleColor Selected,
StyleColor Foreground);
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ internal static class GridOrientationValues
public const string RowsAsSteps = "rows_as_steps";
public const string ColumnsAsSteps = "columns_as_steps";

// GridStyleValidator rejects unknown values before this parser runs; throwing here is
// GridStyleMapper rejects unknown values before this parser runs; throwing here is
// defense-in-depth against a future path that maps an unvalidated DTO.
public static GridOrientation Parse(string? value)
{
Expand Down
18 changes: 9 additions & 9 deletions SemiStep/SemiStep.Core/Configuration/ExecutionPalette.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
namespace SemiStep.Core.Configuration;

public sealed record ExecutionPalette(
string Depth0,
string Depth1,
string Depth2,
string Depth3,
string Depth0Past,
string Depth1Past,
string Depth2Past,
string Depth3Past,
string CurrentStepMarker);
StyleColor Depth0,
StyleColor Depth1,
StyleColor Depth2,
StyleColor Depth3,
StyleColor Depth0Past,
StyleColor Depth1Past,
StyleColor Depth2Past,
StyleColor Depth3Past,
StyleColor CurrentStepMarker);
10 changes: 4 additions & 6 deletions SemiStep/SemiStep.Core/Configuration/Facade/ConfigFacade.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public static async Task<Result<AppConfiguration>> LoadAndValidateAsync(string c

var (properties, columns, groups, actions, gridStyle, connection, appUi) = loadResult.Value;

var gridStyleResult = GridStyleValidator.Validate(gridStyle);
var gridStyleResult = GridStyleMapper.Map(gridStyle);
if (gridStyleResult.IsFailed)
{
return Propagate(gridStyleResult, loadResult);
Expand All @@ -43,7 +43,7 @@ public static async Task<Result<AppConfiguration>> LoadAndValidateAsync(string c
return Propagate(defaultsResult, loadResult, xrefResult);
}

var mapResult = MapToDomain(properties, columns, groups, actions, gridStyle, connection, appUi);
var mapResult = MapToDomain(properties, columns, groups, actions, gridStyleResult.Value, connection, appUi);

if (mapResult.IsFailed)
{
Expand Down Expand Up @@ -118,7 +118,7 @@ private static Result<AppConfiguration> MapToDomain(
List<Dto.ColumnDto> columns,
Dictionary<string, Dictionary<int, string>> groups,
List<Dto.ActionDto> actions,
Dto.GridStyleOptionsDto? gridStyle,
GridStyleOptions gridStyle,
Dto.ConnectionDto? connection,
Dto.AppUiOptionsDto? appUi)
{
Expand Down Expand Up @@ -152,15 +152,13 @@ private static Result<AppConfiguration> MapToDomain(
mappedActions.Add(action.Id, action);
}

var mappedGridStyle = GridStyleMapper.Map(gridStyle);

var plcConfiguration = ConnectionMapper.Map(connection);

var appUiOptions = AppUiOptionsMapper.Map(appUi);

return Result.Ok(new AppConfiguration(
mappedProperties, mappedColumns, mappedGroups,
mappedActions, mappedGridStyle, plcConfiguration, appUiOptions));
mappedActions, gridStyle, plcConfiguration, appUiOptions));
}

private sealed record LoadedSections(
Expand Down
28 changes: 9 additions & 19 deletions SemiStep/SemiStep.Core/Configuration/GridStyleEditorFacade.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@

using SemiStep.Core.Configuration.Loaders;
using SemiStep.Core.Configuration.Mapping;
using SemiStep.Core.Configuration.Validation;

namespace SemiStep.Core.Configuration;

/// <summary>
/// The single public Core seam for the in-app style editor. <see cref="Validate"/> and
/// <see cref="Save"/> check color hex format only; numeric-range validation (font sizes, paddings,
/// row height, spacing, panel height) is the caller's responsibility — the editor view model enforces
/// those bounds before invoking <see cref="Save"/>.
/// The single public Core seam for the in-app style editor. Color hex validation now lives in
/// <see cref="GridStyleMapper"/>, which parses and validates in one pass on <see cref="Load"/>; an invalid
/// color is unrepresentable in <see cref="GridStyleOptions"/>, so <see cref="Save"/> writes without a
/// pre-write gate. Numeric-range validation (font sizes, paddings, row height, spacing, panel height) is the
/// caller's responsibility — the editor view model enforces those bounds before invoking <see cref="Save"/>.
/// </summary>
public sealed class GridStyleEditorFacade : IGridStyleEditorFacade
{
Expand All @@ -24,28 +24,18 @@ public async Task<Result<GridStyleOptions>> Load(string configDir)
return loadResult.ToResult<GridStyleOptions>();
}

var validation = GridStyleValidator.Validate(loadResult.Value);
if (validation.IsFailed)
{
return validation.ToResult<GridStyleOptions>();
}

return Result.Ok(GridStyleMapper.Map(loadResult.Value));
return GridStyleMapper.Map(loadResult.Value);
}

public Result Validate(GridStyleOptions options)
{
return GridStyleValidator.Validate(GridStyleDtoMapper.Map(options));
// Deliberate vacuous pass-through: a typed GridStyleOptions cannot hold an invalid color, so there is
// nothing to validate here. The method stays on IGridStyleEditorFacade until slice 5 trims the interface.
return Result.Ok();
}

public async Task<Result> Save(string configDir, GridStyleOptions options)
{
var validation = Validate(options);
if (validation.IsFailed)
{
return validation;
}

return await _gridStyleWriter.SaveAsync(configDir, options);
}
}
Loading