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;
}
}