diff --git a/Docs/architecture/grid-style-configuration.md b/Docs/architecture/grid-style-configuration.md index e345fe90..179a40bc 100644 --- a/Docs/architecture/grid-style-configuration.md +++ b/Docs/architecture/grid-style-configuration.md @@ -64,6 +64,29 @@ 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. +## 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. + +**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 +mapper owns the small DTO-tree-to-group walk. `SaveThenLoad_DistinctFixture` proves the nested record still +round-trips losslessly through the unchanged file format. Property references elsewhere in this doc that still +show a flat path (for example `GridStyleOptions.FontFamily`) now live under their group +(`GridStyleOptions.Fonts.FontFamily`); those spellings are corrected in the slice-5 doc rewrite. + +**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 +YAML path the operator edits, not the record path. + ## Resources projection The typed record is projected into `Application.Resources` at startup so XAML can bind styles via diff --git a/SemiStep/SemiStep.Core/Configuration/ChangedCellColors.cs b/SemiStep/SemiStep.Core/Configuration/ChangedCellColors.cs new file mode 100644 index 00000000..dc7b4015 --- /dev/null +++ b/SemiStep/SemiStep.Core/Configuration/ChangedCellColors.cs @@ -0,0 +1,5 @@ +namespace SemiStep.Core.Configuration; + +public sealed record ChangedCellColors( + string Changed, + string ChangedSelected); diff --git a/SemiStep/SemiStep.Core/Configuration/ChromeColors.cs b/SemiStep/SemiStep.Core/Configuration/ChromeColors.cs new file mode 100644 index 00000000..d3696891 --- /dev/null +++ b/SemiStep/SemiStep.Core/Configuration/ChromeColors.cs @@ -0,0 +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); diff --git a/SemiStep/SemiStep.Core/Configuration/DepthPalette.cs b/SemiStep/SemiStep.Core/Configuration/DepthPalette.cs new file mode 100644 index 00000000..ee790a5b --- /dev/null +++ b/SemiStep/SemiStep.Core/Configuration/DepthPalette.cs @@ -0,0 +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); diff --git a/SemiStep/SemiStep.Core/Configuration/ExecutionPalette.cs b/SemiStep/SemiStep.Core/Configuration/ExecutionPalette.cs new file mode 100644 index 00000000..afd8cf69 --- /dev/null +++ b/SemiStep/SemiStep.Core/Configuration/ExecutionPalette.cs @@ -0,0 +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); diff --git a/SemiStep/SemiStep.Core/Configuration/GridStyleFonts.cs b/SemiStep/SemiStep.Core/Configuration/GridStyleFonts.cs new file mode 100644 index 00000000..cca70337 --- /dev/null +++ b/SemiStep/SemiStep.Core/Configuration/GridStyleFonts.cs @@ -0,0 +1,10 @@ +namespace SemiStep.Core.Configuration; + +public sealed record GridStyleFonts( + string FontFamily, + int HeaderFontSize, + int HeaderFontWeight, + bool HeaderItalic, + int CellFontSize, + int CellFontWeight, + bool CellItalic); diff --git a/SemiStep/SemiStep.Core/Configuration/GridStyleLayout.cs b/SemiStep/SemiStep.Core/Configuration/GridStyleLayout.cs new file mode 100644 index 00000000..16d61ad8 --- /dev/null +++ b/SemiStep/SemiStep.Core/Configuration/GridStyleLayout.cs @@ -0,0 +1,8 @@ +namespace SemiStep.Core.Configuration; + +public sealed record GridStyleLayout( + double CellPaddingLeft, + double CellPaddingTop, + double CellPaddingRight, + double CellPaddingBottom, + double RowHeight); diff --git a/SemiStep/SemiStep.Core/Configuration/GridStyleOptions.cs b/SemiStep/SemiStep.Core/Configuration/GridStyleOptions.cs index fd8d6cb8..8ed6c194 100644 --- a/SemiStep/SemiStep.Core/Configuration/GridStyleOptions.cs +++ b/SemiStep/SemiStep.Core/Configuration/GridStyleOptions.cs @@ -1,163 +1,106 @@ namespace SemiStep.Core.Configuration; public sealed record GridStyleOptions( - string FontFamily, - int HeaderFontSize, - int HeaderFontWeight, - bool HeaderItalic, - int CellFontSize, - int CellFontWeight, - bool CellItalic, - double CellPaddingLeft, - double CellPaddingTop, - double CellPaddingRight, - double CellPaddingBottom, - double RowHeight, - string SelectionBackgroundColor, - string SelectionForegroundColor, - string CellChangedColor, - string CellChangedSelectedColor, - string DisabledCellDepth0Color, - string DisabledCellDepth1Color, - string DisabledCellDepth2Color, - string DisabledCellDepth3Color, - string DisabledCellDepth0PastColor, - string DisabledCellDepth1PastColor, - string DisabledCellDepth2PastColor, - string DisabledCellDepth3PastColor, - string DisabledCellSelectedColor, - string DisabledCellForegroundColor, - string ReadOnlyCellDepth0Color, - string ReadOnlyCellDepth1Color, - string ReadOnlyCellDepth2Color, - string ReadOnlyCellDepth3Color, - string ReadOnlyCellDepth0PastColor, - string ReadOnlyCellDepth1PastColor, - string ReadOnlyCellDepth2PastColor, - string ReadOnlyCellDepth3PastColor, - string ReadOnlyCellSelectedColor, - string ReadOnlyCellForegroundColor, - string GridLineColor, - string StatusBarBackgroundColor, - string StatusBarForegroundColor, - double StatusBarPadding, - double StatusBarItemSpacing, - int StatusBarFontSize, - int StatusBarFontWeight, - bool StatusBarItalic, - int StatusBarTimerLabelFontSize, - int StatusBarTimerLabelFontWeight, - bool StatusBarTimerLabelItalic, - int StatusBarTimerValueFontSize, - int StatusBarTimerValueFontWeight, - bool StatusBarTimerValueItalic, - string ValidationPanelBackgroundColor, - string ValidationPanelForegroundColor, - string ValidationPanelErrorColor, - string ValidationPanelWarningColor, - double ValidationPanelMaxHeight, - string ExecutionDepth0Color, - string ExecutionDepth1Color, - string ExecutionDepth2Color, - string ExecutionDepth3Color, - string ExecutionDepth0PastColor, - string ExecutionDepth1PastColor, - string ExecutionDepth2PastColor, - string ExecutionDepth3PastColor, - string ExecutionCurrentStepMarkerColor, - string InfoColor, - string ConnectedColor, - string DisconnectedColor, - string LocalModeColor, - string ConnectingColor, - string PanelBackgroundColor, - string PanelHeaderBackgroundColor, - string SubtleBorderColor, - string SeparatorColor, - string SecondaryForegroundColor, - string GridBorderColor, - string GridBackgroundColor, - string HeaderForegroundColor, + GridStyleFonts Fonts, + GridStyleLayout Layout, + SelectionColors Selection, + ChangedCellColors ChangedCells, + DepthPalette ReadOnlyCells, + DepthPalette DisabledCells, + ExecutionPalette Execution, + StatusBarStyle StatusBar, + ValidationPanelStyle ValidationPanel, + ChromeColors Chrome, GridOrientation Orientation) { // Test-only fallback; the #000000 cell palettes are placeholders, never rendered in production. public static GridStyleOptions Default { get; } = new( - FontFamily: "", - HeaderFontSize: 14, - HeaderFontWeight: 700, - HeaderItalic: false, - CellFontSize: 12, - CellFontWeight: 400, - CellItalic: false, - CellPaddingLeft: 6, - CellPaddingTop: 4, - CellPaddingRight: 6, - CellPaddingBottom: 4, - RowHeight: 28, - SelectionBackgroundColor: "#CCE4F7", - SelectionForegroundColor: "#202020", - CellChangedColor: "#FFCC80", - CellChangedSelectedColor: "#EFD3A4", - DisabledCellDepth0Color: "#000000", - DisabledCellDepth1Color: "#000000", - DisabledCellDepth2Color: "#000000", - DisabledCellDepth3Color: "#000000", - DisabledCellDepth0PastColor: "#000000", - DisabledCellDepth1PastColor: "#000000", - DisabledCellDepth2PastColor: "#000000", - DisabledCellDepth3PastColor: "#000000", - DisabledCellSelectedColor: "#000000", - DisabledCellForegroundColor: "#000000", - ReadOnlyCellDepth0Color: "#000000", - ReadOnlyCellDepth1Color: "#000000", - ReadOnlyCellDepth2Color: "#000000", - ReadOnlyCellDepth3Color: "#000000", - ReadOnlyCellDepth0PastColor: "#000000", - ReadOnlyCellDepth1PastColor: "#000000", - ReadOnlyCellDepth2PastColor: "#000000", - ReadOnlyCellDepth3PastColor: "#000000", - ReadOnlyCellSelectedColor: "#000000", - ReadOnlyCellForegroundColor: "#000000", - GridLineColor: "#E0E0E0", - StatusBarBackgroundColor: "#F0F0F0", - StatusBarForegroundColor: "#000000", - StatusBarPadding: 5, - StatusBarItemSpacing: 10, - StatusBarFontSize: 12, - StatusBarFontWeight: 400, - StatusBarItalic: false, - StatusBarTimerLabelFontSize: 14, - StatusBarTimerLabelFontWeight: 400, - StatusBarTimerLabelItalic: false, - StatusBarTimerValueFontSize: 24, - StatusBarTimerValueFontWeight: 400, - StatusBarTimerValueItalic: false, - ValidationPanelBackgroundColor: "#FFFFFF", - ValidationPanelForegroundColor: "#000000", - ValidationPanelErrorColor: "#D32F2F", - ValidationPanelWarningColor: "#F57C00", - ValidationPanelMaxHeight: 100, - ExecutionDepth0Color: "#000000", - ExecutionDepth1Color: "#000000", - ExecutionDepth2Color: "#000000", - ExecutionDepth3Color: "#000000", - ExecutionDepth0PastColor: "#000000", - ExecutionDepth1PastColor: "#000000", - ExecutionDepth2PastColor: "#000000", - ExecutionDepth3PastColor: "#000000", - ExecutionCurrentStepMarkerColor: "#000000", - InfoColor: "#1976D2", - ConnectedColor: "#44BB44", - DisconnectedColor: "#FF4444", - LocalModeColor: "#6C707E", - ConnectingColor: "#FFAF0F", - PanelBackgroundColor: "#F8F8F8", - PanelHeaderBackgroundColor: "#EEEEEE", - SubtleBorderColor: "#D0D0D0", - SeparatorColor: "#C0C0C0", - SecondaryForegroundColor: "#888888", - GridBorderColor: "#808080", - GridBackgroundColor: "#FFFFFF", - HeaderForegroundColor: "#000000", + Fonts: new GridStyleFonts( + FontFamily: "", + HeaderFontSize: 14, + HeaderFontWeight: 700, + HeaderItalic: false, + CellFontSize: 12, + CellFontWeight: 400, + CellItalic: false), + Layout: new GridStyleLayout( + CellPaddingLeft: 6, + CellPaddingTop: 4, + CellPaddingRight: 6, + CellPaddingBottom: 4, + RowHeight: 28), + Selection: new SelectionColors( + Background: "#CCE4F7", + Foreground: "#202020"), + ChangedCells: new ChangedCellColors( + Changed: "#FFCC80", + ChangedSelected: "#EFD3A4"), + ReadOnlyCells: new DepthPalette( + Depth0: "#000000", + Depth1: "#000000", + Depth2: "#000000", + Depth3: "#000000", + Depth0Past: "#000000", + Depth1Past: "#000000", + Depth2Past: "#000000", + Depth3Past: "#000000", + Selected: "#000000", + Foreground: "#000000"), + DisabledCells: new DepthPalette( + Depth0: "#000000", + Depth1: "#000000", + Depth2: "#000000", + Depth3: "#000000", + Depth0Past: "#000000", + Depth1Past: "#000000", + Depth2Past: "#000000", + Depth3Past: "#000000", + Selected: "#000000", + Foreground: "#000000"), + Execution: new ExecutionPalette( + Depth0: "#000000", + Depth1: "#000000", + Depth2: "#000000", + Depth3: "#000000", + Depth0Past: "#000000", + Depth1Past: "#000000", + Depth2Past: "#000000", + Depth3Past: "#000000", + CurrentStepMarker: "#000000"), + StatusBar: new StatusBarStyle( + Background: "#F0F0F0", + Foreground: "#000000", + Padding: 5, + ItemSpacing: 10, + FontSize: 12, + Weight: 400, + Italic: false, + TimerLabelFontSize: 14, + TimerLabelWeight: 400, + TimerLabelItalic: false, + TimerValueFontSize: 24, + TimerValueWeight: 400, + TimerValueItalic: false), + ValidationPanel: new ValidationPanelStyle( + Background: "#FFFFFF", + Foreground: "#000000", + ErrorColor: "#D32F2F", + WarningColor: "#F57C00", + MaxHeight: 100), + Chrome: new ChromeColors( + Info: "#1976D2", + Connected: "#44BB44", + Disconnected: "#FF4444", + LocalMode: "#6C707E", + Connecting: "#FFAF0F", + PanelBackground: "#F8F8F8", + PanelHeaderBackground: "#EEEEEE", + SubtleBorder: "#D0D0D0", + Separator: "#C0C0C0", + SecondaryForeground: "#888888", + GridBorder: "#808080", + GridBackground: "#FFFFFF", + HeaderForeground: "#000000", + GridLine: "#E0E0E0"), Orientation: GridOrientation.RowsAsSteps); } diff --git a/SemiStep/SemiStep.Core/Configuration/Mapping/GridStyleDtoMapper.cs b/SemiStep/SemiStep.Core/Configuration/Mapping/GridStyleDtoMapper.cs index 398e7c8b..3632a5e9 100644 --- a/SemiStep/SemiStep.Core/Configuration/Mapping/GridStyleDtoMapper.cs +++ b/SemiStep/SemiStep.Core/Configuration/Mapping/GridStyleDtoMapper.cs @@ -10,113 +10,113 @@ public static GridStyleOptionsDto Map(GridStyleOptions options) { Fonts = new GridStyleFontsDto { - Family = options.FontFamily, - HeaderSize = options.HeaderFontSize, - HeaderWeight = options.HeaderFontWeight, - HeaderItalic = options.HeaderItalic, - CellSize = options.CellFontSize, - CellWeight = options.CellFontWeight, - CellItalic = options.CellItalic + Family = options.Fonts.FontFamily, + HeaderSize = options.Fonts.HeaderFontSize, + HeaderWeight = options.Fonts.HeaderFontWeight, + HeaderItalic = options.Fonts.HeaderItalic, + CellSize = options.Fonts.CellFontSize, + CellWeight = options.Fonts.CellFontWeight, + CellItalic = options.Fonts.CellItalic }, Layout = new GridStyleLayoutDto { - CellPaddingLeft = options.CellPaddingLeft, - CellPaddingTop = options.CellPaddingTop, - CellPaddingRight = options.CellPaddingRight, - CellPaddingBottom = options.CellPaddingBottom, - RowHeight = options.RowHeight + CellPaddingLeft = options.Layout.CellPaddingLeft, + CellPaddingTop = options.Layout.CellPaddingTop, + CellPaddingRight = options.Layout.CellPaddingRight, + CellPaddingBottom = options.Layout.CellPaddingBottom, + RowHeight = options.Layout.RowHeight }, Colors = new GridStyleColorsDto { Selection = new GridStyleSelectionColorsDto { - Background = options.SelectionBackgroundColor, - Foreground = options.SelectionForegroundColor + Background = options.Selection.Background, + Foreground = options.Selection.Foreground }, Cells = new GridStyleCellColorsDto { - Changed = options.CellChangedColor, - ChangedSelected = options.CellChangedSelectedColor, + Changed = options.ChangedCells.Changed, + ChangedSelected = options.ChangedCells.ChangedSelected, ReadOnly = new GridStyleReadOnlyCellColorsDto { - Depth0 = options.ReadOnlyCellDepth0Color, - Depth1 = options.ReadOnlyCellDepth1Color, - Depth2 = options.ReadOnlyCellDepth2Color, - Depth3 = options.ReadOnlyCellDepth3Color, - Depth0Past = options.ReadOnlyCellDepth0PastColor, - Depth1Past = options.ReadOnlyCellDepth1PastColor, - Depth2Past = options.ReadOnlyCellDepth2PastColor, - Depth3Past = options.ReadOnlyCellDepth3PastColor, - Selected = options.ReadOnlyCellSelectedColor, - Foreground = options.ReadOnlyCellForegroundColor + Depth0 = options.ReadOnlyCells.Depth0, + Depth1 = options.ReadOnlyCells.Depth1, + Depth2 = options.ReadOnlyCells.Depth2, + Depth3 = options.ReadOnlyCells.Depth3, + Depth0Past = options.ReadOnlyCells.Depth0Past, + Depth1Past = options.ReadOnlyCells.Depth1Past, + Depth2Past = options.ReadOnlyCells.Depth2Past, + Depth3Past = options.ReadOnlyCells.Depth3Past, + Selected = options.ReadOnlyCells.Selected, + Foreground = options.ReadOnlyCells.Foreground }, Disabled = new GridStyleDisabledCellColorsDto { - Depth0 = options.DisabledCellDepth0Color, - Depth1 = options.DisabledCellDepth1Color, - Depth2 = options.DisabledCellDepth2Color, - Depth3 = options.DisabledCellDepth3Color, - Depth0Past = options.DisabledCellDepth0PastColor, - Depth1Past = options.DisabledCellDepth1PastColor, - Depth2Past = options.DisabledCellDepth2PastColor, - Depth3Past = options.DisabledCellDepth3PastColor, - Selected = options.DisabledCellSelectedColor, - Foreground = options.DisabledCellForegroundColor + Depth0 = options.DisabledCells.Depth0, + Depth1 = options.DisabledCells.Depth1, + Depth2 = options.DisabledCells.Depth2, + Depth3 = options.DisabledCells.Depth3, + Depth0Past = options.DisabledCells.Depth0Past, + Depth1Past = options.DisabledCells.Depth1Past, + Depth2Past = options.DisabledCells.Depth2Past, + Depth3Past = options.DisabledCells.Depth3Past, + Selected = options.DisabledCells.Selected, + Foreground = options.DisabledCells.Foreground }, Execution = new GridStyleExecutionColorsDto { - Depth0 = options.ExecutionDepth0Color, - Depth1 = options.ExecutionDepth1Color, - Depth2 = options.ExecutionDepth2Color, - Depth3 = options.ExecutionDepth3Color, - Depth0Past = options.ExecutionDepth0PastColor, - Depth1Past = options.ExecutionDepth1PastColor, - Depth2Past = options.ExecutionDepth2PastColor, - Depth3Past = options.ExecutionDepth3PastColor, - CurrentStepMarker = options.ExecutionCurrentStepMarkerColor + Depth0 = options.Execution.Depth0, + Depth1 = options.Execution.Depth1, + Depth2 = options.Execution.Depth2, + Depth3 = options.Execution.Depth3, + Depth0Past = options.Execution.Depth0Past, + Depth1Past = options.Execution.Depth1Past, + Depth2Past = options.Execution.Depth2Past, + Depth3Past = options.Execution.Depth3Past, + CurrentStepMarker = options.Execution.CurrentStepMarker } }, - GridLine = options.GridLineColor + GridLine = options.Chrome.GridLine }, StatusBar = new StatusBarStyleDto { - Background = options.StatusBarBackgroundColor, - Foreground = options.StatusBarForegroundColor, - Padding = options.StatusBarPadding, - ItemSpacing = options.StatusBarItemSpacing, - FontSize = options.StatusBarFontSize, - Weight = options.StatusBarFontWeight, - Italic = options.StatusBarItalic, - TimerLabelFontSize = options.StatusBarTimerLabelFontSize, - TimerLabelWeight = options.StatusBarTimerLabelFontWeight, - TimerLabelItalic = options.StatusBarTimerLabelItalic, - TimerValueFontSize = options.StatusBarTimerValueFontSize, - TimerValueWeight = options.StatusBarTimerValueFontWeight, - TimerValueItalic = options.StatusBarTimerValueItalic + Background = options.StatusBar.Background, + Foreground = options.StatusBar.Foreground, + Padding = options.StatusBar.Padding, + ItemSpacing = options.StatusBar.ItemSpacing, + FontSize = options.StatusBar.FontSize, + Weight = options.StatusBar.Weight, + Italic = options.StatusBar.Italic, + TimerLabelFontSize = options.StatusBar.TimerLabelFontSize, + TimerLabelWeight = options.StatusBar.TimerLabelWeight, + TimerLabelItalic = options.StatusBar.TimerLabelItalic, + TimerValueFontSize = options.StatusBar.TimerValueFontSize, + TimerValueWeight = options.StatusBar.TimerValueWeight, + TimerValueItalic = options.StatusBar.TimerValueItalic }, ValidationPanel = new ValidationPanelStyleDto { - Background = options.ValidationPanelBackgroundColor, - Foreground = options.ValidationPanelForegroundColor, - ErrorColor = options.ValidationPanelErrorColor, - WarningColor = options.ValidationPanelWarningColor, - MaxHeight = options.ValidationPanelMaxHeight + Background = options.ValidationPanel.Background, + Foreground = options.ValidationPanel.Foreground, + ErrorColor = options.ValidationPanel.ErrorColor, + WarningColor = options.ValidationPanel.WarningColor, + MaxHeight = options.ValidationPanel.MaxHeight }, Chrome = new GridStyleChromeColorsDto { - Info = options.InfoColor, - Connected = options.ConnectedColor, - Disconnected = options.DisconnectedColor, - LocalMode = options.LocalModeColor, - Connecting = options.ConnectingColor, - PanelBackground = options.PanelBackgroundColor, - PanelHeaderBackground = options.PanelHeaderBackgroundColor, - SubtleBorder = options.SubtleBorderColor, - Separator = options.SeparatorColor, - SecondaryForeground = options.SecondaryForegroundColor, - GridBorder = options.GridBorderColor, - GridBackground = options.GridBackgroundColor, - HeaderForeground = options.HeaderForegroundColor + Info = options.Chrome.Info, + Connected = options.Chrome.Connected, + Disconnected = options.Chrome.Disconnected, + LocalMode = options.Chrome.LocalMode, + Connecting = options.Chrome.Connecting, + PanelBackground = options.Chrome.PanelBackground, + PanelHeaderBackground = options.Chrome.PanelHeaderBackground, + SubtleBorder = options.Chrome.SubtleBorder, + Separator = options.Chrome.Separator, + SecondaryForeground = options.Chrome.SecondaryForeground, + GridBorder = options.Chrome.GridBorder, + GridBackground = options.Chrome.GridBackground, + HeaderForeground = options.Chrome.HeaderForeground }, Orientation = GridOrientationValues.Serialize(options.Orientation) }; diff --git a/SemiStep/SemiStep.Core/Configuration/Mapping/GridStyleMapper.cs b/SemiStep/SemiStep.Core/Configuration/Mapping/GridStyleMapper.cs index 14d664b3..469a6b5d 100644 --- a/SemiStep/SemiStep.Core/Configuration/Mapping/GridStyleMapper.cs +++ b/SemiStep/SemiStep.Core/Configuration/Mapping/GridStyleMapper.cs @@ -18,83 +18,93 @@ public static GridStyleOptions Map(GridStyleOptionsDto? dto) var executionCells = dto.Colors!.Cells!.Execution!; return new GridStyleOptions( - FontFamily: dto.Fonts?.Family ?? defaults.FontFamily, - HeaderFontSize: dto.Fonts?.HeaderSize ?? defaults.HeaderFontSize, - HeaderFontWeight: dto.Fonts?.HeaderWeight ?? defaults.HeaderFontWeight, - HeaderItalic: dto.Fonts?.HeaderItalic ?? defaults.HeaderItalic, - CellFontSize: dto.Fonts?.CellSize ?? defaults.CellFontSize, - CellFontWeight: dto.Fonts?.CellWeight ?? defaults.CellFontWeight, - CellItalic: dto.Fonts?.CellItalic ?? defaults.CellItalic, - CellPaddingLeft: dto.Layout?.CellPaddingLeft ?? defaults.CellPaddingLeft, - CellPaddingTop: dto.Layout?.CellPaddingTop ?? defaults.CellPaddingTop, - CellPaddingRight: dto.Layout?.CellPaddingRight ?? defaults.CellPaddingRight, - CellPaddingBottom: dto.Layout?.CellPaddingBottom ?? defaults.CellPaddingBottom, - RowHeight: dto.Layout?.RowHeight ?? defaults.RowHeight, - SelectionBackgroundColor: dto.Colors?.Selection?.Background ?? defaults.SelectionBackgroundColor, - SelectionForegroundColor: dto.Colors?.Selection?.Foreground ?? defaults.SelectionForegroundColor, - CellChangedColor: dto.Colors?.Cells?.Changed ?? defaults.CellChangedColor, - CellChangedSelectedColor: dto.Colors?.Cells?.ChangedSelected ?? defaults.CellChangedSelectedColor, - DisabledCellDepth0Color: disabledCells.Depth0!, - DisabledCellDepth1Color: disabledCells.Depth1!, - DisabledCellDepth2Color: disabledCells.Depth2!, - DisabledCellDepth3Color: disabledCells.Depth3!, - DisabledCellDepth0PastColor: disabledCells.Depth0Past!, - DisabledCellDepth1PastColor: disabledCells.Depth1Past!, - DisabledCellDepth2PastColor: disabledCells.Depth2Past!, - DisabledCellDepth3PastColor: disabledCells.Depth3Past!, - DisabledCellSelectedColor: disabledCells.Selected!, - DisabledCellForegroundColor: disabledCells.Foreground!, - ReadOnlyCellDepth0Color: readOnlyCells.Depth0!, - ReadOnlyCellDepth1Color: readOnlyCells.Depth1!, - ReadOnlyCellDepth2Color: readOnlyCells.Depth2!, - ReadOnlyCellDepth3Color: readOnlyCells.Depth3!, - ReadOnlyCellDepth0PastColor: readOnlyCells.Depth0Past!, - ReadOnlyCellDepth1PastColor: readOnlyCells.Depth1Past!, - ReadOnlyCellDepth2PastColor: readOnlyCells.Depth2Past!, - ReadOnlyCellDepth3PastColor: readOnlyCells.Depth3Past!, - ReadOnlyCellSelectedColor: readOnlyCells.Selected!, - ReadOnlyCellForegroundColor: readOnlyCells.Foreground!, - GridLineColor: dto.Colors?.GridLine ?? defaults.GridLineColor, - StatusBarBackgroundColor: dto.StatusBar?.Background ?? defaults.StatusBarBackgroundColor, - StatusBarForegroundColor: dto.StatusBar?.Foreground ?? defaults.StatusBarForegroundColor, - StatusBarPadding: dto.StatusBar?.Padding ?? defaults.StatusBarPadding, - StatusBarItemSpacing: dto.StatusBar?.ItemSpacing ?? defaults.StatusBarItemSpacing, - StatusBarFontSize: dto.StatusBar?.FontSize ?? defaults.StatusBarFontSize, - StatusBarFontWeight: dto.StatusBar?.Weight ?? defaults.StatusBarFontWeight, - StatusBarItalic: dto.StatusBar?.Italic ?? defaults.StatusBarItalic, - StatusBarTimerLabelFontSize: dto.StatusBar?.TimerLabelFontSize ?? defaults.StatusBarTimerLabelFontSize, - StatusBarTimerLabelFontWeight: dto.StatusBar?.TimerLabelWeight ?? defaults.StatusBarTimerLabelFontWeight, - StatusBarTimerLabelItalic: dto.StatusBar?.TimerLabelItalic ?? defaults.StatusBarTimerLabelItalic, - StatusBarTimerValueFontSize: dto.StatusBar?.TimerValueFontSize ?? defaults.StatusBarTimerValueFontSize, - StatusBarTimerValueFontWeight: dto.StatusBar?.TimerValueWeight ?? defaults.StatusBarTimerValueFontWeight, - StatusBarTimerValueItalic: dto.StatusBar?.TimerValueItalic ?? defaults.StatusBarTimerValueItalic, - ValidationPanelBackgroundColor: dto.ValidationPanel?.Background ?? defaults.ValidationPanelBackgroundColor, - ValidationPanelForegroundColor: dto.ValidationPanel?.Foreground ?? defaults.ValidationPanelForegroundColor, - ValidationPanelErrorColor: dto.ValidationPanel?.ErrorColor ?? defaults.ValidationPanelErrorColor, - ValidationPanelWarningColor: dto.ValidationPanel?.WarningColor ?? defaults.ValidationPanelWarningColor, - ValidationPanelMaxHeight: dto.ValidationPanel?.MaxHeight ?? defaults.ValidationPanelMaxHeight, - ExecutionDepth0Color: executionCells.Depth0!, - ExecutionDepth1Color: executionCells.Depth1!, - ExecutionDepth2Color: executionCells.Depth2!, - ExecutionDepth3Color: executionCells.Depth3!, - ExecutionDepth0PastColor: executionCells.Depth0Past!, - ExecutionDepth1PastColor: executionCells.Depth1Past!, - ExecutionDepth2PastColor: executionCells.Depth2Past!, - ExecutionDepth3PastColor: executionCells.Depth3Past!, - ExecutionCurrentStepMarkerColor: executionCells.CurrentStepMarker!, - InfoColor: dto.Chrome?.Info ?? defaults.InfoColor, - ConnectedColor: dto.Chrome?.Connected ?? defaults.ConnectedColor, - DisconnectedColor: dto.Chrome?.Disconnected ?? defaults.DisconnectedColor, - LocalModeColor: dto.Chrome?.LocalMode ?? defaults.LocalModeColor, - ConnectingColor: dto.Chrome?.Connecting ?? defaults.ConnectingColor, - PanelBackgroundColor: dto.Chrome?.PanelBackground ?? defaults.PanelBackgroundColor, - PanelHeaderBackgroundColor: dto.Chrome?.PanelHeaderBackground ?? defaults.PanelHeaderBackgroundColor, - SubtleBorderColor: dto.Chrome?.SubtleBorder ?? defaults.SubtleBorderColor, - SeparatorColor: dto.Chrome?.Separator ?? defaults.SeparatorColor, - SecondaryForegroundColor: dto.Chrome?.SecondaryForeground ?? defaults.SecondaryForegroundColor, - GridBorderColor: dto.Chrome?.GridBorder ?? defaults.GridBorderColor, - GridBackgroundColor: dto.Chrome?.GridBackground ?? defaults.GridBackgroundColor, - HeaderForegroundColor: dto.Chrome?.HeaderForeground ?? defaults.HeaderForegroundColor, + Fonts: new GridStyleFonts( + FontFamily: dto.Fonts?.Family ?? defaults.Fonts.FontFamily, + HeaderFontSize: dto.Fonts?.HeaderSize ?? defaults.Fonts.HeaderFontSize, + HeaderFontWeight: dto.Fonts?.HeaderWeight ?? defaults.Fonts.HeaderFontWeight, + HeaderItalic: dto.Fonts?.HeaderItalic ?? defaults.Fonts.HeaderItalic, + CellFontSize: dto.Fonts?.CellSize ?? defaults.Fonts.CellFontSize, + CellFontWeight: dto.Fonts?.CellWeight ?? defaults.Fonts.CellFontWeight, + CellItalic: dto.Fonts?.CellItalic ?? defaults.Fonts.CellItalic), + Layout: new GridStyleLayout( + CellPaddingLeft: dto.Layout?.CellPaddingLeft ?? defaults.Layout.CellPaddingLeft, + CellPaddingTop: dto.Layout?.CellPaddingTop ?? defaults.Layout.CellPaddingTop, + CellPaddingRight: dto.Layout?.CellPaddingRight ?? defaults.Layout.CellPaddingRight, + CellPaddingBottom: dto.Layout?.CellPaddingBottom ?? defaults.Layout.CellPaddingBottom, + RowHeight: dto.Layout?.RowHeight ?? defaults.Layout.RowHeight), + Selection: new SelectionColors( + Background: dto.Colors?.Selection?.Background ?? defaults.Selection.Background, + Foreground: dto.Colors?.Selection?.Foreground ?? defaults.Selection.Foreground), + ChangedCells: new ChangedCellColors( + Changed: dto.Colors?.Cells?.Changed ?? defaults.ChangedCells.Changed, + ChangedSelected: dto.Colors?.Cells?.ChangedSelected ?? defaults.ChangedCells.ChangedSelected), + ReadOnlyCells: new DepthPalette( + Depth0: readOnlyCells.Depth0!, + Depth1: readOnlyCells.Depth1!, + Depth2: readOnlyCells.Depth2!, + Depth3: readOnlyCells.Depth3!, + Depth0Past: readOnlyCells.Depth0Past!, + Depth1Past: readOnlyCells.Depth1Past!, + Depth2Past: readOnlyCells.Depth2Past!, + Depth3Past: readOnlyCells.Depth3Past!, + Selected: readOnlyCells.Selected!, + Foreground: readOnlyCells.Foreground!), + DisabledCells: new DepthPalette( + Depth0: disabledCells.Depth0!, + Depth1: disabledCells.Depth1!, + Depth2: disabledCells.Depth2!, + Depth3: disabledCells.Depth3!, + Depth0Past: disabledCells.Depth0Past!, + Depth1Past: disabledCells.Depth1Past!, + Depth2Past: disabledCells.Depth2Past!, + Depth3Past: disabledCells.Depth3Past!, + Selected: disabledCells.Selected!, + Foreground: disabledCells.Foreground!), + Execution: new ExecutionPalette( + Depth0: executionCells.Depth0!, + Depth1: executionCells.Depth1!, + Depth2: executionCells.Depth2!, + Depth3: executionCells.Depth3!, + Depth0Past: executionCells.Depth0Past!, + Depth1Past: executionCells.Depth1Past!, + Depth2Past: executionCells.Depth2Past!, + Depth3Past: executionCells.Depth3Past!, + CurrentStepMarker: executionCells.CurrentStepMarker!), + StatusBar: new StatusBarStyle( + Background: dto.StatusBar?.Background ?? defaults.StatusBar.Background, + Foreground: dto.StatusBar?.Foreground ?? defaults.StatusBar.Foreground, + Padding: dto.StatusBar?.Padding ?? defaults.StatusBar.Padding, + ItemSpacing: dto.StatusBar?.ItemSpacing ?? defaults.StatusBar.ItemSpacing, + FontSize: dto.StatusBar?.FontSize ?? defaults.StatusBar.FontSize, + Weight: dto.StatusBar?.Weight ?? defaults.StatusBar.Weight, + Italic: dto.StatusBar?.Italic ?? defaults.StatusBar.Italic, + TimerLabelFontSize: dto.StatusBar?.TimerLabelFontSize ?? defaults.StatusBar.TimerLabelFontSize, + TimerLabelWeight: dto.StatusBar?.TimerLabelWeight ?? defaults.StatusBar.TimerLabelWeight, + TimerLabelItalic: dto.StatusBar?.TimerLabelItalic ?? defaults.StatusBar.TimerLabelItalic, + TimerValueFontSize: dto.StatusBar?.TimerValueFontSize ?? defaults.StatusBar.TimerValueFontSize, + TimerValueWeight: dto.StatusBar?.TimerValueWeight ?? defaults.StatusBar.TimerValueWeight, + TimerValueItalic: dto.StatusBar?.TimerValueItalic ?? defaults.StatusBar.TimerValueItalic), + ValidationPanel: new ValidationPanelStyle( + Background: dto.ValidationPanel?.Background ?? defaults.ValidationPanel.Background, + Foreground: dto.ValidationPanel?.Foreground ?? defaults.ValidationPanel.Foreground, + ErrorColor: dto.ValidationPanel?.ErrorColor ?? defaults.ValidationPanel.ErrorColor, + WarningColor: dto.ValidationPanel?.WarningColor ?? defaults.ValidationPanel.WarningColor, + MaxHeight: dto.ValidationPanel?.MaxHeight ?? defaults.ValidationPanel.MaxHeight), + Chrome: new ChromeColors( + Info: dto.Chrome?.Info ?? defaults.Chrome.Info, + Connected: dto.Chrome?.Connected ?? defaults.Chrome.Connected, + Disconnected: dto.Chrome?.Disconnected ?? defaults.Chrome.Disconnected, + LocalMode: dto.Chrome?.LocalMode ?? defaults.Chrome.LocalMode, + Connecting: dto.Chrome?.Connecting ?? defaults.Chrome.Connecting, + PanelBackground: dto.Chrome?.PanelBackground ?? defaults.Chrome.PanelBackground, + PanelHeaderBackground: dto.Chrome?.PanelHeaderBackground ?? defaults.Chrome.PanelHeaderBackground, + SubtleBorder: dto.Chrome?.SubtleBorder ?? defaults.Chrome.SubtleBorder, + Separator: dto.Chrome?.Separator ?? defaults.Chrome.Separator, + SecondaryForeground: dto.Chrome?.SecondaryForeground ?? defaults.Chrome.SecondaryForeground, + GridBorder: dto.Chrome?.GridBorder ?? defaults.Chrome.GridBorder, + GridBackground: dto.Chrome?.GridBackground ?? defaults.Chrome.GridBackground, + HeaderForeground: dto.Chrome?.HeaderForeground ?? defaults.Chrome.HeaderForeground, + GridLine: dto.Colors?.GridLine ?? defaults.Chrome.GridLine), Orientation: GridOrientationValues.Parse(dto.Orientation)); } } diff --git a/SemiStep/SemiStep.Core/Configuration/SelectionColors.cs b/SemiStep/SemiStep.Core/Configuration/SelectionColors.cs new file mode 100644 index 00000000..087a5de5 --- /dev/null +++ b/SemiStep/SemiStep.Core/Configuration/SelectionColors.cs @@ -0,0 +1,5 @@ +namespace SemiStep.Core.Configuration; + +public sealed record SelectionColors( + string Background, + string Foreground); diff --git a/SemiStep/SemiStep.Core/Configuration/StatusBarStyle.cs b/SemiStep/SemiStep.Core/Configuration/StatusBarStyle.cs new file mode 100644 index 00000000..a4fc107d --- /dev/null +++ b/SemiStep/SemiStep.Core/Configuration/StatusBarStyle.cs @@ -0,0 +1,16 @@ +namespace SemiStep.Core.Configuration; + +public sealed record StatusBarStyle( + string Background, + string Foreground, + double Padding, + double ItemSpacing, + int FontSize, + int Weight, + bool Italic, + int TimerLabelFontSize, + int TimerLabelWeight, + bool TimerLabelItalic, + int TimerValueFontSize, + int TimerValueWeight, + bool TimerValueItalic); diff --git a/SemiStep/SemiStep.Core/Configuration/ValidationPanelStyle.cs b/SemiStep/SemiStep.Core/Configuration/ValidationPanelStyle.cs new file mode 100644 index 00000000..eb827430 --- /dev/null +++ b/SemiStep/SemiStep.Core/Configuration/ValidationPanelStyle.cs @@ -0,0 +1,8 @@ +namespace SemiStep.Core.Configuration; + +public sealed record ValidationPanelStyle( + string Background, + string Foreground, + string ErrorColor, + string WarningColor, + double MaxHeight); diff --git a/SemiStep/SemiStep.Tests/Core/Configuration/ConfigFacadeGridStyleValidationTests.cs b/SemiStep/SemiStep.Tests/Core/Configuration/ConfigFacadeGridStyleValidationTests.cs index 95cd9450..18b433be 100644 --- a/SemiStep/SemiStep.Tests/Core/Configuration/ConfigFacadeGridStyleValidationTests.cs +++ b/SemiStep/SemiStep.Tests/Core/Configuration/ConfigFacadeGridStyleValidationTests.cs @@ -20,8 +20,8 @@ public async Task LoadAndValidateAsync_StandardFixture_LoadsSuccessfully() var result = await ConfigFacade.LoadAndValidateAsync(tempDir.Path); result.IsSuccess.Should().BeTrue(); - result.Value.GridStyle.ExecutionDepth0Color.Should().StartWith("#"); - result.Value.GridStyle.ExecutionCurrentStepMarkerColor.Should().StartWith("#"); + result.Value.GridStyle.Execution.Depth0.Should().StartWith("#"); + result.Value.GridStyle.Execution.CurrentStepMarker.Should().StartWith("#"); } [Fact] diff --git a/SemiStep/SemiStep.Tests/Core/Configuration/GridStyleEditorFacadeTests.cs b/SemiStep/SemiStep.Tests/Core/Configuration/GridStyleEditorFacadeTests.cs index 82190b4e..ed25be8b 100644 --- a/SemiStep/SemiStep.Tests/Core/Configuration/GridStyleEditorFacadeTests.cs +++ b/SemiStep/SemiStep.Tests/Core/Configuration/GridStyleEditorFacadeTests.cs @@ -27,7 +27,7 @@ public async Task Load_ShippedConfig_ReturnsPopulatedRecord(string equipment) var result = await new GridStyleEditorFacade().Load(tempDir.Path); result.IsSuccess.Should().BeTrue(); - result.Value.SelectionBackgroundColor.Should().NotBeNullOrWhiteSpace(); + result.Value.Selection.Background.Should().NotBeNullOrWhiteSpace(); } [Fact] @@ -82,7 +82,8 @@ public async Task Validate_ShippedRecord_ReturnsOk() [Fact] public void Validate_MalformedColor_ReturnsFail() { - var invalid = GridStyleOptions.Default with { ExecutionDepth0Color = "not-a-color" }; + var defaults = GridStyleOptions.Default; + var invalid = defaults with { Execution = defaults.Execution with { Depth0 = "not-a-color" } }; new GridStyleEditorFacade().Validate(invalid).IsFailed.Should().BeTrue(); } @@ -97,9 +98,10 @@ public async Task Save_InvalidRecord_FailsValidationGuardBeforeWriting() var token = TestContext.Current.CancellationToken; var before = await File.ReadAllTextAsync(filePath, token); - var invalid = (await facade.Load(tempDir.Path)).Value + var loadedForInvalid = (await facade.Load(tempDir.Path)).Value; + var invalid = loadedForInvalid with - { ExecutionDepth0Color = "not-a-color" }; + { Execution = loadedForInvalid.Execution with { Depth0 = "not-a-color" } }; (await facade.Save(tempDir.Path, invalid)).IsFailed.Should().BeTrue(); diff --git a/SemiStep/SemiStep.Tests/Core/Configuration/GridStyleMapperTests.cs b/SemiStep/SemiStep.Tests/Core/Configuration/GridStyleMapperTests.cs index 8cc7ebcc..31274290 100644 --- a/SemiStep/SemiStep.Tests/Core/Configuration/GridStyleMapperTests.cs +++ b/SemiStep/SemiStep.Tests/Core/Configuration/GridStyleMapperTests.cs @@ -20,42 +20,42 @@ public void Map_WiresEveryCellPaletteFieldFromDto() var options = GridStyleMapper.Map(dto); - options.SelectionBackgroundColor.Should().Be("#ABCDEF"); - options.SelectionForegroundColor.Should().Be("#FEDCBA"); - options.CellChangedColor.Should().Be("#0ABBCD"); - options.CellChangedSelectedColor.Should().Be("#0DCCEF"); - - options.ReadOnlyCellDepth0Color.Should().Be("#11111B"); - options.ReadOnlyCellDepth1Color.Should().Be("#22222B"); - options.ReadOnlyCellDepth2Color.Should().Be("#33333B"); - options.ReadOnlyCellDepth3Color.Should().Be("#44444B"); - options.ReadOnlyCellDepth0PastColor.Should().Be("#55555B"); - options.ReadOnlyCellDepth1PastColor.Should().Be("#66666B"); - options.ReadOnlyCellDepth2PastColor.Should().Be("#77777B"); - options.ReadOnlyCellDepth3PastColor.Should().Be("#88888B"); - options.ReadOnlyCellSelectedColor.Should().Be("#99999B"); - options.ReadOnlyCellForegroundColor.Should().Be("#AAAAAC"); - - options.DisabledCellDepth0Color.Should().Be("#11111A"); - options.DisabledCellDepth1Color.Should().Be("#22222A"); - options.DisabledCellDepth2Color.Should().Be("#33333A"); - options.DisabledCellDepth3Color.Should().Be("#44444A"); - options.DisabledCellDepth0PastColor.Should().Be("#55555A"); - options.DisabledCellDepth1PastColor.Should().Be("#66666A"); - options.DisabledCellDepth2PastColor.Should().Be("#77777A"); - options.DisabledCellDepth3PastColor.Should().Be("#88888A"); - options.DisabledCellSelectedColor.Should().Be("#99999A"); - options.DisabledCellForegroundColor.Should().Be("#AAAAAB"); - - options.ExecutionDepth0Color.Should().Be("#111111"); - options.ExecutionDepth1Color.Should().Be("#222222"); - options.ExecutionDepth2Color.Should().Be("#333333"); - options.ExecutionDepth3Color.Should().Be("#444444"); - options.ExecutionDepth0PastColor.Should().Be("#555555"); - options.ExecutionDepth1PastColor.Should().Be("#666666"); - options.ExecutionDepth2PastColor.Should().Be("#777777"); - options.ExecutionDepth3PastColor.Should().Be("#888888"); - options.ExecutionCurrentStepMarkerColor.Should().Be("#999999"); + options.Selection.Background.Should().Be("#ABCDEF"); + options.Selection.Foreground.Should().Be("#FEDCBA"); + options.ChangedCells.Changed.Should().Be("#0ABBCD"); + options.ChangedCells.ChangedSelected.Should().Be("#0DCCEF"); + + options.ReadOnlyCells.Depth0.Should().Be("#11111B"); + options.ReadOnlyCells.Depth1.Should().Be("#22222B"); + options.ReadOnlyCells.Depth2.Should().Be("#33333B"); + options.ReadOnlyCells.Depth3.Should().Be("#44444B"); + options.ReadOnlyCells.Depth0Past.Should().Be("#55555B"); + options.ReadOnlyCells.Depth1Past.Should().Be("#66666B"); + options.ReadOnlyCells.Depth2Past.Should().Be("#77777B"); + options.ReadOnlyCells.Depth3Past.Should().Be("#88888B"); + options.ReadOnlyCells.Selected.Should().Be("#99999B"); + options.ReadOnlyCells.Foreground.Should().Be("#AAAAAC"); + + options.DisabledCells.Depth0.Should().Be("#11111A"); + options.DisabledCells.Depth1.Should().Be("#22222A"); + options.DisabledCells.Depth2.Should().Be("#33333A"); + options.DisabledCells.Depth3.Should().Be("#44444A"); + options.DisabledCells.Depth0Past.Should().Be("#55555A"); + options.DisabledCells.Depth1Past.Should().Be("#66666A"); + options.DisabledCells.Depth2Past.Should().Be("#77777A"); + options.DisabledCells.Depth3Past.Should().Be("#88888A"); + options.DisabledCells.Selected.Should().Be("#99999A"); + options.DisabledCells.Foreground.Should().Be("#AAAAAB"); + + options.Execution.Depth0.Should().Be("#111111"); + options.Execution.Depth1.Should().Be("#222222"); + options.Execution.Depth2.Should().Be("#333333"); + options.Execution.Depth3.Should().Be("#444444"); + options.Execution.Depth0Past.Should().Be("#555555"); + options.Execution.Depth1Past.Should().Be("#666666"); + options.Execution.Depth2Past.Should().Be("#777777"); + options.Execution.Depth3Past.Should().Be("#888888"); + options.Execution.CurrentStepMarker.Should().Be("#999999"); } [Fact] @@ -81,19 +81,19 @@ public void Map_WiresEveryChromeFieldFromDto() var options = GridStyleMapper.Map(dto); - options.InfoColor.Should().Be("#1976D2"); - options.ConnectedColor.Should().Be("#44BB44"); - options.DisconnectedColor.Should().Be("#FF4444"); - options.LocalModeColor.Should().Be("#6C707E"); - options.ConnectingColor.Should().Be("#FFAF0F"); - options.PanelBackgroundColor.Should().Be("#F8F8F8"); - options.PanelHeaderBackgroundColor.Should().Be("#EEEEEE"); - options.SubtleBorderColor.Should().Be("#D0D0D0"); - options.SeparatorColor.Should().Be("#C0C0C0"); - options.SecondaryForegroundColor.Should().Be("#888888"); - options.GridBorderColor.Should().Be("#808080"); - options.GridBackgroundColor.Should().Be("#FFFFFF"); - options.HeaderForegroundColor.Should().Be("#000000"); + options.Chrome.Info.Should().Be("#1976D2"); + options.Chrome.Connected.Should().Be("#44BB44"); + options.Chrome.Disconnected.Should().Be("#FF4444"); + options.Chrome.LocalMode.Should().Be("#6C707E"); + options.Chrome.Connecting.Should().Be("#FFAF0F"); + options.Chrome.PanelBackground.Should().Be("#F8F8F8"); + options.Chrome.PanelHeaderBackground.Should().Be("#EEEEEE"); + options.Chrome.SubtleBorder.Should().Be("#D0D0D0"); + options.Chrome.Separator.Should().Be("#C0C0C0"); + options.Chrome.SecondaryForeground.Should().Be("#888888"); + options.Chrome.GridBorder.Should().Be("#808080"); + options.Chrome.GridBackground.Should().Be("#FFFFFF"); + options.Chrome.HeaderForeground.Should().Be("#000000"); } [Fact] @@ -103,11 +103,11 @@ public void Map_OmittedChromeSection_FallsBackToDefaults() var options = GridStyleMapper.Map(dto); - options.InfoColor.Should().Be(GridStyleOptions.Default.InfoColor); - options.LocalModeColor.Should().Be(GridStyleOptions.Default.LocalModeColor); - options.ConnectingColor.Should().Be(GridStyleOptions.Default.ConnectingColor); - options.GridBorderColor.Should().Be(GridStyleOptions.Default.GridBorderColor); - options.HeaderForegroundColor.Should().Be(GridStyleOptions.Default.HeaderForegroundColor); + options.Chrome.Info.Should().Be(GridStyleOptions.Default.Chrome.Info); + options.Chrome.LocalMode.Should().Be(GridStyleOptions.Default.Chrome.LocalMode); + options.Chrome.Connecting.Should().Be(GridStyleOptions.Default.Chrome.Connecting); + options.Chrome.GridBorder.Should().Be(GridStyleOptions.Default.Chrome.GridBorder); + options.Chrome.HeaderForeground.Should().Be(GridStyleOptions.Default.Chrome.HeaderForeground); } [Fact] @@ -126,7 +126,7 @@ public void Map_OmittedChangedColor_FallsBackToDefault() var options = GridStyleMapper.Map(dto); - options.CellChangedColor.Should().Be(GridStyleOptions.Default.CellChangedColor); + options.ChangedCells.Changed.Should().Be(GridStyleOptions.Default.ChangedCells.Changed); } [Fact] @@ -148,15 +148,15 @@ public void Map_WiresStatusBarFontFieldsFromDto() var options = GridStyleMapper.Map(dto); - options.StatusBarFontSize.Should().Be(16); - options.StatusBarFontWeight.Should().Be(500); - options.StatusBarItalic.Should().BeTrue(); - options.StatusBarTimerLabelFontSize.Should().Be(18); - options.StatusBarTimerLabelFontWeight.Should().Be(600); - options.StatusBarTimerLabelItalic.Should().BeTrue(); - options.StatusBarTimerValueFontSize.Should().Be(32); - options.StatusBarTimerValueFontWeight.Should().Be(800); - options.StatusBarTimerValueItalic.Should().BeTrue(); + options.StatusBar.FontSize.Should().Be(16); + options.StatusBar.Weight.Should().Be(500); + options.StatusBar.Italic.Should().BeTrue(); + options.StatusBar.TimerLabelFontSize.Should().Be(18); + options.StatusBar.TimerLabelWeight.Should().Be(600); + options.StatusBar.TimerLabelItalic.Should().BeTrue(); + options.StatusBar.TimerValueFontSize.Should().Be(32); + options.StatusBar.TimerValueWeight.Should().Be(800); + options.StatusBar.TimerValueItalic.Should().BeTrue(); } [Fact] @@ -176,13 +176,13 @@ public void Map_WiresGridFontFieldsFromDto() var options = GridStyleMapper.Map(dto); - options.FontFamily.Should().Be("Cascadia Mono"); - options.HeaderFontSize.Should().Be(20); - options.HeaderFontWeight.Should().Be(800); - options.HeaderItalic.Should().BeTrue(); - options.CellFontSize.Should().Be(15); - options.CellFontWeight.Should().Be(300); - options.CellItalic.Should().BeTrue(); + options.Fonts.FontFamily.Should().Be("Cascadia Mono"); + options.Fonts.HeaderFontSize.Should().Be(20); + options.Fonts.HeaderFontWeight.Should().Be(800); + options.Fonts.HeaderItalic.Should().BeTrue(); + options.Fonts.CellFontSize.Should().Be(15); + options.Fonts.CellFontWeight.Should().Be(300); + options.Fonts.CellItalic.Should().BeTrue(); } [Fact] @@ -192,45 +192,52 @@ public void Map_OmittedFontFields_FallBackToDefaults() var options = GridStyleMapper.Map(dto); - options.FontFamily.Should().Be(GridStyleOptions.Default.FontFamily); - options.HeaderFontSize.Should().Be(14); - options.HeaderFontWeight.Should().Be(700); - options.HeaderItalic.Should().BeFalse(); - options.CellFontSize.Should().Be(12); - options.CellFontWeight.Should().Be(400); - options.CellItalic.Should().BeFalse(); - options.StatusBarFontSize.Should().Be(12); - options.StatusBarFontWeight.Should().Be(400); - options.StatusBarItalic.Should().BeFalse(); - options.StatusBarTimerLabelFontSize.Should().Be(14); - options.StatusBarTimerLabelFontWeight.Should().Be(400); - options.StatusBarTimerLabelItalic.Should().BeFalse(); - options.StatusBarTimerValueFontSize.Should().Be(24); - options.StatusBarTimerValueFontWeight.Should().Be(400); - options.StatusBarTimerValueItalic.Should().BeFalse(); + options.Fonts.FontFamily.Should().Be(GridStyleOptions.Default.Fonts.FontFamily); + options.Fonts.HeaderFontSize.Should().Be(14); + options.Fonts.HeaderFontWeight.Should().Be(700); + options.Fonts.HeaderItalic.Should().BeFalse(); + options.Fonts.CellFontSize.Should().Be(12); + options.Fonts.CellFontWeight.Should().Be(400); + options.Fonts.CellItalic.Should().BeFalse(); + options.StatusBar.FontSize.Should().Be(12); + options.StatusBar.Weight.Should().Be(400); + options.StatusBar.Italic.Should().BeFalse(); + options.StatusBar.TimerLabelFontSize.Should().Be(14); + options.StatusBar.TimerLabelWeight.Should().Be(400); + options.StatusBar.TimerLabelItalic.Should().BeFalse(); + options.StatusBar.TimerValueFontSize.Should().Be(24); + options.StatusBar.TimerValueWeight.Should().Be(400); + options.StatusBar.TimerValueItalic.Should().BeFalse(); } [Fact] public void Map_OptionsToDto_WritesEveryFontField() { - var options = GridStyleOptions.Default with + var defaults = GridStyleOptions.Default; + var options = defaults with { - FontFamily = "Cascadia Mono", - HeaderFontSize = 20, - HeaderFontWeight = 800, - HeaderItalic = true, - CellFontSize = 15, - CellFontWeight = 300, - CellItalic = true, - StatusBarFontSize = 18, - StatusBarFontWeight = 500, - StatusBarItalic = true, - StatusBarTimerLabelFontSize = 19, - StatusBarTimerLabelFontWeight = 600, - StatusBarTimerLabelItalic = true, - StatusBarTimerValueFontSize = 30, - StatusBarTimerValueFontWeight = 800, - StatusBarTimerValueItalic = true + Fonts = defaults.Fonts with + { + FontFamily = "Cascadia Mono", + HeaderFontSize = 20, + HeaderFontWeight = 800, + HeaderItalic = true, + CellFontSize = 15, + CellFontWeight = 300, + CellItalic = true + }, + StatusBar = defaults.StatusBar with + { + FontSize = 18, + Weight = 500, + Italic = true, + TimerLabelFontSize = 19, + TimerLabelWeight = 600, + TimerLabelItalic = true, + TimerValueFontSize = 30, + TimerValueWeight = 800, + TimerValueItalic = true + } }; var dto = GridStyleDtoMapper.Map(options); @@ -256,7 +263,8 @@ public void Map_OptionsToDto_WritesEveryFontField() [Fact] public void Map_OptionsToDto_WritesLocalModeChromeColor() { - var options = GridStyleOptions.Default with { LocalModeColor = "#123456" }; + var defaults = GridStyleOptions.Default; + var options = defaults with { Chrome = defaults.Chrome with { LocalMode = "#123456" } }; var dto = GridStyleDtoMapper.Map(options); @@ -266,7 +274,8 @@ public void Map_OptionsToDto_WritesLocalModeChromeColor() [Fact] public void Map_OptionsToDto_WritesConnectingChromeColor() { - var options = GridStyleOptions.Default with { ConnectingColor = "#123456" }; + var defaults = GridStyleOptions.Default; + var options = defaults with { Chrome = defaults.Chrome with { Connecting = "#123456" } }; var dto = GridStyleDtoMapper.Map(options); diff --git a/SemiStep/SemiStep.Tests/Core/Configuration/GridStyleWriterTests.cs b/SemiStep/SemiStep.Tests/Core/Configuration/GridStyleWriterTests.cs index cd0aab04..899f2451 100644 --- a/SemiStep/SemiStep.Tests/Core/Configuration/GridStyleWriterTests.cs +++ b/SemiStep/SemiStep.Tests/Core/Configuration/GridStyleWriterTests.cs @@ -85,7 +85,7 @@ public async Task Save_EmitsColorScalarsDoubleQuoted() var content = await File.ReadAllTextAsync(filePath, TestContext.Current.CancellationToken); content.Should().Contain("\"#"); - content.Should().Contain($"\"{options.SelectionBackgroundColor}\""); + content.Should().Contain($"\"{options.Selection.Background}\""); var unquotedHex = Regex.Matches(content, @":\s*#[0-9A-Fa-f]+"); unquotedHex.Should().BeEmpty( @@ -102,7 +102,8 @@ public async Task Save_WhenMoveFails_FailsAndLeavesOriginalIntactWithNoTempOrpha var token = TestContext.Current.CancellationToken; var before = await File.ReadAllBytesAsync(filePath, token); - var options = await LoadOptions(tempDir.Path) with { CellFontSize = 20 }; + var loaded = await LoadOptions(tempDir.Path); + var options = loaded with { Fonts = loaded.Fonts with { CellFontSize = 20 } }; Result result; @@ -139,7 +140,8 @@ public async Task Save_UnderCommaLocale_EmitsInvariantDecimals() { using var tempDir = CopyShippedConfig("MBE"); var filePath = GridStyleFilePath(tempDir.Path); - var options = await LoadOptions(tempDir.Path) with { CellPaddingLeft = 6.5 }; + var loaded = await LoadOptions(tempDir.Path); + var options = loaded with { Layout = loaded.Layout with { CellPaddingLeft = 6.5 } }; var previousCulture = CultureInfo.CurrentCulture; try diff --git a/SemiStep/SemiStep.Tests/Helpers/GridStyleOptionsTestData.cs b/SemiStep/SemiStep.Tests/Helpers/GridStyleOptionsTestData.cs index f0897b24..84b5f069 100644 --- a/SemiStep/SemiStep.Tests/Helpers/GridStyleOptionsTestData.cs +++ b/SemiStep/SemiStep.Tests/Helpers/GridStyleOptionsTestData.cs @@ -11,90 +11,101 @@ namespace SemiStep.Tests.Helpers; /// int↔decimal and double↔decimal conversions are exact. All five italics are true — the one /// kind whose type default (false) can equal a fixture value — so a dropped italic Seed line /// surfaces as false ≠ true. is the non-default value, which -/// proves BuildRecord preserves the unsurfaced field through with. +/// guards that BuildRecord carries the unsurfaced field through positional construction from +/// _source.Orientation. /// public static class GridStyleOptionsTestData { public static GridStyleOptions Distinct() { return new GridStyleOptions( - FontFamily: "Distinct Test Font", - HeaderFontSize: 13, - HeaderFontWeight: 300, - HeaderItalic: true, - CellFontSize: 11, - CellFontWeight: 800, - CellItalic: true, - CellPaddingLeft: 6.5, - CellPaddingTop: 4.5, - CellPaddingRight: 7.5, - CellPaddingBottom: 3.5, - RowHeight: 28.5, - SelectionBackgroundColor: "#010101", - SelectionForegroundColor: "#020202", - CellChangedColor: "#030303", - CellChangedSelectedColor: "#040404", - DisabledCellDepth0Color: "#050505", - DisabledCellDepth1Color: "#060606", - DisabledCellDepth2Color: "#070707", - DisabledCellDepth3Color: "#080808", - DisabledCellDepth0PastColor: "#090909", - DisabledCellDepth1PastColor: "#0A0A0A", - DisabledCellDepth2PastColor: "#0B0B0B", - DisabledCellDepth3PastColor: "#0C0C0C", - DisabledCellSelectedColor: "#0D0D0D", - DisabledCellForegroundColor: "#0E0E0E", - ReadOnlyCellDepth0Color: "#0F0F0F", - ReadOnlyCellDepth1Color: "#101010", - ReadOnlyCellDepth2Color: "#111111", - ReadOnlyCellDepth3Color: "#121212", - ReadOnlyCellDepth0PastColor: "#131313", - ReadOnlyCellDepth1PastColor: "#141414", - ReadOnlyCellDepth2PastColor: "#151515", - ReadOnlyCellDepth3PastColor: "#161616", - ReadOnlyCellSelectedColor: "#171717", - ReadOnlyCellForegroundColor: "#181818", - GridLineColor: "#191919", - StatusBarBackgroundColor: "#1A1A1A", - StatusBarForegroundColor: "#1B1B1B", - StatusBarPadding: 5.5, - StatusBarItemSpacing: 10.5, - StatusBarFontSize: 17, - StatusBarFontWeight: 500, - StatusBarItalic: true, - StatusBarTimerLabelFontSize: 19, - StatusBarTimerLabelFontWeight: 600, - StatusBarTimerLabelItalic: true, - StatusBarTimerValueFontSize: 23, - StatusBarTimerValueFontWeight: 700, - StatusBarTimerValueItalic: true, - ValidationPanelBackgroundColor: "#1C1C1C", - ValidationPanelForegroundColor: "#1D1D1D", - ValidationPanelErrorColor: "#1E1E1E", - ValidationPanelWarningColor: "#1F1F1F", - ValidationPanelMaxHeight: 150.5, - ExecutionDepth0Color: "#202020", - ExecutionDepth1Color: "#212121", - ExecutionDepth2Color: "#222222", - ExecutionDepth3Color: "#232323", - ExecutionDepth0PastColor: "#242424", - ExecutionDepth1PastColor: "#252525", - ExecutionDepth2PastColor: "#262626", - ExecutionDepth3PastColor: "#272727", - ExecutionCurrentStepMarkerColor: "#282828", - InfoColor: "#292929", - ConnectedColor: "#2A2A2A", - DisconnectedColor: "#2B2B2B", - LocalModeColor: "#2C2C2C", - ConnectingColor: "#2D2D2D", - PanelBackgroundColor: "#2E2E2E", - PanelHeaderBackgroundColor: "#2F2F2F", - SubtleBorderColor: "#303030", - SeparatorColor: "#313131", - SecondaryForegroundColor: "#323232", - GridBorderColor: "#333333", - GridBackgroundColor: "#343434", - HeaderForegroundColor: "#353535", + Fonts: new GridStyleFonts( + FontFamily: "Distinct Test Font", + HeaderFontSize: 13, + HeaderFontWeight: 300, + HeaderItalic: true, + CellFontSize: 11, + CellFontWeight: 800, + CellItalic: true), + Layout: new GridStyleLayout( + CellPaddingLeft: 6.5, + CellPaddingTop: 4.5, + CellPaddingRight: 7.5, + CellPaddingBottom: 3.5, + RowHeight: 28.5), + Selection: new SelectionColors( + Background: "#010101", + Foreground: "#020202"), + ChangedCells: new ChangedCellColors( + Changed: "#030303", + ChangedSelected: "#040404"), + DisabledCells: new DepthPalette( + Depth0: "#050505", + Depth1: "#060606", + Depth2: "#070707", + Depth3: "#080808", + Depth0Past: "#090909", + Depth1Past: "#0A0A0A", + Depth2Past: "#0B0B0B", + Depth3Past: "#0C0C0C", + Selected: "#0D0D0D", + Foreground: "#0E0E0E"), + ReadOnlyCells: new DepthPalette( + Depth0: "#0F0F0F", + Depth1: "#101010", + Depth2: "#111111", + Depth3: "#121212", + Depth0Past: "#131313", + Depth1Past: "#141414", + Depth2Past: "#151515", + Depth3Past: "#161616", + Selected: "#171717", + Foreground: "#181818"), + Chrome: new ChromeColors( + Info: "#292929", + Connected: "#2A2A2A", + Disconnected: "#2B2B2B", + LocalMode: "#2C2C2C", + Connecting: "#2D2D2D", + PanelBackground: "#2E2E2E", + PanelHeaderBackground: "#2F2F2F", + SubtleBorder: "#303030", + Separator: "#313131", + SecondaryForeground: "#323232", + GridBorder: "#333333", + GridBackground: "#343434", + HeaderForeground: "#353535", + GridLine: "#191919"), + StatusBar: new StatusBarStyle( + Background: "#1A1A1A", + Foreground: "#1B1B1B", + Padding: 5.5, + ItemSpacing: 10.5, + FontSize: 17, + Weight: 500, + Italic: true, + TimerLabelFontSize: 19, + TimerLabelWeight: 600, + TimerLabelItalic: true, + TimerValueFontSize: 23, + TimerValueWeight: 700, + TimerValueItalic: true), + ValidationPanel: new ValidationPanelStyle( + Background: "#1C1C1C", + Foreground: "#1D1D1D", + ErrorColor: "#1E1E1E", + WarningColor: "#1F1F1F", + MaxHeight: 150.5), + Execution: new ExecutionPalette( + Depth0: "#202020", + Depth1: "#212121", + Depth2: "#222222", + Depth3: "#232323", + Depth0Past: "#242424", + Depth1Past: "#252525", + Depth2Past: "#262626", + Depth3Past: "#272727", + CurrentStepMarker: "#282828"), Orientation: GridOrientation.ColumnsAsSteps); } } diff --git a/SemiStep/SemiStep.Tests/UI/MainWindow/AppStatusBarTimerFontTests.cs b/SemiStep/SemiStep.Tests/UI/MainWindow/AppStatusBarTimerFontTests.cs index 64c1b68a..7217eda7 100644 --- a/SemiStep/SemiStep.Tests/UI/MainWindow/AppStatusBarTimerFontTests.cs +++ b/SemiStep/SemiStep.Tests/UI/MainWindow/AppStatusBarTimerFontTests.cs @@ -44,12 +44,16 @@ public ValueTask DisposeAsync() [AvaloniaFact] public void Timer_LabelAndValueTextBlocks_RenderAtTheirSeparateRoleSizes() { - var statusBar = ShowStatusBar(GridStyleOptions.Default with + var defaults = GridStyleOptions.Default; + var statusBar = ShowStatusBar(defaults with { - StatusBarTimerLabelFontSize = (int)TimerLabelFontSize, - StatusBarTimerLabelFontWeight = 400, - StatusBarTimerValueFontSize = (int)TimerValueFontSize, - StatusBarTimerValueFontWeight = 700 + StatusBar = defaults.StatusBar with + { + TimerLabelFontSize = (int)TimerLabelFontSize, + TimerLabelWeight = 400, + TimerValueFontSize = (int)TimerValueFontSize, + TimerValueWeight = 700 + } }); var labels = statusBar.GetVisualDescendants() diff --git a/SemiStep/SemiStep.Tests/UI/RecipeGrid/ColumnWidthCalculatorTests.cs b/SemiStep/SemiStep.Tests/UI/RecipeGrid/ColumnWidthCalculatorTests.cs index 657b1e91..e3c4008b 100644 --- a/SemiStep/SemiStep.Tests/UI/RecipeGrid/ColumnWidthCalculatorTests.cs +++ b/SemiStep/SemiStep.Tests/UI/RecipeGrid/ColumnWidthCalculatorTests.cs @@ -35,13 +35,13 @@ public sealed class ColumnWidthCalculatorTests : IAsyncLifetime private const int LargeHeaderFontSize = 28; private static int ContentChrome => - (int)Math.Ceiling(GridStyleOptions.Default.CellFontSize * ChromeFontMultiple); + (int)Math.Ceiling(GridStyleOptions.Default.Fonts.CellFontSize * ChromeFontMultiple); private static int HeaderFloorChrome => - (int)Math.Ceiling(GridStyleOptions.Default.HeaderFontSize * ChromeFontMultiple); + (int)Math.Ceiling(GridStyleOptions.Default.Fonts.HeaderFontSize * ChromeFontMultiple); private static int MinColumnWidthFloor => - (int)Math.Ceiling(GridStyleOptions.Default.CellFontSize * MinColumnWidthEms); + (int)Math.Ceiling(GridStyleOptions.Default.Fonts.CellFontSize * MinColumnWidthEms); private readonly UIFixture _fixture = new(); private ColumnWidthCalculator _calculator = null!; @@ -100,7 +100,7 @@ public void MultiWordHeader_FloorsAtLongestWord() var pixelWidth = GetPixelWidth(_calculator.CalculateColumnWidth(taskColumn)); var wholeHeaderWidth = (int)Math.Ceiling( - MeasureText(header, GridStyleOptions.Default.HeaderFontSize, FontWeight.Bold) + HeaderFloorChrome); + MeasureText(header, GridStyleOptions.Default.Fonts.HeaderFontSize, FontWeight.Bold) + HeaderFloorChrome); pixelWidth.Should().BeLessThan(wholeHeaderWidth, "the floor is the longest WORD, never the whole single-line header"); @@ -111,8 +111,8 @@ public void HeaderFloorMeasuredBoldAtHeaderFontSize() { var word = "Начальное"; - var boldHeaderFont = MeasureText(word, GridStyleOptions.Default.HeaderFontSize, FontWeight.Bold); - var normalCellFont = MeasureText(word, GridStyleOptions.Default.CellFontSize, FontWeight.Normal); + var boldHeaderFont = MeasureText(word, GridStyleOptions.Default.Fonts.HeaderFontSize, FontWeight.Bold); + var normalCellFont = MeasureText(word, GridStyleOptions.Default.Fonts.CellFontSize, FontWeight.Normal); boldHeaderFont.Should().BeGreaterThan(normalCellFont, "the header-word floor must be measured bold at HeaderFontSize, not at the cell font"); @@ -124,7 +124,7 @@ public void LongestWordFloor_FarBelowWholeHeaderWidth() var header = "An extremely long header label that would otherwise widen this column"; var longestWordFloor = LongestHeaderWordFloor(header); - var wholeHeaderWidth = MeasureText(header, GridStyleOptions.Default.HeaderFontSize, FontWeight.Bold) + var wholeHeaderWidth = MeasureText(header, GridStyleOptions.Default.Fonts.HeaderFontSize, FontWeight.Bold) + HeaderFloorChrome; longestWordFloor.Should().BeLessThan(wholeHeaderWidth / 2, @@ -159,7 +159,7 @@ public void PropertyField_WithMax_SizesFromMaxRepresentative() durationProperty.Max!.Value.ToString(CultureInfo.InvariantCulture), durationProperty.FormatKind, durationProperty.Units); - var contentWidth = MeasureText(representative, GridStyleOptions.Default.CellFontSize, FontWeight.Normal); + var contentWidth = MeasureText(representative, GridStyleOptions.Default.Fonts.CellFontSize, FontWeight.Normal); pixelWidth.Should().BeGreaterThan((int)contentWidth, "the column must fully cover the formatted Max representative"); @@ -183,7 +183,7 @@ public void PropertyField_StringTyped_SizesFromCappedSampleNotFullMaxLength() var pixelWidth = GetPixelWidth(_calculator.CalculateColumnWidth(stringColumn)); var cappedRepresentative = new string('0', StringSampleCap); - var cappedContentWidth = MeasureText(cappedRepresentative, GridStyleOptions.Default.CellFontSize, FontWeight.Normal); + var cappedContentWidth = MeasureText(cappedRepresentative, GridStyleOptions.Default.Fonts.CellFontSize, FontWeight.Normal); pixelWidth.Should().BeGreaterThan((int)cappedContentWidth, "the column must fully cover the capped sample"); @@ -191,7 +191,7 @@ public void PropertyField_StringTyped_SizesFromCappedSampleNotFullMaxLength() var fullLengthRepresentative = new string('0', stringProperty.MaxLength!.Value); var fullLengthWidth = (int)Math.Ceiling( - MeasureText(fullLengthRepresentative, GridStyleOptions.Default.CellFontSize, FontWeight.Normal) + ContentChrome); + MeasureText(fullLengthRepresentative, GridStyleOptions.Default.Fonts.CellFontSize, FontWeight.Normal) + ContentChrome); pixelWidth.Should().BeLessThan(fullLengthWidth, "the cap must keep the column far below the full MaxLength width"); } @@ -309,7 +309,7 @@ public void PropertyField_NegativeMinWiderThanMax_SizesFromMinRepresentative() [AvaloniaFact] public void Chrome_ScalesWithFont_ChromeRemainderGrowsBeyondContentScaling() { - var smallCellFont = GridStyleOptions.Default.CellFontSize; + var smallCellFont = GridStyleOptions.Default.Fonts.CellFontSize; var largeCellFont = LargeCellFontSize; var durationColumn = _fixture.RecipeMetadataRegistry.GetColumn("step_duration").Value; @@ -317,7 +317,7 @@ public void Chrome_ScalesWithFont_ChromeRemainderGrowsBeyondContentScaling() _fixture.RecipeMetadataRegistry, GridStyleOptions.Default); var largeFontCalculator = new ColumnWidthCalculator( _fixture.RecipeMetadataRegistry, - GridStyleOptions.Default with { CellFontSize = largeCellFont, HeaderFontSize = LargeHeaderFontSize }); + GridStyleOptions.Default with { Fonts = GridStyleOptions.Default.Fonts with { CellFontSize = largeCellFont, HeaderFontSize = LargeHeaderFontSize } }); var smallFontWidth = GetPixelWidth(smallFontCalculator.CalculateColumnWidth(durationColumn)); var largeFontWidth = GetPixelWidth(largeFontCalculator.CalculateColumnWidth(durationColumn)); @@ -343,13 +343,17 @@ public void Typefaces_ThreadConfiguredFamilyWeightAndItalic_PerRole() // The headless text shaper measures by glyph count and size only, so weight/family/italic // produce identical widths; assert the role typefaces the measurement uses instead, which // fails if a regression drops family/weight/italic from the measured Typeface. - var style = GridStyleOptions.Default with + var defaults = GridStyleOptions.Default; + var style = defaults with { - FontFamily = "Courier New", - CellFontWeight = 600, - CellItalic = true, - HeaderFontWeight = 900, - HeaderItalic = false + Fonts = defaults.Fonts with + { + FontFamily = "Courier New", + CellFontWeight = 600, + CellItalic = true, + HeaderFontWeight = 900, + HeaderItalic = false + } }; var calculator = new ColumnWidthCalculator(_fixture.RecipeMetadataRegistry, style); @@ -404,7 +408,7 @@ public void ComboColumn_WiderThanContentColumn_ByChevronBudget() [AvaloniaFact] public void ComboChrome_StaysConstantAcrossFonts_WhileContentChromeGrows() { - var smallCellFont = GridStyleOptions.Default.CellFontSize; + var smallCellFont = GridStyleOptions.Default.Fonts.CellFontSize; var largeCellFont = LargeCellFontSize; var representative = new string('0', StringSampleCap); var sampleProperty = TestPropertyTypeDefinitionBuilder.CreateString("sample", StringSampleCap); @@ -429,7 +433,7 @@ public void ComboChrome_StaysConstantAcrossFonts_WhileContentChromeGrows() var smallFontCalculator = new ColumnWidthCalculator(registry, GridStyleOptions.Default); var largeFontCalculator = new ColumnWidthCalculator( - registry, GridStyleOptions.Default with { CellFontSize = largeCellFont, HeaderFontSize = LargeHeaderFontSize }); + registry, GridStyleOptions.Default with { Fonts = GridStyleOptions.Default.Fonts with { CellFontSize = largeCellFont, HeaderFontSize = LargeHeaderFontSize } }); var smallComboWidth = GetPixelWidth(smallFontCalculator.CalculateColumnWidth(comboColumn)); var largeComboWidth = GetPixelWidth(largeFontCalculator.CalculateColumnWidth(comboColumn)); @@ -462,7 +466,7 @@ private static double MeasureRepresentative(PropertyTypeDefinition property, dou property.FormatKind, property.Units); - return MeasureText(representative, GridStyleOptions.Default.CellFontSize, FontWeight.Normal); + return MeasureText(representative, GridStyleOptions.Default.Fonts.CellFontSize, FontWeight.Normal); } private static double LongestHeaderWordFloor(string header) @@ -477,7 +481,7 @@ private static double LongestHeaderWordFloor(string header) var maxWordWidth = 0.0; foreach (var word in words) { - var wordWidth = MeasureText(word, GridStyleOptions.Default.HeaderFontSize, FontWeight.Bold); + var wordWidth = MeasureText(word, GridStyleOptions.Default.Fonts.HeaderFontSize, FontWeight.Bold); if (wordWidth > maxWordWidth) { maxWordWidth = wordWidth; @@ -492,7 +496,7 @@ private double MeasureActionContentWidth() var maxWidth = 0.0; foreach (var action in _fixture.RecipeMetadataRegistry.GetAllActions()) { - var width = MeasureText(action.UiName, GridStyleOptions.Default.CellFontSize, FontWeight.Normal); + var width = MeasureText(action.UiName, GridStyleOptions.Default.Fonts.CellFontSize, FontWeight.Normal); if (width > maxWidth) { maxWidth = width; diff --git a/SemiStep/SemiStep.Tests/UI/RecipeGrid/GridFactoryFontTests.cs b/SemiStep/SemiStep.Tests/UI/RecipeGrid/GridFactoryFontTests.cs index a8b6745d..d3392006 100644 --- a/SemiStep/SemiStep.Tests/UI/RecipeGrid/GridFactoryFontTests.cs +++ b/SemiStep/SemiStep.Tests/UI/RecipeGrid/GridFactoryFontTests.cs @@ -65,7 +65,8 @@ public void NumberingColumn_CarriesConfiguredCellFont() [AvaloniaFact] public void NumberingColumn_EmptyFamily_UsesChromeFontDefault() { - var grid = BuildGrid(GridStyleOptions.Default with { FontFamily = "" }); + var emptyFamily = GridStyleOptions.Default; + var grid = BuildGrid(emptyFamily with { Fonts = emptyFamily.Fonts with { FontFamily = "" } }); var numberingColumn = (DataGridTextColumn)grid.Columns[0]; @@ -112,7 +113,8 @@ public void ComboBoxCell_CarriesConfiguredCellFont() [AvaloniaFact] public void EmptyFamily_UsesChromeFontDefault() { - var factory = new TextCellFactory(GridStyleOptions.Default with { FontFamily = "" }); + var emptyFamily = GridStyleOptions.Default; + var factory = new TextCellFactory(emptyFamily with { Fonts = emptyFamily.Fonts with { FontFamily = "" } }); var columnDef = _fixture.RecipeMetadataRegistry.GetColumn("task").Value; var column = (DataGridTemplateColumn)factory.CreateEditableColumn(columnDef, new DataGridLength(100), maxLength: null); @@ -125,15 +127,19 @@ public void EmptyFamily_UsesChromeFontDefault() private GridStyleOptions NonDefaultStyle() { - return GridStyleOptions.Default with + var defaults = GridStyleOptions.Default; + return defaults with { - FontFamily = ConfiguredFamily, - HeaderFontSize = ConfiguredHeaderSize, - HeaderFontWeight = ConfiguredHeaderWeight, - HeaderItalic = true, - CellFontSize = ConfiguredCellSize, - CellFontWeight = ConfiguredCellWeight, - CellItalic = true, + Fonts = defaults.Fonts with + { + FontFamily = ConfiguredFamily, + HeaderFontSize = ConfiguredHeaderSize, + HeaderFontWeight = ConfiguredHeaderWeight, + HeaderItalic = true, + CellFontSize = ConfiguredCellSize, + CellFontWeight = ConfiguredCellWeight, + CellItalic = true, + }, }; } diff --git a/SemiStep/SemiStep.Tests/UI/RecipeGrid/Transposed/TransposedCellBackgroundConverterTests.cs b/SemiStep/SemiStep.Tests/UI/RecipeGrid/Transposed/TransposedCellBackgroundConverterTests.cs index 7fd15e5c..0e6af33d 100644 --- a/SemiStep/SemiStep.Tests/UI/RecipeGrid/Transposed/TransposedCellBackgroundConverterTests.cs +++ b/SemiStep/SemiStep.Tests/UI/RecipeGrid/Transposed/TransposedCellBackgroundConverterTests.cs @@ -155,37 +155,46 @@ private static Control BuildHostWithPalette(out IResourceDictionary resources) // different brush instance and the reference check fails loudly. private static GridStyleOptions BuildDistinctPaletteGridStyle() { - return GridStyleOptions.Default with + var defaults = GridStyleOptions.Default; + return defaults with { - GridBackgroundColor = "#101010", - SelectionBackgroundColor = "#202020", - CellChangedColor = "#303030", - CellChangedSelectedColor = "#404040", - ReadOnlyCellSelectedColor = "#505050", - DisabledCellSelectedColor = "#606060", - ExecutionDepth1Color = "#110000", - ExecutionDepth2Color = "#120000", - ExecutionDepth3Color = "#130000", - ExecutionDepth0PastColor = "#140000", - ExecutionDepth1PastColor = "#150000", - ExecutionDepth2PastColor = "#160000", - ExecutionDepth3PastColor = "#170000", - ReadOnlyCellDepth0Color = "#210000", - ReadOnlyCellDepth1Color = "#220000", - ReadOnlyCellDepth2Color = "#230000", - ReadOnlyCellDepth3Color = "#240000", - ReadOnlyCellDepth0PastColor = "#250000", - ReadOnlyCellDepth1PastColor = "#260000", - ReadOnlyCellDepth2PastColor = "#270000", - ReadOnlyCellDepth3PastColor = "#280000", - DisabledCellDepth0Color = "#310000", - DisabledCellDepth1Color = "#320000", - DisabledCellDepth2Color = "#330000", - DisabledCellDepth3Color = "#340000", - DisabledCellDepth0PastColor = "#350000", - DisabledCellDepth1PastColor = "#360000", - DisabledCellDepth2PastColor = "#370000", - DisabledCellDepth3PastColor = "#380000", + Chrome = defaults.Chrome with { GridBackground = "#101010" }, + Selection = defaults.Selection with { Background = "#202020" }, + ChangedCells = new ChangedCellColors( + Changed: "#303030", + ChangedSelected: "#404040"), + ReadOnlyCells = new DepthPalette( + Depth0: "#210000", + Depth1: "#220000", + Depth2: "#230000", + Depth3: "#240000", + Depth0Past: "#250000", + Depth1Past: "#260000", + Depth2Past: "#270000", + Depth3Past: "#280000", + Selected: "#505050", + Foreground: defaults.ReadOnlyCells.Foreground), + DisabledCells = new DepthPalette( + Depth0: "#310000", + Depth1: "#320000", + Depth2: "#330000", + Depth3: "#340000", + Depth0Past: "#350000", + Depth1Past: "#360000", + Depth2Past: "#370000", + Depth3Past: "#380000", + Selected: "#606060", + Foreground: defaults.DisabledCells.Foreground), + Execution = defaults.Execution with + { + Depth1 = "#110000", + Depth2 = "#120000", + Depth3 = "#130000", + Depth0Past = "#140000", + Depth1Past = "#150000", + Depth2Past = "#160000", + Depth3Past = "#170000", + }, }; } } diff --git a/SemiStep/SemiStep.Tests/UI/StyleEditor/GridStyleEditorViewModelTests.cs b/SemiStep/SemiStep.Tests/UI/StyleEditor/GridStyleEditorViewModelTests.cs index 50cc34d5..2873e157 100644 --- a/SemiStep/SemiStep.Tests/UI/StyleEditor/GridStyleEditorViewModelTests.cs +++ b/SemiStep/SemiStep.Tests/UI/StyleEditor/GridStyleEditorViewModelTests.cs @@ -48,7 +48,7 @@ public void Seed_PopulatesEverySurfacedProperty_FromDistinctFixture() foreach (var property in properties) { - var recordValue = RecordField(property).GetValue(fixture); + var recordValue = LeafValue(fixture, RecordPath(property)); var actual = property.GetValue(viewModel); var because = $"property {property.Name} must be seeded from the fixture"; @@ -95,7 +95,7 @@ public void BuildRecord_PerturbingEachProperty_ChangesOnlyThatMappedField() property.SetValue(viewModel, Perturb(property, seededValue)); var built = viewModel.BuildRecord(); - var mappedField = RecordField(property).Name; + var mappedField = RecordPath(property); var changedFields = ChangedRecordFields(baseline, built); changedFields.Should().BeEquivalentTo( new[] { mappedField }, @@ -119,8 +119,8 @@ public void BuildRecord_AfterEditingColorAndFontSize_ChangesOnlyThoseFields() var expected = source with { - CellFontSize = 18, - SelectionBackgroundColor = "#123456" + Fonts = source.Fonts with { CellFontSize = 18 }, + Selection = source.Selection with { Background = "#123456" } }; record.Should().Be(expected); } @@ -132,7 +132,7 @@ public void BuildRecord_RoundsFractionalStatusBarFontSize_ToNearestInt() viewModel.StatusBarFontSize = 14.6m; - viewModel.BuildRecord().StatusBarFontSize.Should().Be(15); + viewModel.BuildRecord().StatusBar.FontSize.Should().Be(15); } [AvaloniaTheory] @@ -174,14 +174,21 @@ public void CanSave_IsFalse_WhenStatusBarTimerLabelFontSizeOutOfRange(int fontSi [AvaloniaFact] public void Seed_PopulatesFontFamilyWeightAndItalic_FromRecord() { - var source = GridStyleOptions.Default with + var defaults = GridStyleOptions.Default; + var source = defaults with { - FontFamily = "Cascadia Code", - HeaderFontWeight = 600, - HeaderItalic = true, - CellFontWeight = 500, - StatusBarTimerLabelFontSize = 18, - StatusBarTimerLabelItalic = true + Fonts = defaults.Fonts with + { + FontFamily = "Cascadia Code", + HeaderFontWeight = 600, + HeaderItalic = true, + CellFontWeight = 500 + }, + StatusBar = defaults.StatusBar with + { + TimerLabelFontSize = 18, + TimerLabelItalic = true + } }; var viewModel = CreateViewModel(source); @@ -206,16 +213,17 @@ public void BuildRecord_CarriesEditedFontFamilyWeightAndItalic() var record = viewModel.BuildRecord(); - record.FontFamily.Should().Be("Consolas"); - record.HeaderFontWeight.Should().Be(900); - record.CellItalic.Should().BeTrue(); - record.StatusBarTimerValueFontWeight.Should().Be(300); + record.Fonts.FontFamily.Should().Be("Consolas"); + record.Fonts.HeaderFontWeight.Should().Be(900); + record.Fonts.CellItalic.Should().BeTrue(); + record.StatusBar.TimerValueWeight.Should().Be(300); } [AvaloniaFact] public void AvailableFontWeights_IncludeSeededWeights_NotInCuratedList() { - var source = GridStyleOptions.Default with { HeaderFontWeight = 333 }; + var defaults = GridStyleOptions.Default; + var source = defaults with { Fonts = defaults.Fonts with { HeaderFontWeight = 333 } }; var viewModel = CreateViewModel(source); viewModel.AvailableFontWeights.Should().Contain(option => option.Value == 333); @@ -224,7 +232,8 @@ public void AvailableFontWeights_IncludeSeededWeights_NotInCuratedList() [AvaloniaFact] public void AvailableFontFamilies_StartWithDefaultSentinel_AndIncludeSeededFamily() { - var source = GridStyleOptions.Default with { FontFamily = "No Such Installed Font 12345" }; + var defaults = GridStyleOptions.Default; + var source = defaults with { Fonts = defaults.Fonts with { FontFamily = "No Such Installed Font 12345" } }; var viewModel = CreateViewModel(source); viewModel.AvailableFontFamilies[0].Value.Should().Be(""); @@ -280,7 +289,7 @@ public void ToHex_PreservesSixDigitForm_ForOpaqueColors() viewModel.SelectionBackground = Color.Parse("#A1B2C3"); - viewModel.BuildRecord().SelectionBackgroundColor.Should().Be("#A1B2C3"); + viewModel.BuildRecord().Selection.Background.Should().Be("#A1B2C3"); } [AvaloniaFact] @@ -290,7 +299,7 @@ public void ToHex_EmitsEightDigitForm_ForNonOpaqueColors() viewModel.SelectionBackground = Color.FromArgb(0x80, 0x11, 0x22, 0x33); - viewModel.BuildRecord().SelectionBackgroundColor.Should().Be("#80112233"); + viewModel.BuildRecord().Selection.Background.Should().Be("#80112233"); } [AvaloniaFact] @@ -435,14 +444,111 @@ private static IReadOnlyList EditableProperties() .ToList(); } - private static PropertyInfo RecordField(PropertyInfo viewModelProperty) + // Hand-maintained VM-property -> nested record leaf path. NOT derived: several leaf names diverge + // from the VM property name (StatusBarFontWeight -> StatusBar.Weight, ValidationPanelError -> + // ValidationPanel.ErrorColor, every Color drops its trailing "Color"), so a mistyped entry surfaces + // as a red guard test rather than a silent pass. Every entry is cross-checked against the group records. + private static readonly IReadOnlyDictionary _viewModelToRecordPath = + new Dictionary + { + ["FontFamily"] = "Fonts.FontFamily", + ["HeaderFontSize"] = "Fonts.HeaderFontSize", + ["HeaderFontWeight"] = "Fonts.HeaderFontWeight", + ["HeaderItalic"] = "Fonts.HeaderItalic", + ["CellFontSize"] = "Fonts.CellFontSize", + ["CellFontWeight"] = "Fonts.CellFontWeight", + ["CellItalic"] = "Fonts.CellItalic", + ["CellPaddingLeft"] = "Layout.CellPaddingLeft", + ["CellPaddingTop"] = "Layout.CellPaddingTop", + ["CellPaddingRight"] = "Layout.CellPaddingRight", + ["CellPaddingBottom"] = "Layout.CellPaddingBottom", + ["RowHeight"] = "Layout.RowHeight", + ["SelectionBackground"] = "Selection.Background", + ["SelectionForeground"] = "Selection.Foreground", + ["CellChanged"] = "ChangedCells.Changed", + ["CellChangedSelected"] = "ChangedCells.ChangedSelected", + ["ReadOnlyCellDepth0"] = "ReadOnlyCells.Depth0", + ["ReadOnlyCellDepth1"] = "ReadOnlyCells.Depth1", + ["ReadOnlyCellDepth2"] = "ReadOnlyCells.Depth2", + ["ReadOnlyCellDepth3"] = "ReadOnlyCells.Depth3", + ["ReadOnlyCellDepth0Past"] = "ReadOnlyCells.Depth0Past", + ["ReadOnlyCellDepth1Past"] = "ReadOnlyCells.Depth1Past", + ["ReadOnlyCellDepth2Past"] = "ReadOnlyCells.Depth2Past", + ["ReadOnlyCellDepth3Past"] = "ReadOnlyCells.Depth3Past", + ["ReadOnlyCellSelected"] = "ReadOnlyCells.Selected", + ["ReadOnlyCellForeground"] = "ReadOnlyCells.Foreground", + ["DisabledCellDepth0"] = "DisabledCells.Depth0", + ["DisabledCellDepth1"] = "DisabledCells.Depth1", + ["DisabledCellDepth2"] = "DisabledCells.Depth2", + ["DisabledCellDepth3"] = "DisabledCells.Depth3", + ["DisabledCellDepth0Past"] = "DisabledCells.Depth0Past", + ["DisabledCellDepth1Past"] = "DisabledCells.Depth1Past", + ["DisabledCellDepth2Past"] = "DisabledCells.Depth2Past", + ["DisabledCellDepth3Past"] = "DisabledCells.Depth3Past", + ["DisabledCellSelected"] = "DisabledCells.Selected", + ["DisabledCellForeground"] = "DisabledCells.Foreground", + ["ExecutionDepth0"] = "Execution.Depth0", + ["ExecutionDepth1"] = "Execution.Depth1", + ["ExecutionDepth2"] = "Execution.Depth2", + ["ExecutionDepth3"] = "Execution.Depth3", + ["ExecutionDepth0Past"] = "Execution.Depth0Past", + ["ExecutionDepth1Past"] = "Execution.Depth1Past", + ["ExecutionDepth2Past"] = "Execution.Depth2Past", + ["ExecutionDepth3Past"] = "Execution.Depth3Past", + ["ExecutionCurrentStepMarker"] = "Execution.CurrentStepMarker", + ["StatusBarBackground"] = "StatusBar.Background", + ["StatusBarForeground"] = "StatusBar.Foreground", + ["StatusBarPadding"] = "StatusBar.Padding", + ["StatusBarItemSpacing"] = "StatusBar.ItemSpacing", + ["StatusBarFontSize"] = "StatusBar.FontSize", + ["StatusBarFontWeight"] = "StatusBar.Weight", + ["StatusBarItalic"] = "StatusBar.Italic", + ["StatusBarTimerLabelFontSize"] = "StatusBar.TimerLabelFontSize", + ["StatusBarTimerLabelFontWeight"] = "StatusBar.TimerLabelWeight", + ["StatusBarTimerLabelItalic"] = "StatusBar.TimerLabelItalic", + ["StatusBarTimerValueFontSize"] = "StatusBar.TimerValueFontSize", + ["StatusBarTimerValueFontWeight"] = "StatusBar.TimerValueWeight", + ["StatusBarTimerValueItalic"] = "StatusBar.TimerValueItalic", + ["ValidationPanelBackground"] = "ValidationPanel.Background", + ["ValidationPanelForeground"] = "ValidationPanel.Foreground", + ["ValidationPanelError"] = "ValidationPanel.ErrorColor", + ["ValidationPanelWarning"] = "ValidationPanel.WarningColor", + ["ValidationPanelMaxHeight"] = "ValidationPanel.MaxHeight", + ["GridLine"] = "Chrome.GridLine", + ["Info"] = "Chrome.Info", + ["Connected"] = "Chrome.Connected", + ["Disconnected"] = "Chrome.Disconnected", + ["LocalMode"] = "Chrome.LocalMode", + ["Connecting"] = "Chrome.Connecting", + ["PanelBackground"] = "Chrome.PanelBackground", + ["PanelHeaderBackground"] = "Chrome.PanelHeaderBackground", + ["SubtleBorder"] = "Chrome.SubtleBorder", + ["Separator"] = "Chrome.Separator", + ["SecondaryForeground"] = "Chrome.SecondaryForeground", + ["GridBorder"] = "Chrome.GridBorder", + ["GridBackground"] = "Chrome.GridBackground", + ["HeaderForeground"] = "Chrome.HeaderForeground", + }; + + private static string RecordPath(PropertyInfo viewModelProperty) { - var fieldName = viewModelProperty.PropertyType == typeof(Color) - ? viewModelProperty.Name + "Color" - : viewModelProperty.Name; + return _viewModelToRecordPath.TryGetValue(viewModelProperty.Name, out var path) + ? path + : throw new InvalidOperationException($"No GridStyleOptions path maps to {viewModelProperty.Name}"); + } - return typeof(GridStyleOptions).GetProperty(fieldName) - ?? throw new InvalidOperationException($"No GridStyleOptions field maps to {viewModelProperty.Name}"); + private static object? LeafValue(GridStyleOptions record, string dottedPath) + { + object current = record; + foreach (var segment in dottedPath.Split('.')) + { + var property = current.GetType().GetProperty(segment) + ?? throw new InvalidOperationException($"No record property '{segment}' on {current.GetType().Name}"); + current = property.GetValue(current) + ?? throw new InvalidOperationException($"Record property '{segment}' was null"); + } + + return current; } // Color perturbation stays opaque so ToHex changes; weights are unvalidated ints so any value round-trips. @@ -477,12 +583,42 @@ private static PropertyInfo RecordField(PropertyInfo viewModelProperty) throw new InvalidOperationException($"No perturbation defined for {property.Name} ({property.PropertyType})"); } + // Typed recursive leaf walk: a string/primitive/enum property is a LEAF compared at its dotted path; + // a record group RECURSES. Keeps root-level Orientation (an enum) a first-class leaf, so a BuildRecord + // bug that also flips Orientation cannot slip the exact-one-leaf perturbation assertion. private static IReadOnlyList ChangedRecordFields(GridStyleOptions baseline, GridStyleOptions candidate) { - return typeof(GridStyleOptions).GetProperties() - .Where(property => !Equals(property.GetValue(baseline), property.GetValue(candidate))) - .Select(property => property.Name) - .ToList(); + var changed = new List(); + CollectChangedLeaves(baseline, candidate, prefix: null, changed); + return changed; + } + + private static void CollectChangedLeaves(object baseline, object candidate, string? prefix, List changed) + { + // Instance-only: the static GridStyleOptions.Default property would otherwise recurse into itself. + foreach (var property in baseline.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance)) + { + var baselineValue = property.GetValue(baseline); + var candidateValue = property.GetValue(candidate); + var path = prefix is null ? property.Name : $"{prefix}.{property.Name}"; + + if (IsLeaf(property.PropertyType)) + { + if (!Equals(baselineValue, candidateValue)) + { + changed.Add(path); + } + } + else + { + CollectChangedLeaves(baselineValue!, candidateValue!, path, changed); + } + } + } + + private static bool IsLeaf(Type type) + { + return type.IsPrimitive || type.IsEnum || type == typeof(string); } private static async Task ExecuteSwallowing(ReactiveCommand command) diff --git a/SemiStep/SemiStep.Tests/UI/StyleEditor/GridStyleEditorWindowTests.cs b/SemiStep/SemiStep.Tests/UI/StyleEditor/GridStyleEditorWindowTests.cs index 2fe116d0..2092f232 100644 --- a/SemiStep/SemiStep.Tests/UI/StyleEditor/GridStyleEditorWindowTests.cs +++ b/SemiStep/SemiStep.Tests/UI/StyleEditor/GridStyleEditorWindowTests.cs @@ -37,7 +37,7 @@ public async Task Save_AfterEditingColorAndNumeric_PersistsToYaml_AndReloads() NullLogger.Instance); viewModel.SelectionBackground = Color.Parse("#123456"); - viewModel.CellFontSize = loaded.CellFontSize + 1; + viewModel.CellFontSize = loaded.Fonts.CellFontSize + 1; viewModel.CanSave.Should().BeTrue(); var saved = await viewModel.SaveCommand.Execute(); @@ -46,8 +46,8 @@ public async Task Save_AfterEditingColorAndNumeric_PersistsToYaml_AndReloads() viewModel.ErrorMessage.Should().BeNull(); var reloaded = (await facade.Load(configDir)).Value; - reloaded.SelectionBackgroundColor.Should().Be("#123456"); - reloaded.CellFontSize.Should().Be(loaded.CellFontSize + 1); + reloaded.Selection.Background.Should().Be("#123456"); + reloaded.Fonts.CellFontSize.Should().Be(loaded.Fonts.CellFontSize + 1); } [AvaloniaFact] diff --git a/SemiStep/SemiStep.Tests/UI/Styles/CellPaletteInstallerTests.cs b/SemiStep/SemiStep.Tests/UI/Styles/CellPaletteInstallerTests.cs index 5834092f..4c316c46 100644 --- a/SemiStep/SemiStep.Tests/UI/Styles/CellPaletteInstallerTests.cs +++ b/SemiStep/SemiStep.Tests/UI/Styles/CellPaletteInstallerTests.cs @@ -19,66 +19,77 @@ public sealed class CellPaletteInstallerTests [AvaloniaFact] public void Install_PopulatesAllCellBrushes_WithExpectedColors() { - var gridStyle = GridStyleOptions.Default with + var defaults = GridStyleOptions.Default; + var gridStyle = defaults with { - ReadOnlyCellDepth0Color = "#D8D8D8", - ReadOnlyCellDepth1Color = "#CCD5E0", - ReadOnlyCellDepth2Color = "#B8C3D1", - ReadOnlyCellDepth3Color = "#94A2B3", - ReadOnlyCellDepth0PastColor = "#C8C8C8", - ReadOnlyCellDepth1PastColor = "#BCC4CE", - ReadOnlyCellDepth2PastColor = "#ACB7C2", - ReadOnlyCellDepth3PastColor = "#8590A0", - ReadOnlyCellSelectedColor = "#6B95C0", - ReadOnlyCellForegroundColor = "#606060", - DisabledCellDepth0Color = "#E0E0E0", - DisabledCellDepth1Color = "#D5DEEA", - DisabledCellDepth2Color = "#C2CEDB", - DisabledCellDepth3Color = "#9DABBC", - DisabledCellDepth0PastColor = "#D0D0D0", - DisabledCellDepth1PastColor = "#C5CDD8", - DisabledCellDepth2PastColor = "#B5C0CC", - DisabledCellDepth3PastColor = "#909AAA", - DisabledCellSelectedColor = "#89B4D7", - DisabledCellForegroundColor = "#808080", - SelectionBackgroundColor = "#CCE4F7", - SelectionForegroundColor = "#202020", - CellChangedColor = "#FFCC80", - CellChangedSelectedColor = "#EFD3A4", - GridLineColor = "#CCCCCC", - StatusBarBackgroundColor = "#F0F0F0", - StatusBarForegroundColor = "#111111", - StatusBarPadding = 5, - StatusBarItemSpacing = 10, - FontFamily = "Arial", - StatusBarFontSize = 14, - StatusBarFontWeight = 700, - StatusBarItalic = true, - StatusBarTimerLabelFontSize = 16, - StatusBarTimerLabelFontWeight = 500, - StatusBarTimerLabelItalic = true, - StatusBarTimerValueFontSize = 28, - StatusBarTimerValueFontWeight = 600, - StatusBarTimerValueItalic = false, - CellFontSize = 12, - ValidationPanelBackgroundColor = "#FBFBFB", - ValidationPanelForegroundColor = "#222222", - ValidationPanelErrorColor = "#D32F2F", - ValidationPanelWarningColor = "#F57C00", - ValidationPanelMaxHeight = 100, - InfoColor = "#1976D2", - ConnectedColor = "#44BB44", - DisconnectedColor = "#FF4444", - LocalModeColor = "#6C707E", - ConnectingColor = "#FFAF0F", - PanelBackgroundColor = "#F8F8F8", - PanelHeaderBackgroundColor = "#EEEEEE", - SubtleBorderColor = "#D0D0D0", - SeparatorColor = "#C0C0C0", - SecondaryForegroundColor = "#888888", - GridBorderColor = "#808080", - GridBackgroundColor = "#FFFFFF", - HeaderForegroundColor = "#000000", + Fonts = defaults.Fonts with + { + FontFamily = "Arial", + CellFontSize = 12, + }, + Selection = new SelectionColors( + Background: "#CCE4F7", + Foreground: "#202020"), + ChangedCells = new ChangedCellColors( + Changed: "#FFCC80", + ChangedSelected: "#EFD3A4"), + ReadOnlyCells = new DepthPalette( + Depth0: "#D8D8D8", + Depth1: "#CCD5E0", + Depth2: "#B8C3D1", + Depth3: "#94A2B3", + Depth0Past: "#C8C8C8", + Depth1Past: "#BCC4CE", + Depth2Past: "#ACB7C2", + Depth3Past: "#8590A0", + Selected: "#6B95C0", + Foreground: "#606060"), + DisabledCells = new DepthPalette( + Depth0: "#E0E0E0", + Depth1: "#D5DEEA", + Depth2: "#C2CEDB", + Depth3: "#9DABBC", + Depth0Past: "#D0D0D0", + Depth1Past: "#C5CDD8", + Depth2Past: "#B5C0CC", + Depth3Past: "#909AAA", + Selected: "#89B4D7", + Foreground: "#808080"), + StatusBar = new StatusBarStyle( + Background: "#F0F0F0", + Foreground: "#111111", + Padding: 5, + ItemSpacing: 10, + FontSize: 14, + Weight: 700, + Italic: true, + TimerLabelFontSize: 16, + TimerLabelWeight: 500, + TimerLabelItalic: true, + TimerValueFontSize: 28, + TimerValueWeight: 600, + TimerValueItalic: false), + ValidationPanel = new ValidationPanelStyle( + Background: "#FBFBFB", + Foreground: "#222222", + ErrorColor: "#D32F2F", + WarningColor: "#F57C00", + MaxHeight: 100), + Chrome = new ChromeColors( + Info: "#1976D2", + Connected: "#44BB44", + Disconnected: "#FF4444", + LocalMode: "#6C707E", + Connecting: "#FFAF0F", + PanelBackground: "#F8F8F8", + PanelHeaderBackground: "#EEEEEE", + SubtleBorder: "#D0D0D0", + Separator: "#C0C0C0", + SecondaryForeground: "#888888", + GridBorder: "#808080", + GridBackground: "#FFFFFF", + HeaderForeground: "#000000", + GridLine: "#CCCCCC"), }; var resources = new ResourceDictionary(); @@ -133,7 +144,7 @@ public void Install_PopulatesAllCellBrushes_WithExpectedColors() resources[CellPaletteInstaller.StatusBarItemSpacingKey].Should().Be(10d); resources[CellPaletteInstaller.StatusBarFontSizeKey].Should().Be(14d); resources[CellPaletteInstaller.ValidationPanelMaxHeightKey].Should().Be(100d); - resources[CellPaletteInstaller.RowHeightKey].Should().Be(gridStyle.RowHeight); + resources[CellPaletteInstaller.RowHeightKey].Should().Be(gridStyle.Layout.RowHeight); resources[CellPaletteInstaller.AppFontFamilyKey].Should().BeOfType() .Which.Name.Should().Be("Arial"); @@ -154,7 +165,8 @@ public void Install_PopulatesAllCellBrushes_WithExpectedColors() [AvaloniaFact] public void Install_EmptyFontFamily_InstallsThemeDefaultFamily() { - var gridStyle = GridStyleOptions.Default with { FontFamily = "" }; + var defaults = GridStyleOptions.Default; + var gridStyle = defaults with { Fonts = defaults.Fonts with { FontFamily = "" } }; var resources = new ResourceDictionary(); CellPaletteInstaller.Install(resources, gridStyle); diff --git a/SemiStep/SemiStep.Tests/UI/Styles/ExecutionPaletteInstallerTests.cs b/SemiStep/SemiStep.Tests/UI/Styles/ExecutionPaletteInstallerTests.cs index 7247e03a..15a6727e 100644 --- a/SemiStep/SemiStep.Tests/UI/Styles/ExecutionPaletteInstallerTests.cs +++ b/SemiStep/SemiStep.Tests/UI/Styles/ExecutionPaletteInstallerTests.cs @@ -20,15 +20,16 @@ public void Install_PopulatesNineBrushKeys_WithExpectedColors() { var gridStyle = GridStyleOptions.Default with { - ExecutionDepth0Color = "#FFFFFF", - ExecutionDepth1Color = "#E8F3FF", - ExecutionDepth2Color = "#D0E7FF", - ExecutionDepth3Color = "#A8D0FF", - ExecutionDepth0PastColor = "#F0F0F0", - ExecutionDepth1PastColor = "#DCE5EE", - ExecutionDepth2PastColor = "#C4D2E0", - ExecutionDepth3PastColor = "#9CB4CC", - ExecutionCurrentStepMarkerColor = "#FF8800", + Execution = new ExecutionPalette( + Depth0: "#FFFFFF", + Depth1: "#E8F3FF", + Depth2: "#D0E7FF", + Depth3: "#A8D0FF", + Depth0Past: "#F0F0F0", + Depth1Past: "#DCE5EE", + Depth2Past: "#C4D2E0", + Depth3Past: "#9CB4CC", + CurrentStepMarker: "#FF8800"), }; var resources = new ResourceDictionary(); diff --git a/SemiStep/SemiStep.UI/RecipeGrid/ColumnBuilder.cs b/SemiStep/SemiStep.UI/RecipeGrid/ColumnBuilder.cs index 08f53efe..e9c5dd28 100644 --- a/SemiStep/SemiStep.UI/RecipeGrid/ColumnBuilder.cs +++ b/SemiStep/SemiStep.UI/RecipeGrid/ColumnBuilder.cs @@ -32,7 +32,7 @@ public sealed class ColumnBuilder( public void BuildColumns(DataGrid grid) { - grid.RowHeight = gridStyle.RowHeight; + grid.RowHeight = gridStyle.Layout.RowHeight; // The numbering column is a DataGridTextColumn with no FontFeatures property; tabular figures // reach its generated cells through the inherited grid-level value set here. grid.SetValue(TextElement.FontFeaturesProperty, GridFonts.TabularFigures); @@ -53,17 +53,17 @@ private void AddNumberingColumn(DataGrid grid) Header = "No", HeaderTemplate = _wrappingHeaderTemplate, Binding = new Binding("StepNumber"), - FontSize = gridStyle.CellFontSize, - FontWeight = (FontWeight)gridStyle.CellFontWeight, - FontStyle = gridStyle.CellItalic ? FontStyle.Italic : FontStyle.Normal, + FontSize = gridStyle.Fonts.CellFontSize, + FontWeight = (FontWeight)gridStyle.Fonts.CellFontWeight, + FontStyle = gridStyle.Fonts.CellItalic ? FontStyle.Italic : FontStyle.Normal, IsReadOnly = true, Width = DataGridLength.Auto, MinWidth = _widthCalculator.MinColumnWidth, CanUserSort = false }; - column.FontFamily = string.IsNullOrWhiteSpace(gridStyle.FontFamily) + column.FontFamily = string.IsNullOrWhiteSpace(gridStyle.Fonts.FontFamily) ? GridFonts.DefaultFamily - : new FontFamily(gridStyle.FontFamily); + : new FontFamily(gridStyle.Fonts.FontFamily); column.CellStyleClasses.Add(StepNumberColumnClass); grid.Columns.Add(column); diff --git a/SemiStep/SemiStep.UI/RecipeGrid/ColumnWidthCalculator.cs b/SemiStep/SemiStep.UI/RecipeGrid/ColumnWidthCalculator.cs index dead1df9..68d4c252 100644 --- a/SemiStep/SemiStep.UI/RecipeGrid/ColumnWidthCalculator.cs +++ b/SemiStep/SemiStep.UI/RecipeGrid/ColumnWidthCalculator.cs @@ -29,12 +29,12 @@ public sealed class ColumnWidthCalculator( private const int ComboBoxChromeWidth = 38; // See Docs/architecture/recipe-grid-column-sizing.md - private int ContentChrome => (int)Math.Ceiling(gridStyle.CellFontSize * ChromeFontMultiple); + private int ContentChrome => (int)Math.Ceiling(gridStyle.Fonts.CellFontSize * ChromeFontMultiple); - private int HeaderFloorChrome => (int)Math.Ceiling(gridStyle.HeaderFontSize * ChromeFontMultiple); + private int HeaderFloorChrome => (int)Math.Ceiling(gridStyle.Fonts.HeaderFontSize * ChromeFontMultiple); // The numbering column (ColumnBuilder) uses it as its MinWidth, so do not lower it. - public int MinColumnWidth => (int)Math.Ceiling(gridStyle.CellFontSize * MinColumnWidthEms); + public int MinColumnWidth => (int)Math.Ceiling(gridStyle.Fonts.CellFontSize * MinColumnWidthEms); public DataGridLength CalculateColumnWidth(GridColumnDefinition columnDef) { @@ -198,24 +198,24 @@ private IEnumerable CollectGroupDisplayStrings(string columnKey) // Empty family means "grid default": measure with the chrome font so the width matches the // rendered cell, which also falls back to the chrome font. private FontFamily MeasureFontFamily => - string.IsNullOrWhiteSpace(gridStyle.FontFamily) ? GridFonts.DefaultFamily : new FontFamily(gridStyle.FontFamily); + string.IsNullOrWhiteSpace(gridStyle.Fonts.FontFamily) ? GridFonts.DefaultFamily : new FontFamily(gridStyle.Fonts.FontFamily); // The single typeface the cell-role measurement uses, threading the configured family, weight, and // italic so the measured width tracks the rendered cell. Exposed for the measurement-threading test. internal Typeface CellTypeface => - new(MeasureFontFamily, gridStyle.CellItalic ? FontStyle.Italic : FontStyle.Normal, (FontWeight)gridStyle.CellFontWeight); + new(MeasureFontFamily, gridStyle.Fonts.CellItalic ? FontStyle.Italic : FontStyle.Normal, (FontWeight)gridStyle.Fonts.CellFontWeight); internal Typeface HeaderTypeface => - new(MeasureFontFamily, gridStyle.HeaderItalic ? FontStyle.Italic : FontStyle.Normal, (FontWeight)gridStyle.HeaderFontWeight); + new(MeasureFontFamily, gridStyle.Fonts.HeaderItalic ? FontStyle.Italic : FontStyle.Normal, (FontWeight)gridStyle.Fonts.HeaderFontWeight); private double MeasureContent(string text) { - return MeasureText(text, CellTypeface, gridStyle.CellFontSize); + return MeasureText(text, CellTypeface, gridStyle.Fonts.CellFontSize); } private double MeasureHeaderWord(string word) { - return MeasureText(word, HeaderTypeface, gridStyle.HeaderFontSize); + return MeasureText(word, HeaderTypeface, gridStyle.Fonts.HeaderFontSize); } private static double MeasureText(string text, Typeface typeface, double fontSize) diff --git a/SemiStep/SemiStep.UI/RecipeGrid/GridFontApplier.cs b/SemiStep/SemiStep.UI/RecipeGrid/GridFontApplier.cs index ac950815..9fe5d619 100644 --- a/SemiStep/SemiStep.UI/RecipeGrid/GridFontApplier.cs +++ b/SemiStep/SemiStep.UI/RecipeGrid/GridFontApplier.cs @@ -13,12 +13,12 @@ internal static class GridFontApplier { public static void ApplyHeaderFont(Control control, GridStyleOptions gridStyle) { - Apply(control, gridStyle.FontFamily, gridStyle.HeaderFontSize, gridStyle.HeaderFontWeight, gridStyle.HeaderItalic); + Apply(control, gridStyle.Fonts.FontFamily, gridStyle.Fonts.HeaderFontSize, gridStyle.Fonts.HeaderFontWeight, gridStyle.Fonts.HeaderItalic); } public static void ApplyCellFont(Control control, GridStyleOptions gridStyle) { - Apply(control, gridStyle.FontFamily, gridStyle.CellFontSize, gridStyle.CellFontWeight, gridStyle.CellItalic); + Apply(control, gridStyle.Fonts.FontFamily, gridStyle.Fonts.CellFontSize, gridStyle.Fonts.CellFontWeight, gridStyle.Fonts.CellItalic); } private static void Apply(Control control, string fontFamily, int fontSize, int fontWeight, bool italic) diff --git a/SemiStep/SemiStep.UI/RecipeGrid/TextCellFactory.cs b/SemiStep/SemiStep.UI/RecipeGrid/TextCellFactory.cs index 53286f74..664bab33 100644 --- a/SemiStep/SemiStep.UI/RecipeGrid/TextCellFactory.cs +++ b/SemiStep/SemiStep.UI/RecipeGrid/TextCellFactory.cs @@ -51,10 +51,10 @@ private FuncDataTemplate CreateStepStartTimeTemplate() { VerticalAlignment = VerticalAlignment.Center, Padding = new Thickness( - gridStyle.CellPaddingLeft, - gridStyle.CellPaddingTop, - gridStyle.CellPaddingRight, - gridStyle.CellPaddingBottom), + gridStyle.Layout.CellPaddingLeft, + gridStyle.Layout.CellPaddingTop, + gridStyle.Layout.CellPaddingRight, + gridStyle.Layout.CellPaddingBottom), TextAlignment = TextAlignment.Center, }; GridFontApplier.ApplyCellFont(textBlock, gridStyle); @@ -81,10 +81,10 @@ private FuncDataTemplate CreateMultiBindingTemplate(string c { VerticalAlignment = VerticalAlignment.Center, Padding = new Thickness( - gridStyle.CellPaddingLeft, - gridStyle.CellPaddingTop, - gridStyle.CellPaddingRight, - gridStyle.CellPaddingBottom), + gridStyle.Layout.CellPaddingLeft, + gridStyle.Layout.CellPaddingTop, + gridStyle.Layout.CellPaddingRight, + gridStyle.Layout.CellPaddingBottom), TextAlignment = TextAlignment.Center, }; GridFontApplier.ApplyCellFont(textBlock, gridStyle); @@ -133,10 +133,10 @@ public FuncDataTemplate CreateEditingTemplate(string columnK MinHeight = 0, BorderThickness = new Thickness(0), Padding = new Thickness( - gridStyle.CellPaddingLeft, - gridStyle.CellPaddingTop, - gridStyle.CellPaddingRight, - gridStyle.CellPaddingBottom), + gridStyle.Layout.CellPaddingLeft, + gridStyle.Layout.CellPaddingTop, + gridStyle.Layout.CellPaddingRight, + gridStyle.Layout.CellPaddingBottom), TextAlignment = TextAlignment.Center, }; GridFontApplier.ApplyCellFont(textBox, gridStyle); diff --git a/SemiStep/SemiStep.UI/RecipeGrid/Transposed/TransposedCellTemplateFactory.cs b/SemiStep/SemiStep.UI/RecipeGrid/Transposed/TransposedCellTemplateFactory.cs index f1fc4c49..a5859287 100644 --- a/SemiStep/SemiStep.UI/RecipeGrid/Transposed/TransposedCellTemplateFactory.cs +++ b/SemiStep/SemiStep.UI/RecipeGrid/Transposed/TransposedCellTemplateFactory.cs @@ -107,10 +107,10 @@ private TextBlock CreateDisplayTextBlock(bool stretch) { VerticalAlignment = VerticalAlignment.Center, Padding = new Thickness( - gridStyle.CellPaddingLeft, - gridStyle.CellPaddingTop, - gridStyle.CellPaddingRight, - gridStyle.CellPaddingBottom), + gridStyle.Layout.CellPaddingLeft, + gridStyle.Layout.CellPaddingTop, + gridStyle.Layout.CellPaddingRight, + gridStyle.Layout.CellPaddingBottom), TextAlignment = TextAlignment.Center, }; if (stretch) @@ -196,10 +196,10 @@ private Control CreateTextBoxEditor() BorderThickness = new Thickness(0), Background = Brushes.Transparent, Padding = new Thickness( - gridStyle.CellPaddingLeft, - gridStyle.CellPaddingTop, - gridStyle.CellPaddingRight, - gridStyle.CellPaddingBottom), + gridStyle.Layout.CellPaddingLeft, + gridStyle.Layout.CellPaddingTop, + gridStyle.Layout.CellPaddingRight, + gridStyle.Layout.CellPaddingBottom), TextAlignment = TextAlignment.Center, }; GridFontApplier.ApplyCellFont(textBox, gridStyle); diff --git a/SemiStep/SemiStep.UI/RecipeGrid/Transposed/TransposedRecipeGridView.axaml.cs b/SemiStep/SemiStep.UI/RecipeGrid/Transposed/TransposedRecipeGridView.axaml.cs index 11f87f46..40e012a1 100644 --- a/SemiStep/SemiStep.UI/RecipeGrid/Transposed/TransposedRecipeGridView.axaml.cs +++ b/SemiStep/SemiStep.UI/RecipeGrid/Transposed/TransposedRecipeGridView.axaml.cs @@ -328,15 +328,15 @@ private void BuildColumnCellsPool(TransposedRecipeGridSurface? surface) : new TransposedColumnCellsPool( surface.ParameterDescriptors, new TransposedCellTemplateFactory(surface, _editCoordinator), - surface.GridStyle.RowHeight); + surface.GridStyle.Layout.RowHeight); } private void ApplyGridStyle(GridStyleOptions gridStyle) { - Resources[CellHeightResourceKey] = gridStyle.RowHeight; + Resources[CellHeightResourceKey] = gridStyle.Layout.RowHeight; Resources[MarkerSlotHeightResourceKey] = CurrentStepMarkerHeight; Resources[StepColumnWidthResourceKey] = - Math.Max(MinimumStepColumnWidth, gridStyle.CellFontSize * FontSizeToColumnWidthFactor); + Math.Max(MinimumStepColumnWidth, gridStyle.Fonts.CellFontSize * FontSizeToColumnWidthFactor); GridFontApplier.ApplyCellFont(StepListBox, gridStyle); GridFontApplier.ApplyHeaderFont(ParameterNameColumn, gridStyle); diff --git a/SemiStep/SemiStep.UI/StyleEditor/GridStyleEditorViewModel.cs b/SemiStep/SemiStep.UI/StyleEditor/GridStyleEditorViewModel.cs index 88b21040..ec9ae424 100644 --- a/SemiStep/SemiStep.UI/StyleEditor/GridStyleEditorViewModel.cs +++ b/SemiStep/SemiStep.UI/StyleEditor/GridStyleEditorViewModel.cs @@ -20,8 +20,9 @@ namespace SemiStep.UI.StyleEditor; /// loaded record (a separate mutable copy — never the DI singleton), it exposes colors as /// and sizes as ? for two-way binding. /// gates on the Core color validator AND VM-side numeric range checks (the Core validator is -/// colors-only). The editor surfaces effectively the whole record; rebuilds -/// it via with over the seeded source, so any field that is not surfaced is preserved. +/// colors-only). The editor surfaces effectively the whole record; constructs +/// a new record positionally, carrying the one unsurfaced field +/// straight from _source.Orientation. /// public sealed class GridStyleEditorViewModel : ReactiveObject { @@ -202,86 +203,95 @@ public async Task LoadAsync() public GridStyleOptions BuildRecord() { - return _source with - { - HeaderFontSize = ToInt(HeaderFontSize, _source.HeaderFontSize), - CellFontSize = ToInt(CellFontSize, _source.CellFontSize), - CellPaddingLeft = ToDouble(CellPaddingLeft, _source.CellPaddingLeft), - CellPaddingTop = ToDouble(CellPaddingTop, _source.CellPaddingTop), - CellPaddingRight = ToDouble(CellPaddingRight, _source.CellPaddingRight), - CellPaddingBottom = ToDouble(CellPaddingBottom, _source.CellPaddingBottom), - RowHeight = ToDouble(RowHeight, _source.RowHeight), - StatusBarPadding = ToDouble(StatusBarPadding, _source.StatusBarPadding), - StatusBarItemSpacing = ToDouble(StatusBarItemSpacing, _source.StatusBarItemSpacing), - StatusBarFontSize = ToInt(StatusBarFontSize, _source.StatusBarFontSize), - StatusBarTimerLabelFontSize = ToInt(StatusBarTimerLabelFontSize, _source.StatusBarTimerLabelFontSize), - StatusBarTimerValueFontSize = ToInt(StatusBarTimerValueFontSize, _source.StatusBarTimerValueFontSize), - ValidationPanelMaxHeight = ToDouble(ValidationPanelMaxHeight, _source.ValidationPanelMaxHeight), - FontFamily = FontFamily ?? _source.FontFamily, - HeaderFontWeight = HeaderFontWeight, - HeaderItalic = HeaderItalic, - CellFontWeight = CellFontWeight, - CellItalic = CellItalic, - StatusBarFontWeight = StatusBarFontWeight, - StatusBarItalic = StatusBarItalic, - StatusBarTimerLabelFontWeight = StatusBarTimerLabelFontWeight, - StatusBarTimerLabelItalic = StatusBarTimerLabelItalic, - StatusBarTimerValueFontWeight = StatusBarTimerValueFontWeight, - StatusBarTimerValueItalic = StatusBarTimerValueItalic, - SelectionBackgroundColor = HexColor.ToHex(SelectionBackground), - SelectionForegroundColor = HexColor.ToHex(SelectionForeground), - CellChangedColor = HexColor.ToHex(CellChanged), - CellChangedSelectedColor = HexColor.ToHex(CellChangedSelected), - DisabledCellDepth0Color = HexColor.ToHex(DisabledCellDepth0), - DisabledCellDepth1Color = HexColor.ToHex(DisabledCellDepth1), - DisabledCellDepth2Color = HexColor.ToHex(DisabledCellDepth2), - DisabledCellDepth3Color = HexColor.ToHex(DisabledCellDepth3), - DisabledCellDepth0PastColor = HexColor.ToHex(DisabledCellDepth0Past), - DisabledCellDepth1PastColor = HexColor.ToHex(DisabledCellDepth1Past), - DisabledCellDepth2PastColor = HexColor.ToHex(DisabledCellDepth2Past), - DisabledCellDepth3PastColor = HexColor.ToHex(DisabledCellDepth3Past), - DisabledCellSelectedColor = HexColor.ToHex(DisabledCellSelected), - DisabledCellForegroundColor = HexColor.ToHex(DisabledCellForeground), - ReadOnlyCellDepth0Color = HexColor.ToHex(ReadOnlyCellDepth0), - ReadOnlyCellDepth1Color = HexColor.ToHex(ReadOnlyCellDepth1), - ReadOnlyCellDepth2Color = HexColor.ToHex(ReadOnlyCellDepth2), - ReadOnlyCellDepth3Color = HexColor.ToHex(ReadOnlyCellDepth3), - ReadOnlyCellDepth0PastColor = HexColor.ToHex(ReadOnlyCellDepth0Past), - ReadOnlyCellDepth1PastColor = HexColor.ToHex(ReadOnlyCellDepth1Past), - ReadOnlyCellDepth2PastColor = HexColor.ToHex(ReadOnlyCellDepth2Past), - ReadOnlyCellDepth3PastColor = HexColor.ToHex(ReadOnlyCellDepth3Past), - ReadOnlyCellSelectedColor = HexColor.ToHex(ReadOnlyCellSelected), - ReadOnlyCellForegroundColor = HexColor.ToHex(ReadOnlyCellForeground), - GridLineColor = HexColor.ToHex(GridLine), - StatusBarBackgroundColor = HexColor.ToHex(StatusBarBackground), - StatusBarForegroundColor = HexColor.ToHex(StatusBarForeground), - ValidationPanelBackgroundColor = HexColor.ToHex(ValidationPanelBackground), - ValidationPanelForegroundColor = HexColor.ToHex(ValidationPanelForeground), - ValidationPanelErrorColor = HexColor.ToHex(ValidationPanelError), - ValidationPanelWarningColor = HexColor.ToHex(ValidationPanelWarning), - ExecutionDepth0Color = HexColor.ToHex(ExecutionDepth0), - ExecutionDepth1Color = HexColor.ToHex(ExecutionDepth1), - ExecutionDepth2Color = HexColor.ToHex(ExecutionDepth2), - ExecutionDepth3Color = HexColor.ToHex(ExecutionDepth3), - ExecutionDepth0PastColor = HexColor.ToHex(ExecutionDepth0Past), - ExecutionDepth1PastColor = HexColor.ToHex(ExecutionDepth1Past), - ExecutionDepth2PastColor = HexColor.ToHex(ExecutionDepth2Past), - ExecutionDepth3PastColor = HexColor.ToHex(ExecutionDepth3Past), - ExecutionCurrentStepMarkerColor = HexColor.ToHex(ExecutionCurrentStepMarker), - InfoColor = HexColor.ToHex(Info), - ConnectedColor = HexColor.ToHex(Connected), - DisconnectedColor = HexColor.ToHex(Disconnected), - LocalModeColor = HexColor.ToHex(LocalMode), - ConnectingColor = HexColor.ToHex(Connecting), - PanelBackgroundColor = HexColor.ToHex(PanelBackground), - PanelHeaderBackgroundColor = HexColor.ToHex(PanelHeaderBackground), - SubtleBorderColor = HexColor.ToHex(SubtleBorder), - SeparatorColor = HexColor.ToHex(Separator), - SecondaryForegroundColor = HexColor.ToHex(SecondaryForeground), - GridBorderColor = HexColor.ToHex(GridBorder), - GridBackgroundColor = HexColor.ToHex(GridBackground), - HeaderForegroundColor = HexColor.ToHex(HeaderForeground) - }; + return new GridStyleOptions( + Fonts: new GridStyleFonts( + FontFamily: FontFamily ?? _source.Fonts.FontFamily, + HeaderFontSize: ToInt(HeaderFontSize, _source.Fonts.HeaderFontSize), + HeaderFontWeight: HeaderFontWeight, + HeaderItalic: HeaderItalic, + CellFontSize: ToInt(CellFontSize, _source.Fonts.CellFontSize), + CellFontWeight: CellFontWeight, + CellItalic: CellItalic), + Layout: new GridStyleLayout( + CellPaddingLeft: ToDouble(CellPaddingLeft, _source.Layout.CellPaddingLeft), + CellPaddingTop: ToDouble(CellPaddingTop, _source.Layout.CellPaddingTop), + CellPaddingRight: ToDouble(CellPaddingRight, _source.Layout.CellPaddingRight), + CellPaddingBottom: ToDouble(CellPaddingBottom, _source.Layout.CellPaddingBottom), + RowHeight: ToDouble(RowHeight, _source.Layout.RowHeight)), + Selection: new SelectionColors( + Background: HexColor.ToHex(SelectionBackground), + Foreground: HexColor.ToHex(SelectionForeground)), + ChangedCells: new ChangedCellColors( + Changed: HexColor.ToHex(CellChanged), + ChangedSelected: HexColor.ToHex(CellChangedSelected)), + ReadOnlyCells: new DepthPalette( + Depth0: HexColor.ToHex(ReadOnlyCellDepth0), + Depth1: HexColor.ToHex(ReadOnlyCellDepth1), + Depth2: HexColor.ToHex(ReadOnlyCellDepth2), + Depth3: HexColor.ToHex(ReadOnlyCellDepth3), + Depth0Past: HexColor.ToHex(ReadOnlyCellDepth0Past), + Depth1Past: HexColor.ToHex(ReadOnlyCellDepth1Past), + Depth2Past: HexColor.ToHex(ReadOnlyCellDepth2Past), + Depth3Past: HexColor.ToHex(ReadOnlyCellDepth3Past), + Selected: HexColor.ToHex(ReadOnlyCellSelected), + Foreground: HexColor.ToHex(ReadOnlyCellForeground)), + DisabledCells: new DepthPalette( + Depth0: HexColor.ToHex(DisabledCellDepth0), + Depth1: HexColor.ToHex(DisabledCellDepth1), + Depth2: HexColor.ToHex(DisabledCellDepth2), + Depth3: HexColor.ToHex(DisabledCellDepth3), + Depth0Past: HexColor.ToHex(DisabledCellDepth0Past), + Depth1Past: HexColor.ToHex(DisabledCellDepth1Past), + Depth2Past: HexColor.ToHex(DisabledCellDepth2Past), + Depth3Past: HexColor.ToHex(DisabledCellDepth3Past), + Selected: HexColor.ToHex(DisabledCellSelected), + Foreground: HexColor.ToHex(DisabledCellForeground)), + Execution: new ExecutionPalette( + Depth0: HexColor.ToHex(ExecutionDepth0), + Depth1: HexColor.ToHex(ExecutionDepth1), + Depth2: HexColor.ToHex(ExecutionDepth2), + Depth3: HexColor.ToHex(ExecutionDepth3), + Depth0Past: HexColor.ToHex(ExecutionDepth0Past), + Depth1Past: HexColor.ToHex(ExecutionDepth1Past), + Depth2Past: HexColor.ToHex(ExecutionDepth2Past), + Depth3Past: HexColor.ToHex(ExecutionDepth3Past), + CurrentStepMarker: HexColor.ToHex(ExecutionCurrentStepMarker)), + StatusBar: new StatusBarStyle( + Background: HexColor.ToHex(StatusBarBackground), + Foreground: HexColor.ToHex(StatusBarForeground), + Padding: ToDouble(StatusBarPadding, _source.StatusBar.Padding), + ItemSpacing: ToDouble(StatusBarItemSpacing, _source.StatusBar.ItemSpacing), + FontSize: ToInt(StatusBarFontSize, _source.StatusBar.FontSize), + Weight: StatusBarFontWeight, + Italic: StatusBarItalic, + TimerLabelFontSize: ToInt(StatusBarTimerLabelFontSize, _source.StatusBar.TimerLabelFontSize), + TimerLabelWeight: StatusBarTimerLabelFontWeight, + TimerLabelItalic: StatusBarTimerLabelItalic, + TimerValueFontSize: ToInt(StatusBarTimerValueFontSize, _source.StatusBar.TimerValueFontSize), + TimerValueWeight: StatusBarTimerValueFontWeight, + TimerValueItalic: StatusBarTimerValueItalic), + ValidationPanel: new ValidationPanelStyle( + Background: HexColor.ToHex(ValidationPanelBackground), + Foreground: HexColor.ToHex(ValidationPanelForeground), + ErrorColor: HexColor.ToHex(ValidationPanelError), + WarningColor: HexColor.ToHex(ValidationPanelWarning), + MaxHeight: ToDouble(ValidationPanelMaxHeight, _source.ValidationPanel.MaxHeight)), + Chrome: new ChromeColors( + Info: HexColor.ToHex(Info), + Connected: HexColor.ToHex(Connected), + Disconnected: HexColor.ToHex(Disconnected), + LocalMode: HexColor.ToHex(LocalMode), + Connecting: HexColor.ToHex(Connecting), + PanelBackground: HexColor.ToHex(PanelBackground), + PanelHeaderBackground: HexColor.ToHex(PanelHeaderBackground), + SubtleBorder: HexColor.ToHex(SubtleBorder), + Separator: HexColor.ToHex(Separator), + SecondaryForeground: HexColor.ToHex(SecondaryForeground), + GridBorder: HexColor.ToHex(GridBorder), + GridBackground: HexColor.ToHex(GridBackground), + HeaderForeground: HexColor.ToHex(HeaderForeground), + GridLine: HexColor.ToHex(GridLine)), + Orientation: _source.Orientation); } private async Task SaveAsync() @@ -322,88 +332,88 @@ private void Seed(GridStyleOptions options) { _seeding = true; - HeaderFontSize = options.HeaderFontSize; - CellFontSize = options.CellFontSize; - CellPaddingLeft = (decimal)options.CellPaddingLeft; - CellPaddingTop = (decimal)options.CellPaddingTop; - CellPaddingRight = (decimal)options.CellPaddingRight; - CellPaddingBottom = (decimal)options.CellPaddingBottom; - RowHeight = (decimal)options.RowHeight; - StatusBarPadding = (decimal)options.StatusBarPadding; - StatusBarItemSpacing = (decimal)options.StatusBarItemSpacing; - StatusBarFontSize = options.StatusBarFontSize; - StatusBarTimerLabelFontSize = options.StatusBarTimerLabelFontSize; - StatusBarTimerValueFontSize = options.StatusBarTimerValueFontSize; - ValidationPanelMaxHeight = (decimal)options.ValidationPanelMaxHeight; - - FontFamily = options.FontFamily; - HeaderFontWeight = options.HeaderFontWeight; - HeaderItalic = options.HeaderItalic; - CellFontWeight = options.CellFontWeight; - CellItalic = options.CellItalic; - StatusBarFontWeight = options.StatusBarFontWeight; - StatusBarItalic = options.StatusBarItalic; - StatusBarTimerLabelFontWeight = options.StatusBarTimerLabelFontWeight; - StatusBarTimerLabelItalic = options.StatusBarTimerLabelItalic; - StatusBarTimerValueFontWeight = options.StatusBarTimerValueFontWeight; - StatusBarTimerValueItalic = options.StatusBarTimerValueItalic; - - AvailableFontFamilies = BuildFontFamilies(options.FontFamily); + HeaderFontSize = options.Fonts.HeaderFontSize; + CellFontSize = options.Fonts.CellFontSize; + CellPaddingLeft = (decimal)options.Layout.CellPaddingLeft; + CellPaddingTop = (decimal)options.Layout.CellPaddingTop; + CellPaddingRight = (decimal)options.Layout.CellPaddingRight; + CellPaddingBottom = (decimal)options.Layout.CellPaddingBottom; + RowHeight = (decimal)options.Layout.RowHeight; + StatusBarPadding = (decimal)options.StatusBar.Padding; + StatusBarItemSpacing = (decimal)options.StatusBar.ItemSpacing; + StatusBarFontSize = options.StatusBar.FontSize; + StatusBarTimerLabelFontSize = options.StatusBar.TimerLabelFontSize; + StatusBarTimerValueFontSize = options.StatusBar.TimerValueFontSize; + ValidationPanelMaxHeight = (decimal)options.ValidationPanel.MaxHeight; + + FontFamily = options.Fonts.FontFamily; + HeaderFontWeight = options.Fonts.HeaderFontWeight; + HeaderItalic = options.Fonts.HeaderItalic; + CellFontWeight = options.Fonts.CellFontWeight; + CellItalic = options.Fonts.CellItalic; + StatusBarFontWeight = options.StatusBar.Weight; + StatusBarItalic = options.StatusBar.Italic; + StatusBarTimerLabelFontWeight = options.StatusBar.TimerLabelWeight; + StatusBarTimerLabelItalic = options.StatusBar.TimerLabelItalic; + StatusBarTimerValueFontWeight = options.StatusBar.TimerValueWeight; + StatusBarTimerValueItalic = options.StatusBar.TimerValueItalic; + + AvailableFontFamilies = BuildFontFamilies(options.Fonts.FontFamily); AvailableFontWeights = BuildFontWeights(options); - SelectionBackground = HexColor.Parse(options.SelectionBackgroundColor); - SelectionForeground = HexColor.Parse(options.SelectionForegroundColor); - CellChanged = HexColor.Parse(options.CellChangedColor); - CellChangedSelected = HexColor.Parse(options.CellChangedSelectedColor); - DisabledCellDepth0 = HexColor.Parse(options.DisabledCellDepth0Color); - DisabledCellDepth1 = HexColor.Parse(options.DisabledCellDepth1Color); - DisabledCellDepth2 = HexColor.Parse(options.DisabledCellDepth2Color); - DisabledCellDepth3 = HexColor.Parse(options.DisabledCellDepth3Color); - DisabledCellDepth0Past = HexColor.Parse(options.DisabledCellDepth0PastColor); - DisabledCellDepth1Past = HexColor.Parse(options.DisabledCellDepth1PastColor); - DisabledCellDepth2Past = HexColor.Parse(options.DisabledCellDepth2PastColor); - DisabledCellDepth3Past = HexColor.Parse(options.DisabledCellDepth3PastColor); - DisabledCellSelected = HexColor.Parse(options.DisabledCellSelectedColor); - DisabledCellForeground = HexColor.Parse(options.DisabledCellForegroundColor); - ReadOnlyCellDepth0 = HexColor.Parse(options.ReadOnlyCellDepth0Color); - ReadOnlyCellDepth1 = HexColor.Parse(options.ReadOnlyCellDepth1Color); - ReadOnlyCellDepth2 = HexColor.Parse(options.ReadOnlyCellDepth2Color); - ReadOnlyCellDepth3 = HexColor.Parse(options.ReadOnlyCellDepth3Color); - ReadOnlyCellDepth0Past = HexColor.Parse(options.ReadOnlyCellDepth0PastColor); - ReadOnlyCellDepth1Past = HexColor.Parse(options.ReadOnlyCellDepth1PastColor); - ReadOnlyCellDepth2Past = HexColor.Parse(options.ReadOnlyCellDepth2PastColor); - ReadOnlyCellDepth3Past = HexColor.Parse(options.ReadOnlyCellDepth3PastColor); - ReadOnlyCellSelected = HexColor.Parse(options.ReadOnlyCellSelectedColor); - ReadOnlyCellForeground = HexColor.Parse(options.ReadOnlyCellForegroundColor); - GridLine = HexColor.Parse(options.GridLineColor); - StatusBarBackground = HexColor.Parse(options.StatusBarBackgroundColor); - StatusBarForeground = HexColor.Parse(options.StatusBarForegroundColor); - ValidationPanelBackground = HexColor.Parse(options.ValidationPanelBackgroundColor); - ValidationPanelForeground = HexColor.Parse(options.ValidationPanelForegroundColor); - ValidationPanelError = HexColor.Parse(options.ValidationPanelErrorColor); - ValidationPanelWarning = HexColor.Parse(options.ValidationPanelWarningColor); - ExecutionDepth0 = HexColor.Parse(options.ExecutionDepth0Color); - ExecutionDepth1 = HexColor.Parse(options.ExecutionDepth1Color); - ExecutionDepth2 = HexColor.Parse(options.ExecutionDepth2Color); - ExecutionDepth3 = HexColor.Parse(options.ExecutionDepth3Color); - ExecutionDepth0Past = HexColor.Parse(options.ExecutionDepth0PastColor); - ExecutionDepth1Past = HexColor.Parse(options.ExecutionDepth1PastColor); - ExecutionDepth2Past = HexColor.Parse(options.ExecutionDepth2PastColor); - ExecutionDepth3Past = HexColor.Parse(options.ExecutionDepth3PastColor); - ExecutionCurrentStepMarker = HexColor.Parse(options.ExecutionCurrentStepMarkerColor); - Info = HexColor.Parse(options.InfoColor); - Connected = HexColor.Parse(options.ConnectedColor); - Disconnected = HexColor.Parse(options.DisconnectedColor); - LocalMode = HexColor.Parse(options.LocalModeColor); - Connecting = HexColor.Parse(options.ConnectingColor); - PanelBackground = HexColor.Parse(options.PanelBackgroundColor); - PanelHeaderBackground = HexColor.Parse(options.PanelHeaderBackgroundColor); - SubtleBorder = HexColor.Parse(options.SubtleBorderColor); - Separator = HexColor.Parse(options.SeparatorColor); - SecondaryForeground = HexColor.Parse(options.SecondaryForegroundColor); - GridBorder = HexColor.Parse(options.GridBorderColor); - GridBackground = HexColor.Parse(options.GridBackgroundColor); - HeaderForeground = HexColor.Parse(options.HeaderForegroundColor); + SelectionBackground = HexColor.Parse(options.Selection.Background); + SelectionForeground = HexColor.Parse(options.Selection.Foreground); + CellChanged = HexColor.Parse(options.ChangedCells.Changed); + CellChangedSelected = HexColor.Parse(options.ChangedCells.ChangedSelected); + DisabledCellDepth0 = HexColor.Parse(options.DisabledCells.Depth0); + DisabledCellDepth1 = HexColor.Parse(options.DisabledCells.Depth1); + DisabledCellDepth2 = HexColor.Parse(options.DisabledCells.Depth2); + DisabledCellDepth3 = HexColor.Parse(options.DisabledCells.Depth3); + DisabledCellDepth0Past = HexColor.Parse(options.DisabledCells.Depth0Past); + DisabledCellDepth1Past = HexColor.Parse(options.DisabledCells.Depth1Past); + DisabledCellDepth2Past = HexColor.Parse(options.DisabledCells.Depth2Past); + DisabledCellDepth3Past = HexColor.Parse(options.DisabledCells.Depth3Past); + DisabledCellSelected = HexColor.Parse(options.DisabledCells.Selected); + DisabledCellForeground = HexColor.Parse(options.DisabledCells.Foreground); + ReadOnlyCellDepth0 = HexColor.Parse(options.ReadOnlyCells.Depth0); + ReadOnlyCellDepth1 = HexColor.Parse(options.ReadOnlyCells.Depth1); + ReadOnlyCellDepth2 = HexColor.Parse(options.ReadOnlyCells.Depth2); + ReadOnlyCellDepth3 = HexColor.Parse(options.ReadOnlyCells.Depth3); + ReadOnlyCellDepth0Past = HexColor.Parse(options.ReadOnlyCells.Depth0Past); + ReadOnlyCellDepth1Past = HexColor.Parse(options.ReadOnlyCells.Depth1Past); + ReadOnlyCellDepth2Past = HexColor.Parse(options.ReadOnlyCells.Depth2Past); + ReadOnlyCellDepth3Past = HexColor.Parse(options.ReadOnlyCells.Depth3Past); + ReadOnlyCellSelected = HexColor.Parse(options.ReadOnlyCells.Selected); + ReadOnlyCellForeground = HexColor.Parse(options.ReadOnlyCells.Foreground); + GridLine = HexColor.Parse(options.Chrome.GridLine); + StatusBarBackground = HexColor.Parse(options.StatusBar.Background); + StatusBarForeground = HexColor.Parse(options.StatusBar.Foreground); + ValidationPanelBackground = HexColor.Parse(options.ValidationPanel.Background); + ValidationPanelForeground = HexColor.Parse(options.ValidationPanel.Foreground); + ValidationPanelError = HexColor.Parse(options.ValidationPanel.ErrorColor); + ValidationPanelWarning = HexColor.Parse(options.ValidationPanel.WarningColor); + ExecutionDepth0 = HexColor.Parse(options.Execution.Depth0); + ExecutionDepth1 = HexColor.Parse(options.Execution.Depth1); + ExecutionDepth2 = HexColor.Parse(options.Execution.Depth2); + ExecutionDepth3 = HexColor.Parse(options.Execution.Depth3); + ExecutionDepth0Past = HexColor.Parse(options.Execution.Depth0Past); + ExecutionDepth1Past = HexColor.Parse(options.Execution.Depth1Past); + ExecutionDepth2Past = HexColor.Parse(options.Execution.Depth2Past); + ExecutionDepth3Past = HexColor.Parse(options.Execution.Depth3Past); + ExecutionCurrentStepMarker = HexColor.Parse(options.Execution.CurrentStepMarker); + Info = HexColor.Parse(options.Chrome.Info); + Connected = HexColor.Parse(options.Chrome.Connected); + Disconnected = HexColor.Parse(options.Chrome.Disconnected); + LocalMode = HexColor.Parse(options.Chrome.LocalMode); + Connecting = HexColor.Parse(options.Chrome.Connecting); + PanelBackground = HexColor.Parse(options.Chrome.PanelBackground); + PanelHeaderBackground = HexColor.Parse(options.Chrome.PanelHeaderBackground); + SubtleBorder = HexColor.Parse(options.Chrome.SubtleBorder); + Separator = HexColor.Parse(options.Chrome.Separator); + SecondaryForeground = HexColor.Parse(options.Chrome.SecondaryForeground); + GridBorder = HexColor.Parse(options.Chrome.GridBorder); + GridBackground = HexColor.Parse(options.Chrome.GridBackground); + HeaderForeground = HexColor.Parse(options.Chrome.HeaderForeground); _seeding = false; RecomputeCanSave(); @@ -461,11 +471,11 @@ private static IReadOnlyList BuildFontWeights(GridStyleOptions var seededWeights = new[] { - options.HeaderFontWeight, - options.CellFontWeight, - options.StatusBarFontWeight, - options.StatusBarTimerLabelFontWeight, - options.StatusBarTimerValueFontWeight + options.Fonts.HeaderFontWeight, + options.Fonts.CellFontWeight, + options.StatusBar.Weight, + options.StatusBar.TimerLabelWeight, + options.StatusBar.TimerValueWeight }; foreach (var weight in seededWeights.Distinct()) diff --git a/SemiStep/SemiStep.UI/Styles/CellPaletteInstaller.cs b/SemiStep/SemiStep.UI/Styles/CellPaletteInstaller.cs index fdc80bc4..fac74408 100644 --- a/SemiStep/SemiStep.UI/Styles/CellPaletteInstaller.cs +++ b/SemiStep/SemiStep.UI/Styles/CellPaletteInstaller.cs @@ -72,67 +72,67 @@ public static void Install(IResourceDictionary resources, GridStyleOptions gridS ArgumentNullException.ThrowIfNull(resources); ArgumentNullException.ThrowIfNull(gridStyle); - resources[CellReadOnlyDepth0BrushKey] = PaletteBrushFactory.From(gridStyle.ReadOnlyCellDepth0Color); - resources[CellReadOnlyDepth1BrushKey] = PaletteBrushFactory.From(gridStyle.ReadOnlyCellDepth1Color); - resources[CellReadOnlyDepth2BrushKey] = PaletteBrushFactory.From(gridStyle.ReadOnlyCellDepth2Color); - resources[CellReadOnlyDepth3BrushKey] = PaletteBrushFactory.From(gridStyle.ReadOnlyCellDepth3Color); - resources[CellReadOnlyDepth0PastBrushKey] = PaletteBrushFactory.From(gridStyle.ReadOnlyCellDepth0PastColor); - resources[CellReadOnlyDepth1PastBrushKey] = PaletteBrushFactory.From(gridStyle.ReadOnlyCellDepth1PastColor); - resources[CellReadOnlyDepth2PastBrushKey] = PaletteBrushFactory.From(gridStyle.ReadOnlyCellDepth2PastColor); - resources[CellReadOnlyDepth3PastBrushKey] = PaletteBrushFactory.From(gridStyle.ReadOnlyCellDepth3PastColor); - resources[CellReadOnlySelectedBackgroundBrushKey] = PaletteBrushFactory.From(gridStyle.ReadOnlyCellSelectedColor); - resources[CellReadOnlyForegroundBrushKey] = PaletteBrushFactory.From(gridStyle.ReadOnlyCellForegroundColor); - resources[CellDisabledDepth0BrushKey] = PaletteBrushFactory.From(gridStyle.DisabledCellDepth0Color); - resources[CellDisabledDepth1BrushKey] = PaletteBrushFactory.From(gridStyle.DisabledCellDepth1Color); - resources[CellDisabledDepth2BrushKey] = PaletteBrushFactory.From(gridStyle.DisabledCellDepth2Color); - resources[CellDisabledDepth3BrushKey] = PaletteBrushFactory.From(gridStyle.DisabledCellDepth3Color); - resources[CellDisabledDepth0PastBrushKey] = PaletteBrushFactory.From(gridStyle.DisabledCellDepth0PastColor); - resources[CellDisabledDepth1PastBrushKey] = PaletteBrushFactory.From(gridStyle.DisabledCellDepth1PastColor); - resources[CellDisabledDepth2PastBrushKey] = PaletteBrushFactory.From(gridStyle.DisabledCellDepth2PastColor); - resources[CellDisabledDepth3PastBrushKey] = PaletteBrushFactory.From(gridStyle.DisabledCellDepth3PastColor); - resources[CellDisabledSelectedBackgroundBrushKey] = PaletteBrushFactory.From(gridStyle.DisabledCellSelectedColor); - resources[CellDisabledForegroundBrushKey] = PaletteBrushFactory.From(gridStyle.DisabledCellForegroundColor); - resources[SelectionBackgroundBrushKey] = PaletteBrushFactory.From(gridStyle.SelectionBackgroundColor); - resources[SelectionForegroundBrushKey] = PaletteBrushFactory.From(gridStyle.SelectionForegroundColor); - resources[CellChangedBrushKey] = PaletteBrushFactory.From(gridStyle.CellChangedColor); + resources[CellReadOnlyDepth0BrushKey] = PaletteBrushFactory.From(gridStyle.ReadOnlyCells.Depth0); + resources[CellReadOnlyDepth1BrushKey] = PaletteBrushFactory.From(gridStyle.ReadOnlyCells.Depth1); + resources[CellReadOnlyDepth2BrushKey] = PaletteBrushFactory.From(gridStyle.ReadOnlyCells.Depth2); + resources[CellReadOnlyDepth3BrushKey] = PaletteBrushFactory.From(gridStyle.ReadOnlyCells.Depth3); + resources[CellReadOnlyDepth0PastBrushKey] = PaletteBrushFactory.From(gridStyle.ReadOnlyCells.Depth0Past); + resources[CellReadOnlyDepth1PastBrushKey] = PaletteBrushFactory.From(gridStyle.ReadOnlyCells.Depth1Past); + resources[CellReadOnlyDepth2PastBrushKey] = PaletteBrushFactory.From(gridStyle.ReadOnlyCells.Depth2Past); + resources[CellReadOnlyDepth3PastBrushKey] = PaletteBrushFactory.From(gridStyle.ReadOnlyCells.Depth3Past); + resources[CellReadOnlySelectedBackgroundBrushKey] = PaletteBrushFactory.From(gridStyle.ReadOnlyCells.Selected); + resources[CellReadOnlyForegroundBrushKey] = PaletteBrushFactory.From(gridStyle.ReadOnlyCells.Foreground); + resources[CellDisabledDepth0BrushKey] = PaletteBrushFactory.From(gridStyle.DisabledCells.Depth0); + resources[CellDisabledDepth1BrushKey] = PaletteBrushFactory.From(gridStyle.DisabledCells.Depth1); + resources[CellDisabledDepth2BrushKey] = PaletteBrushFactory.From(gridStyle.DisabledCells.Depth2); + resources[CellDisabledDepth3BrushKey] = PaletteBrushFactory.From(gridStyle.DisabledCells.Depth3); + resources[CellDisabledDepth0PastBrushKey] = PaletteBrushFactory.From(gridStyle.DisabledCells.Depth0Past); + resources[CellDisabledDepth1PastBrushKey] = PaletteBrushFactory.From(gridStyle.DisabledCells.Depth1Past); + resources[CellDisabledDepth2PastBrushKey] = PaletteBrushFactory.From(gridStyle.DisabledCells.Depth2Past); + resources[CellDisabledDepth3PastBrushKey] = PaletteBrushFactory.From(gridStyle.DisabledCells.Depth3Past); + resources[CellDisabledSelectedBackgroundBrushKey] = PaletteBrushFactory.From(gridStyle.DisabledCells.Selected); + resources[CellDisabledForegroundBrushKey] = PaletteBrushFactory.From(gridStyle.DisabledCells.Foreground); + resources[SelectionBackgroundBrushKey] = PaletteBrushFactory.From(gridStyle.Selection.Background); + resources[SelectionForegroundBrushKey] = PaletteBrushFactory.From(gridStyle.Selection.Foreground); + resources[CellChangedBrushKey] = PaletteBrushFactory.From(gridStyle.ChangedCells.Changed); resources[CellChangedSelectedBackgroundBrushKey] = - PaletteBrushFactory.From(gridStyle.CellChangedSelectedColor); - resources[GridLineBrushKey] = PaletteBrushFactory.From(gridStyle.GridLineColor); - resources[StatusBarBackgroundBrushKey] = PaletteBrushFactory.From(gridStyle.StatusBarBackgroundColor); - resources[StatusBarForegroundBrushKey] = PaletteBrushFactory.From(gridStyle.StatusBarForegroundColor); - resources[StatusBarPaddingKey] = new Thickness(gridStyle.StatusBarPadding); - resources[StatusBarItemSpacingKey] = gridStyle.StatusBarItemSpacing; - resources[AppFontFamilyKey] = ToFontFamily(gridStyle.FontFamily); - resources[StatusBarFontSizeKey] = (double)gridStyle.StatusBarFontSize; - resources[StatusBarFontWeightKey] = (FontWeight)gridStyle.StatusBarFontWeight; - resources[StatusBarFontStyleKey] = ToFontStyle(gridStyle.StatusBarItalic); - resources[StatusBarTimerLabelFontSizeKey] = (double)gridStyle.StatusBarTimerLabelFontSize; - resources[StatusBarTimerLabelFontWeightKey] = (FontWeight)gridStyle.StatusBarTimerLabelFontWeight; - resources[StatusBarTimerLabelFontStyleKey] = ToFontStyle(gridStyle.StatusBarTimerLabelItalic); - resources[StatusBarTimerValueFontSizeKey] = (double)gridStyle.StatusBarTimerValueFontSize; - resources[StatusBarTimerValueFontWeightKey] = (FontWeight)gridStyle.StatusBarTimerValueFontWeight; - resources[StatusBarTimerValueFontStyleKey] = ToFontStyle(gridStyle.StatusBarTimerValueItalic); - resources[ErrorBrushKey] = PaletteBrushFactory.From(gridStyle.ValidationPanelErrorColor); - resources[WarningBrushKey] = PaletteBrushFactory.From(gridStyle.ValidationPanelWarningColor); + PaletteBrushFactory.From(gridStyle.ChangedCells.ChangedSelected); + resources[GridLineBrushKey] = PaletteBrushFactory.From(gridStyle.Chrome.GridLine); + resources[StatusBarBackgroundBrushKey] = PaletteBrushFactory.From(gridStyle.StatusBar.Background); + resources[StatusBarForegroundBrushKey] = PaletteBrushFactory.From(gridStyle.StatusBar.Foreground); + resources[StatusBarPaddingKey] = new Thickness(gridStyle.StatusBar.Padding); + resources[StatusBarItemSpacingKey] = gridStyle.StatusBar.ItemSpacing; + resources[AppFontFamilyKey] = ToFontFamily(gridStyle.Fonts.FontFamily); + resources[StatusBarFontSizeKey] = (double)gridStyle.StatusBar.FontSize; + resources[StatusBarFontWeightKey] = (FontWeight)gridStyle.StatusBar.Weight; + resources[StatusBarFontStyleKey] = ToFontStyle(gridStyle.StatusBar.Italic); + resources[StatusBarTimerLabelFontSizeKey] = (double)gridStyle.StatusBar.TimerLabelFontSize; + resources[StatusBarTimerLabelFontWeightKey] = (FontWeight)gridStyle.StatusBar.TimerLabelWeight; + resources[StatusBarTimerLabelFontStyleKey] = ToFontStyle(gridStyle.StatusBar.TimerLabelItalic); + resources[StatusBarTimerValueFontSizeKey] = (double)gridStyle.StatusBar.TimerValueFontSize; + resources[StatusBarTimerValueFontWeightKey] = (FontWeight)gridStyle.StatusBar.TimerValueWeight; + resources[StatusBarTimerValueFontStyleKey] = ToFontStyle(gridStyle.StatusBar.TimerValueItalic); + resources[ErrorBrushKey] = PaletteBrushFactory.From(gridStyle.ValidationPanel.ErrorColor); + resources[WarningBrushKey] = PaletteBrushFactory.From(gridStyle.ValidationPanel.WarningColor); resources[ValidationPanelBackgroundBrushKey] = - PaletteBrushFactory.From(gridStyle.ValidationPanelBackgroundColor); + PaletteBrushFactory.From(gridStyle.ValidationPanel.Background); resources[ValidationPanelForegroundBrushKey] = - PaletteBrushFactory.From(gridStyle.ValidationPanelForegroundColor); - resources[ValidationPanelMaxHeightKey] = gridStyle.ValidationPanelMaxHeight; - resources[InfoBrushKey] = PaletteBrushFactory.From(gridStyle.InfoColor); - resources[ConnectedBrushKey] = PaletteBrushFactory.From(gridStyle.ConnectedColor); - resources[DisconnectedBrushKey] = PaletteBrushFactory.From(gridStyle.DisconnectedColor); - resources[LocalModeBrushKey] = PaletteBrushFactory.From(gridStyle.LocalModeColor); - resources[ConnectingBrushKey] = PaletteBrushFactory.From(gridStyle.ConnectingColor); - resources[PanelBackgroundBrushKey] = PaletteBrushFactory.From(gridStyle.PanelBackgroundColor); - resources[PanelHeaderBackgroundBrushKey] = PaletteBrushFactory.From(gridStyle.PanelHeaderBackgroundColor); - resources[SubtleBorderBrushKey] = PaletteBrushFactory.From(gridStyle.SubtleBorderColor); - resources[SeparatorBrushKey] = PaletteBrushFactory.From(gridStyle.SeparatorColor); - resources[SecondaryForegroundBrushKey] = PaletteBrushFactory.From(gridStyle.SecondaryForegroundColor); - resources[GridBorderBrushKey] = PaletteBrushFactory.From(gridStyle.GridBorderColor); - resources[GridBackgroundBrushKey] = PaletteBrushFactory.From(gridStyle.GridBackgroundColor); - resources[HeaderForegroundBrushKey] = PaletteBrushFactory.From(gridStyle.HeaderForegroundColor); - resources[RowHeightKey] = gridStyle.RowHeight; + PaletteBrushFactory.From(gridStyle.ValidationPanel.Foreground); + resources[ValidationPanelMaxHeightKey] = gridStyle.ValidationPanel.MaxHeight; + resources[InfoBrushKey] = PaletteBrushFactory.From(gridStyle.Chrome.Info); + resources[ConnectedBrushKey] = PaletteBrushFactory.From(gridStyle.Chrome.Connected); + resources[DisconnectedBrushKey] = PaletteBrushFactory.From(gridStyle.Chrome.Disconnected); + resources[LocalModeBrushKey] = PaletteBrushFactory.From(gridStyle.Chrome.LocalMode); + resources[ConnectingBrushKey] = PaletteBrushFactory.From(gridStyle.Chrome.Connecting); + resources[PanelBackgroundBrushKey] = PaletteBrushFactory.From(gridStyle.Chrome.PanelBackground); + resources[PanelHeaderBackgroundBrushKey] = PaletteBrushFactory.From(gridStyle.Chrome.PanelHeaderBackground); + resources[SubtleBorderBrushKey] = PaletteBrushFactory.From(gridStyle.Chrome.SubtleBorder); + resources[SeparatorBrushKey] = PaletteBrushFactory.From(gridStyle.Chrome.Separator); + resources[SecondaryForegroundBrushKey] = PaletteBrushFactory.From(gridStyle.Chrome.SecondaryForeground); + resources[GridBorderBrushKey] = PaletteBrushFactory.From(gridStyle.Chrome.GridBorder); + resources[GridBackgroundBrushKey] = PaletteBrushFactory.From(gridStyle.Chrome.GridBackground); + resources[HeaderForegroundBrushKey] = PaletteBrushFactory.From(gridStyle.Chrome.HeaderForeground); + resources[RowHeightKey] = gridStyle.Layout.RowHeight; } // An empty family means "theme default": FontFamily.Default keeps the application's default diff --git a/SemiStep/SemiStep.UI/Styles/ExecutionPaletteInstaller.cs b/SemiStep/SemiStep.UI/Styles/ExecutionPaletteInstaller.cs index e8f4f220..b3c124c3 100644 --- a/SemiStep/SemiStep.UI/Styles/ExecutionPaletteInstaller.cs +++ b/SemiStep/SemiStep.UI/Styles/ExecutionPaletteInstaller.cs @@ -22,14 +22,14 @@ public static void Install(IResourceDictionary resources, GridStyleOptions gridS ArgumentNullException.ThrowIfNull(gridStyle); // Depth0 brushes (the "outside any loop" tint) are installed but not yet consumed by any selector. - resources[ExecRowDepth0BrushKey] = PaletteBrushFactory.From(gridStyle.ExecutionDepth0Color); - resources[ExecRowDepth1BrushKey] = PaletteBrushFactory.From(gridStyle.ExecutionDepth1Color); - resources[ExecRowDepth2BrushKey] = PaletteBrushFactory.From(gridStyle.ExecutionDepth2Color); - resources[ExecRowDepth3BrushKey] = PaletteBrushFactory.From(gridStyle.ExecutionDepth3Color); - resources[ExecRowDepth0PastBrushKey] = PaletteBrushFactory.From(gridStyle.ExecutionDepth0PastColor); - resources[ExecRowDepth1PastBrushKey] = PaletteBrushFactory.From(gridStyle.ExecutionDepth1PastColor); - resources[ExecRowDepth2PastBrushKey] = PaletteBrushFactory.From(gridStyle.ExecutionDepth2PastColor); - resources[ExecRowDepth3PastBrushKey] = PaletteBrushFactory.From(gridStyle.ExecutionDepth3PastColor); - resources[CurrentStepMarkerBrushKey] = PaletteBrushFactory.From(gridStyle.ExecutionCurrentStepMarkerColor); + resources[ExecRowDepth0BrushKey] = PaletteBrushFactory.From(gridStyle.Execution.Depth0); + resources[ExecRowDepth1BrushKey] = PaletteBrushFactory.From(gridStyle.Execution.Depth1); + resources[ExecRowDepth2BrushKey] = PaletteBrushFactory.From(gridStyle.Execution.Depth2); + resources[ExecRowDepth3BrushKey] = PaletteBrushFactory.From(gridStyle.Execution.Depth3); + resources[ExecRowDepth0PastBrushKey] = PaletteBrushFactory.From(gridStyle.Execution.Depth0Past); + resources[ExecRowDepth1PastBrushKey] = PaletteBrushFactory.From(gridStyle.Execution.Depth1Past); + resources[ExecRowDepth2PastBrushKey] = PaletteBrushFactory.From(gridStyle.Execution.Depth2Past); + resources[ExecRowDepth3PastBrushKey] = PaletteBrushFactory.From(gridStyle.Execution.Depth3Past); + resources[CurrentStepMarkerBrushKey] = PaletteBrushFactory.From(gridStyle.Execution.CurrentStepMarker); } } diff --git a/docs/plans/completed/20260805-grid-style-nest-record.md b/docs/plans/completed/20260805-grid-style-nest-record.md new file mode 100644 index 00000000..39291ae6 --- /dev/null +++ b/docs/plans/completed/20260805-grid-style-nest-record.md @@ -0,0 +1,157 @@ +# Slice 3 — Nest GridStyleOptions to mirror the consumption groups + +## Overview + +Slice 3 of the #118 debloat, and the structural core: replace the flat, 78-arity `GridStyleOptions` record with a +one-level nested root composing ~10 per-group records. This is the big line-count win and the **only slice that ripples +the runtime consumers** — but every touch is a mechanical property-path change guarded by the compiler, and slice 1's +save→load + Seed/perturbation guards catch any dropped or cross-wired field. + +**The YAML format, the DTOs, and `GridStyleValidator` do not change.** The file on disk is byte-identical before and +after; the DTO layer keeps its nested snake_case shape and its per-key error reporting. Only the *record* the DTO maps +to/from changes shape, plus everything that reads that record. + +**Shape choices:** +- **One-level nesting** — ~10 group records directly on the root, matching how consumers read (short paths like + `gridStyle.ReadOnlyCells.Depth1`); the load mapper owns the small DTO-tree walk (`colors.cells.readonly` → the group). +- **Standalone `ExecutionPalette`** — one reusable `DepthPalette` for the two identical 10-field blocks (ReadOnly + + Disabled); Execution is its own record (8 depths + marker, no selected/foreground). + +**Colors stay `string` in this slice.** Typing them as `StyleColor` is slice 4 — doing it here would mean re-touching +all 53 in place. So the group records carry `string …Color` fields exactly as the flat record does today; only the +*grouping* changes. + +## The target shape + +Root record (one level, ~10 groups + orientation): + +```csharp +public sealed record GridStyleOptions( + GridStyleFonts Fonts, + GridStyleLayout Layout, + SelectionColors Selection, + ChangedCellColors ChangedCells, // colors.cells.changed / changed_selected + DepthPalette ReadOnlyCells, // colors.cells.readonly (10 fields) + DepthPalette DisabledCells, // colors.cells.disabled (10 fields, identical shape) + ExecutionPalette Execution, // colors.cells.execution (8 depths + marker) + StatusBarStyle StatusBar, + ValidationPanelStyle ValidationPanel, + ChromeColors Chrome, // 13 chrome + grid_line folded in (see note) + GridOrientation Orientation); // unsurfaced, carried through +``` + +Group records (all `public sealed record`, positional so a missed field is CS7036; Core, `SemiStep.Core.Configuration`; +new files, one per record; colors as `string`, sizes `int`/`double` as today): + +| Record | Fields | DTO source | +|---|---|---| +| `GridStyleFonts` | FontFamily, HeaderFontSize, HeaderFontWeight, HeaderItalic, CellFontSize, CellFontWeight, CellItalic (7) | `fonts` | +| `GridStyleLayout` | CellPaddingLeft/Top/Right/Bottom, RowHeight (5) | `layout` | +| `SelectionColors` | Background, Foreground (2) | `colors.selection` | +| `ChangedCellColors` | Changed, ChangedSelected (2) | `colors.cells.changed` / `changed_selected` | +| `DepthPalette` | Depth0/1/2/3, Depth0Past/1Past/2Past/3Past, Selected, Foreground (10) | `colors.cells.readonly` **and** `…disabled` (one type, used twice) | +| `ExecutionPalette` | Depth0/1/2/3, Depth0Past/1Past/2Past/3Past, CurrentStepMarker (9) | `colors.cells.execution` | +| `StatusBarStyle` | Background, Foreground, Padding, ItemSpacing, FontSize, Weight, Italic, TimerLabelFontSize/Weight/Italic, TimerValueFontSize/Weight/Italic (13) | `status_bar` | +| `ValidationPanelStyle` | Background, Foreground, ErrorColor, WarningColor, MaxHeight (5) | `validation_panel` | +| `ChromeColors` | Info, Connected, Disconnected, LocalMode, Connecting, PanelBackground, PanelHeaderBackground, SubtleBorder, Separator, SecondaryForeground, GridBorder, GridBackground, HeaderForeground, **GridLine** (14) | `chrome` (13) + `colors.grid_line` | + +7+5+2+2+10+10+9+13+5+14 = 77 surfaced + `Orientation` = 78 fields, all accounted for. + +**Two loose fields** sit outside a sub-DTO in the source (`colors.grid_line`, `colors.cells.changed`/`changed_selected`). +(a) `grid_line` folds into `ChromeColors.GridLine` — it is grid chrome (beside `grid_border`/`grid_background`), and +one-level nesting means no lone `string` on the root; the mapper reads it from `colors.grid_line` into `Chrome`. +(b) `changed`/`changed_selected` become a small `ChangedCellColors` group. Both are the small cross-section reads the +one-level shape absorbs into the mapper. + +## Acceptance Evidence + +- `GridStyleOptions` is the nested root above; the ~10 group records exist; `DepthPalette` is one type used for both + ReadOnly and Disabled. `Default` is composed from per-group values, byte-equal to today's flat `Default`. +- Both mappers rebuilt as per-group construction; **no YAML/DTO/validator change** — `SaveThenLoad_DistinctFixture…` + (record→dto→yaml→dto→record) stays green, proving the nested record round-trips losslessly through the unchanged file + format. +- Every consumer reads the nested path (`gridStyle.Fonts.CellFontSize`, `gridStyle.ReadOnlyCells.Depth1`, …); the app + compiles with zero flat-field references left. +- Slice 1's guards, adapted to the nested record, still bite (proven by scratch checks); the `SurfacedEditablePropertyCount` + (77) is unchanged — the VM's public property surface does NOT change in this slice (that's slice 5). +- `dotnet build SemiStep.slnx` 0 warnings; full `dotnet test` green; `dotnet format` clean; screenshot tests + (`SemiStep.Screenshots/GridStyleEditorScreenshotTests`, if present) unchanged. + +## Task 1: The full nested rewrite — record, mappers, consumers, VM, fixture, ALL test fixes (one atomic commit) + +The record-shape change ripples until nothing references a flat field — this is **one atomic edit** across production AND +tests. A partial state does not compile. This task includes the slice-1 guard-test *adaptation* too (the name map + the +leaf walk), because those tests must reach green in the same commit — deferring them would leave committed red tests. +Do it all, reach full build+test green, commit once. (Task 2 only *proves* the adapted guards still bite and updates the +doc — no source change there.) + +**Files:** +- Create: `Configuration/GridStyleFonts.cs`, `GridStyleLayout.cs`, `SelectionColors.cs`, `ChangedCellColors.cs`, `DepthPalette.cs`, `ExecutionPalette.cs`, `StatusBarStyle.cs`, `ValidationPanelStyle.cs`, `ChromeColors.cs` (all `SemiStep.Core.Configuration`, BOM, positional `public sealed record`). +- Rewrite: `Configuration/GridStyleOptions.cs`, `Mapping/GridStyleMapper.cs`, `GridStyleDtoMapper.cs`. +- Modify: `SemiStep.UI/StyleEditor/GridStyleEditorViewModel.cs` (`Seed`/`BuildRecord` nested paths — the ~77 public properties do NOT change; `RecomputeCanSave`/`Validate` unchanged). +- Modify consumers (path rename only): `RecipeGrid/GridFontApplier.cs`, `ColumnBuilder.cs`, `ColumnWidthCalculator.cs`, `TextCellFactory.cs`, `Transposed/TransposedCellTemplateFactory.cs`, `Transposed/TransposedRecipeGridView.axaml.cs`, `ActiveRecipeGridSurface.cs`, `Styles/CellPaletteInstaller.cs`, `Styles/ExecutionPaletteInstaller.cs`. (`App.axaml.cs`/`ConfigFacade`/DI: carrier only, verify untouched.) +- Modify tests (flat-path → nested; compiler drives): `Helpers/GridStyleOptionsTestData.cs`, and every test that reads/`with`-mutates a flat record field — at least `GridStyleEditorFacadeTests`, `GridStyleEditorViewModelTests` (the non-guard tests + the guard adaptation), `GridStyleEditorWindowTests`, `ConfigFacadeGridStyleValidationTests`, `GridStyleMapperTests`, `CellPaletteInstallerTests`, `ExecutionPaletteInstallerTests`, `GridFactoryFontTests`, `ColumnWidthCalculatorTests`, `AppStatusBarTimerFontTests`, `TransposedCellBackgroundConverterTests`, `ActiveRecipeGridSurfaceTests`, `GridStyleWriterTests`, `GridStyleOrientationTests`, `SemiStep.Screenshots/RecipeGridScreenshotTests`. **This list is a starting point — grep the whole solution for the flat field names and fix every hit; the build gate is the ground truth.** + +- [x] Create the ~10 group records (positional, `string` colors, per the table). `DepthPalette` is ONE type used for both `ReadOnlyCells` and `DisabledCells`. +- [x] Rewrite `GridStyleOptions` as the nested root + `Default`, every value byte-equal to today's flat `Default`. +- [x] **Rewrite both mappers, preserving the CURRENT fallback regimes exactly** (read `GridStyleMapper.cs` first — do NOT blanket-add `?? defaults`): + - Whole-DTO-null → early `return GridStyleOptions.Default` (unchanged). + - `?? defaults..` per field ONLY for fonts / layout / selection / changed-cells / grid_line / status_bar / validation_panel / chrome — the defaults now read off the nested `GridStyleOptions.Default`'s group instances. + - **The three cell palettes (readonly / disabled / execution) keep their NO-fallback `!` chain** (`dto.Colors!.Cells!.ReadOnly!.Depth0!`, …) — validator-guaranteed presence; adding `?? defaults` there would silently swap a validator failure for a `#000000` placeholder. Keep the validator-precondition intent. + - `Orientation` via `GridOrientationValues.Parse`, not `??`. + - `Chrome.GridLine` ↔ `colors.grid_line`; `ChangedCells` ↔ `colors.cells.changed`/`changed_selected` (the two cross-section reads the one-level nesting accepts). `GridStyleDtoMapper` reads `options..` into each DTO section field-for-field. +- [x] Rewire the VM `Seed`/`BuildRecord` to nested paths (`BuildRecord` builds each group positionally; `Orientation` carried from `_source.Orientation`); rewire all ~9 consumers; rebuild `Distinct()` nested (same 77 distinct values). +- [x] **Adapt the slice-1 guards to the nested record** (`GridStyleEditorViewModelTests`): + - `RecordField` → a VM-property→**nested-path** map (`SelectionBackground`→`Selection.Background`, `HeaderFontSize`→`Fonts.HeaderFontSize`, `RowHeight`→`Layout.RowHeight`, `ReadOnlyCellDepth1`→`ReadOnlyCells.Depth1`, `ExecutionCurrentStepMarker`→`Execution.CurrentStepMarker`, `Info`→`Chrome.Info`, `GridLine`→`Chrome.GridLine`, …). Exhaustive (all 77); count assert stays 77. This map is **hand-maintained, not derived** — several leaf names differ from the VM property name (`StatusBarFontWeight`→`StatusBar.Weight`, `StatusBarTimerLabelFontWeight`→`StatusBar.TimerLabelWeight`, `FontFamily`→`Fonts.FontFamily`), so a mistyped entry surfaces as a red guard test, not a silent pass. Cross-check each entry against the group records. + - `ChangedRecordFields` → a **typed recursive leaf walk**: a property whose type is `string`/primitive/`enum` is a LEAF reported at its dotted path; a property whose type is a `record` group RECURSES. This keeps root-level `Orientation` (an enum) a first-class leaf — otherwise a `BuildRecord` bug that also flips Orientation would slip the exact-one-leaf assertion. Compare two `GridStyleOptions`, return the set of differing leaf paths; the perturbation asserts that set == `{ mappedPath }`. + - Seed-populates compares each VM property to the fixture's mapped nested-leaf value (`Convert.ToDecimal` for boxed numerics, `HexColor.Parse` for colors — only the record access nests). +- [x] `dotnet build SemiStep.slnx` 0 warnings (zero flat-field references left) AND full `dotnet test` green (all non-guard tests + the adapted guards). One commit. + +## Task 2: Prove the adapted guards still bite + doc + +No source change — verification + doc only. + +- [x] **Non-vacuity scratch checks (do NOT commit any break):** + - **Perturbation** — a dropped `BuildRecord` field is a compile error now (positional construction → CS7036), so the runtime check is a **same-type cross-wire**: temporarily pass a sibling's value into the wrong positional slot (e.g. `SelectionForeground` into `SelectionColors`'s Background slot, or swap the `ReadOnlyCells`/`DisabledCells` `DepthPalette` instances in `BuildRecord`) → the perturbation test goes red for the mis-wired leaf(s). Revert. + - **Seed-populates** — delete a `Seed` line (a nullable numeric or an italic) → red. Revert. + - **Save→load** — delete a `GridStyleDtoMapper` line (nullable object-initializer → compiles, drifts through the fallback) → `SaveThenLoad_DistinctFixture` red. Revert. + - Record all three results in the progress log. If the perturbation cross-wire does NOT go red, the leaf walk is wrong — that's a Task-1 defect to fix (re-open Task 1), not something to paper over. +- [x] Interim doc note in `Docs/architecture/grid-style-configuration.md`: the record is now nested per group (full rewrite is slice 5); YAML/DTO/validator unchanged. **Note for slice 4:** the error-path key `colors.grid_line` and the record path `Chrome.GridLine` deliberately diverge — slice 4's mapper-resident validation must keep emitting the `colors.grid_line` key, not rename it. +- [x] `dotnet build SemiStep.slnx` 0 warnings; full `dotnet test` green (incl. screenshot tests); `dotnet format`. (screenshot project pre-existing-broken at HEAD, excluded from slnx) + +## Post-Completion + +**Next:** slice 4 — introduce `StyleColor` (readonly record struct, Core, Avalonia-free) and flip the 53 color fields +(now living in the group records) from `string` to `StyleColor`; fold `GridStyleValidator`'s per-key checks into the +load mapper (aggregate, same error identity); delete `HexColor`; retarget `PaletteBrushFactory.From(StyleColor)`. Then +slice 5 — split the VM into per-group drafts + grouped AXAML, which closes #118. Two forks await slice 4/5 planning: +`StyleColor` struct-vs-class + the `#RGB`/`#ARGB` parse discrepancy (slice 4), and the AXAML rename scope + facade +`Validate` fate (slice 5). + +**Executed by exec:** +- branch: grid-style-nest-record + +## Verify it yourself + +This slice is a pure structural refactor: the on-disk YAML is byte-identical, the DTOs/validator are untouched, +and the operator-visible behaviour of the style editor and grid is unchanged. There is no manual repro that +distinguishes before from after — the whole point is that nothing observable moves. The evidence is in the tests +and the diff. + +1. **Nothing on disk changed.** `git diff master...HEAD -- '*Dto*.cs' '*Validator*.cs' 'SemiStep/**/grid_style.yaml'` + returns empty — only the record shape and its readers moved. +2. **The record round-trips losslessly through the unchanged file format.** Run the acceptance test that drives + record→dto→yaml→dto→record with a distinct-value fixture: + `dotnet test SemiStep/SemiStep.Tests/SemiStep.Tests.csproj --filter "FullyQualifiedName~SaveThenLoad_DistinctFixture"` + — green means every one of the 78 fields survived the nesting and lands on the correct target field. +3. **The silent-drop guards still bite the nested record.** The slice-1 guards were adapted (nested-path map + + typed leaf walk) and their non-vacuity was re-proven by scratch checks — see the progress log entries for the + cross-wire → `BuildRecord_PerturbingEachProperty_ChangesOnlyThatMappedField` RED, delete-Seed-line → + `Seed_PopulatesEverySurfacedProperty_FromDistinctFixture` RED, delete-DtoMapper-line → `SaveThenLoad_DistinctFixture` RED. + Run all three green: `dotnet test SemiStep/SemiStep.Tests/SemiStep.Tests.csproj --filter "FullyQualifiedName~GridStyleEditorViewModelTests"`. +4. **Zero flat-field references remain and the app builds clean.** `dotnet build SemiStep.slnx` → 0 warnings, 0 errors; + full `dotnet test` → 1687 passed. A dropped or cross-wired field would surface as a red guard or a build error, not + a silent revert. +5. **Manual smoke (optional):** open the style editor, change a handful of values across groups (a font size, a + selection color, a status-bar timer weight, orientation), save, reopen — every value persists. Pre-fix commit that + would fail a dropped-field scenario is demonstrated by the Task-2 scratch checks, not reproducible by hand.