using System; using System.Collections.Generic; using System.Globalization; namespace codelanguage; /// /// The single source of truth for how an interface control looks. Both the /// interface window editor (design time) and the Craft++ Seeker (run time) /// resolve appearance through , so a saved .cpf renders /// identically in both. Pure and headless; InterfaceFidelityTests asserts the /// editor and runtime paths agree for every control kind. /// public static class ControlVisualSpec { /// Editor placeholder background, also the window fill. public const string DefaultWindowBackground = "#FFF5F5F5"; // WhiteSmoke public const string DefaultMapBackground = "#FF2F4F4F"; // DarkSlateGray /// Default fill for non-window controls (as authored in the editor). public const string DefaultControlBackground = "#FFF5F5F5"; public const string DefaultOutputBackground = "#FFFFFFFF"; public const string DefaultBorderColor = "#FF808080"; // Gray public const string DefaultTextColor = "#FF000000"; /// Normalized appearance of one control. public sealed record Visual( string Type, string Background, string BorderColor, double BorderThickness, string Foreground, string FontFamily, double FontSize, double Padding, bool IsVisible); /// Canonical, ordered property fallbacks per control kind. private static readonly Dictionary> Defaults = new(StringComparer.OrdinalIgnoreCase) { ["Window"] = new(StringComparer.OrdinalIgnoreCase) { ["background-color"] = DefaultWindowBackground, ["text-color"] = DefaultTextColor, ["font-family"] = "Arial", ["font-size"] = "12", ["is-visible"] = "true", }, ["Map"] = new(StringComparer.OrdinalIgnoreCase) { ["background-color"] = DefaultMapBackground, ["is-default"] = "true", ["view-size"] = "15x15", ["zoom"] = "0", ["show-names"] = "false", }, ["Output"] = new(StringComparer.OrdinalIgnoreCase) { ["background-color"] = DefaultOutputBackground, ["text-color"] = DefaultTextColor, ["max-lines"] = "1000", ["allow-html"] = "false", ["can-scroll"] = "true", }, ["Input"] = new(StringComparer.OrdinalIgnoreCase) { ["background-color"] = DefaultControlBackground, ["text-color"] = DefaultTextColor, ["multi-line"] = "false", ["is-password"] = "false", }, ["Button"] = new(StringComparer.OrdinalIgnoreCase) { ["button-type"] = "pushbutton", ["is-checked"] = "false", }, ["Label"] = new(StringComparer.OrdinalIgnoreCase) { ["background-color"] = "", ["text-color"] = DefaultTextColor, }, ["Bar"] = new(StringComparer.OrdinalIgnoreCase) { ["value"] = "0", ["is-slider"] = "false", }, ["Tabs"] = new(StringComparer.OrdinalIgnoreCase) { ["tabs"] = "", ["current-tab"] = "", }, ["Menu"] = new(StringComparer.OrdinalIgnoreCase) { ["items"] = "File>New|File>Open|Help>About", }, ["Macro"] = new(StringComparer.OrdinalIgnoreCase) { ["bindings"] = "W=move_north|A=move_west|S=move_south|D=move_east", }, ["Grid"] = new(StringComparer.OrdinalIgnoreCase) { ["cells"] = "4x4", ["cell-span"] = "1x1", ["is-list"] = "false", }, ["Child"] = new(StringComparer.OrdinalIgnoreCase) { ["orientation"] = "horizontal", ["splitter"] = "50", }, ["Browser"] = new(StringComparer.OrdinalIgnoreCase) { ["allow-html"] = "true", ["allow-navigation"] = "true", ["browser-zoom"] = "100", }, ["Info"] = new(StringComparer.OrdinalIgnoreCase) { ["stat-panel"] = "true", ["verb-panel"] = "true", }, }; /// Applies the canonical defaults for then the authored properties. public static Dictionary WithDefaults(string type, IEnumerable>? authored) { var result = new Dictionary(StringComparer.OrdinalIgnoreCase); if (Defaults.TryGetValue(type, out var defaults)) foreach (var pair in defaults) result[pair.Key] = pair.Value; // Every control kind shares these appearance fallbacks. SetIfMissing(result, "text-color", DefaultTextColor); SetIfMissing(result, "font-family", "Arial"); SetIfMissing(result, "font-size", "12"); SetIfMissing(result, "is-visible", "true"); if (authored != null) foreach (var pair in authored) result[pair.Key] = pair.Value; return result; } /// Resolves the normalized appearance both the editor and the Seeker render. public static Visual Resolve(string type, IEnumerable>? authored) => Resolve(type, authored, null); /// /// Resolves appearance, preferring the control's explicit background field /// (where DmfInterfaceDocument.Parse stores an authored background-color) /// over any background-color inside the property string. /// public static Visual Resolve(string type, IEnumerable>? authored, string? backgroundField) { var props = WithDefaults(type, authored); string normalized = NormalizeType(type); string background = !string.IsNullOrWhiteSpace(backgroundField) ? backgroundField : Get(props, "background-color", DefaultBackgroundFor(normalized)); string border = Get(props, "border-color", DefaultBorderColor); // Windows draw no inner frame; every other control has a 1px outline in // both the editor and the Seeker. double borderThickness = normalized == "Window" ? 0 : 1; if (double.TryParse(Get(props, "border-thickness", null), NumberStyles.Float, CultureInfo.InvariantCulture, out double thickness)) borderThickness = Math.Clamp(thickness, 0, 64); double fontSize = double.TryParse(Get(props, "font-size", "12"), NumberStyles.Float, CultureInfo.InvariantCulture, out double size) && double.IsFinite(size) && size is >= 1 and <= 512 ? size : 12; double padding = normalized == "Input" ? 4 : 0; if (double.TryParse(Get(props, "padding", null), NumberStyles.Float, CultureInfo.InvariantCulture, out double pad)) padding = Math.Clamp(pad, 0, 256); bool visible = !string.Equals(Get(props, "is-visible", "true"), "false", StringComparison.OrdinalIgnoreCase); return new Visual( normalized, background, border, borderThickness, Get(props, "text-color", DefaultTextColor), Get(props, "font-family", "Arial"), fontSize, padding, visible); } /// Canonical control kind (matches DmfInterfaceDocument's names). public static string NormalizeType(string? type) => (type ?? "").Trim().ToLowerInvariant() switch { "window" or "mainwindow" => "Window", "subwindow" => "SubWindow", "map" => "Map", "output" => "Output", "input" => "Input", "button" or "checkbox" or "radio" => "Button", "label" => "Label", "bar" => "Bar", "tabs" or "tab" => "Tabs", "menu" => "Menu", "macro" => "Macro", "grid" => "Grid", "child" => "Child", "browser" => "Browser", "info" => "Info", "select" => "Select", "image" => "Image", "panel" => "Panel", _ => "Panel", }; /// The background used when a control authors none. public static string DefaultBackgroundFor(string normalizedType) => normalizedType switch { "Window" or "Panel" or "Info" or "Child" or "Browser" or "Grid" or "Tabs" or "Menu" => DefaultWindowBackground, "Map" => DefaultMapBackground, "Output" => DefaultOutputBackground, "Label" => "", _ => DefaultControlBackground, }; private static string Get(Dictionary props, string key, string? fallback) => props.TryGetValue(key, out string? value) && !string.IsNullOrWhiteSpace(value) ? value : fallback ?? ""; private static void SetIfMissing(Dictionary props, string key, string value) { if (!props.ContainsKey(key)) props[key] = value; } }