Forge
csharpdeeb25a2
1using System.Globalization;
2using System.Text.Json;
3using System.Text.Json.Nodes;
4using System.Text.Json.Serialization;
5using Avalonia;
6using Avalonia.Controls;
7using Avalonia.Media;
8using Avalonia.Threading;
9
10namespace CascadeIDE.Services;
11
12/// <summary>
13/// Снимок цветов UI для агента (<c>ide_get_ui_theme</c>): читает <see cref="Application.Current"/>.<see cref="StyledElement.Resources"/>
14/// (те же ключи <c>CascadeTheme.*</c>, что и <see cref="UiThemeApply"/> / <c>App.axaml</c>).
15/// </summary>
16public static class UiThemeSnapshot
17{
18 private static readonly JsonSerializerOptions Options = new()
19 {
20 WriteIndented = true,
21 DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
22 };
23
24 /// <summary>JSON для MCP: актуальные кисти из Application.Resources (с UI-потока).</summary>
25 public static string GetJson()
26 {
27 if (Application.Current is null)
28 return SerializeStaticFallback();
29
30 if (UiScheduler.Default.CheckAccess())
31 return CaptureAndSerialize(Application.Current);
32
33 return UiScheduler.Default.Invoke(() =>
34 {
35 if (Application.Current is null)
36 return SerializeStaticFallback();
37 return CaptureAndSerialize(Application.Current);
38 });
39 }
40
41 private static string CaptureAndSerialize(Application app)
42 {
43 var res = app.Resources;
44 var variantName = app.ActualThemeVariant.Key.ToString();
45 var requestedName = app.RequestedThemeVariant?.Key.ToString() ?? "Default";
46
47 static string? H(IResourceDictionary d, string k) => TryBrushString(d, k);
48
49 var mainBg = H(res, UiThemeApply.Keys.MainWindowBackground);
50 var menuBg = H(res, UiThemeApply.Keys.MenuBackground);
51 var menuFg = H(res, UiThemeApply.Keys.MenuForeground);
52
53 var theme = new
54 {
55 _snapshot = new
56 {
57 source = "application.resources",
58 actual_theme_variant = variantName,
59 requested_theme_variant = requestedName
60 },
61 main_window = new { background = mainBg },
62 menu = new { background = menuBg, foreground = menuFg },
63 menu_item = new { foreground = menuFg },
64 button = new
65 {
66 background = H(res, UiThemeApply.Keys.ButtonBackground),
67 foreground = H(res, UiThemeApply.Keys.ButtonForeground),
68 border_brush = H(res, UiThemeApply.Keys.ButtonBorderBrush),
69 border_thickness = 1,
70 hover_background = H(res, UiThemeApply.Keys.ButtonHoverBackground),
71 disabled_background = H(res, UiThemeApply.Keys.ButtonDisabledBackground),
72 disabled_foreground = H(res, UiThemeApply.Keys.ButtonDisabledForeground)
73 },
74 toolbar = new { background = H(res, UiThemeApply.Keys.ToolbarBackground) },
75 toolbar_text = new
76 {
77 foreground = H(res, UiThemeApply.Keys.ToolbarTextForeground),
78 error_foreground = H(res, UiThemeApply.Keys.ToolbarErrorForeground)
79 },
80 editor = new
81 {
82 background = H(res, UiThemeApply.Keys.EditorBackground),
83 foreground = H(res, UiThemeApply.Keys.EditorForeground),
84 selection_brush = H(res, UiThemeApply.Keys.EditorSelectionBrush),
85 selection_foreground = H(res, UiThemeApply.Keys.EditorSelectionForeground),
86 font_family = "Consolas, Cascadia Code, monospace",
87 font_size = 13
88 },
89 editor_column = new
90 {
91 border_brush = H(res, UiThemeApply.Keys.EditorColumnBorderBrush),
92 background = H(res, UiThemeApply.Keys.EditorColumnBackground),
93 current_file_foreground = H(res, UiThemeApply.Keys.CurrentFileForeground)
94 },
95 workspace_layout = new
96 {
97 border_brush = H(res, UiThemeApply.Keys.WorkspacePanelBorderBrush)
98 },
99 markdown_preview_panel = new
100 {
101 background = H(res, UiThemeApply.Keys.MarkdownPreviewPanelBackground),
102 border_brush = H(res, UiThemeApply.Keys.MarkdownPreviewPanelBorderBrush)
103 },
104 solution_explorer = new
105 {
106 border_brush = H(res, UiThemeApply.Keys.SolutionExplorerBorderBrush),
107 header_foreground = H(res, UiThemeApply.Keys.SolutionExplorerHeaderForeground),
108 tree_item_foreground = H(res, UiThemeApply.Keys.SolutionExplorerHeaderForeground)
109 },
110 panel_chrome = new
111 {
112 title_foreground = H(res, UiThemeApply.Keys.PanelChromeTitleForeground),
113 accent_brush = H(res, UiThemeApply.Keys.PanelTitleAccentBrush),
114 header_background = H(res, UiThemeApply.Keys.PanelChromeHeaderBackground),
115 header_separator = H(res, UiThemeApply.Keys.PanelChromeHeaderSeparatorBrush),
116 menu_glyph_foreground = H(res, UiThemeApply.Keys.PanelChromeMenuGlyphForeground)
117 },
118 build_output = new
119 {
120 background = H(res, UiThemeApply.Keys.BuildOutputBackground),
121 foreground = H(res, UiThemeApply.Keys.TerminalForeground),
122 border_brush = H(res, UiThemeApply.Keys.BuildOutputBorderBrush)
123 },
124 chat_panel = new
125 {
126 background = H(res, UiThemeApply.Keys.ChatPanelBackground),
127 label_foreground = H(res, UiThemeApply.Keys.ChatLabelForeground),
128 secondary_foreground = H(res, UiThemeApply.Keys.ChatLabelForeground),
129 message_bubble_background = H(res, UiThemeApply.Keys.ChatMessageBubbleBackground),
130 message_role_foreground = H(res, UiThemeApply.Keys.ChatLabelForeground),
131 message_content_foreground = H(res, UiThemeApply.Keys.ChatMessageContentForeground),
132 send_button_background = H(res, UiThemeApply.Keys.SendButtonBackground),
133 send_button_foreground = H(res, UiThemeApply.Keys.SendButtonForeground),
134 checkbox_foreground = H(res, UiThemeApply.Keys.ChatLabelForeground)
135 },
136 terminal = new
137 {
138 background = H(res, UiThemeApply.Keys.TerminalBackground),
139 foreground = H(res, UiThemeApply.Keys.TerminalForeground),
140 input_background = H(res, UiThemeApply.Keys.TerminalInputBackground),
141 font_family = "Consolas, monospace",
142 font_size = 12
143 },
144 mcp_banner = new
145 {
146 background = H(res, UiThemeApply.Keys.McpBannerBackground),
147 foreground = H(res, UiThemeApply.Keys.McpBannerForeground)
148 },
149 preview_window = new
150 {
151 background = H(res, UiThemeApply.Keys.PreviewWindowBackground),
152 content_area_background = H(res, UiThemeApply.Keys.PreviewWindowBackground)
153 },
154 power_cockpit = new
155 {
156 neon_border = H(res, UiThemeApply.Keys.PowerNeonBorder),
157 neon_accent = H(res, UiThemeApply.Keys.PowerNeonAccent),
158 panel_background = H(res, UiThemeApply.Keys.PowerCockpitPanelBackground),
159 safety_dock_background = H(res, UiThemeApply.Keys.PowerSafetyDockBackground),
160 workspace_health_strip_background = H(res, UiThemeApply.Keys.PowerWorkspaceHealthStripBackground),
161 safety_observe = H(res, UiThemeApply.Keys.PowerSafetyObserve),
162 safety_confirm = H(res, UiThemeApply.Keys.PowerSafetyConfirm),
163 safety_autonomous = H(res, UiThemeApply.Keys.PowerSafetyAutonomous),
164 emergency = H(res, UiThemeApply.Keys.PowerEmergency)
165 },
166 solution_explorer_tree_power = new
167 {
168 panel_background = H(res, "CascadeTheme.PowerSolutionTreePanelBackground"),
169 tree_foreground = H(res, "CascadeTheme.PowerSolutionTreeForeground"),
170 item_selected_background = H(res, "CascadeTheme.PowerSolutionTreeItemSelectedBackground"),
171 item_pointer_over_background = H(res, "CascadeTheme.PowerSolutionTreeItemPointerOverBackground"),
172 panel_border_subtle = H(res, "CascadeTheme.PowerSolutionPanelBorderSubtle"),
173 task_queue_bubble_background = H(res, "CascadeTheme.PowerSolutionTaskQueueBubbleBackground")
174 },
175 power_island_frame_brushes = new
176 {
177 editor = H(res, "CascadeTheme.PowerEditorIslandFrameBrush"),
178 chat = H(res, "CascadeTheme.PowerChatIslandFrameBrush"),
179 solution = H(res, "CascadeTheme.PowerSolutionIslandFrameBrush")
180 }
181 };
182
183 var node = JsonSerializer.SerializeToNode(theme, Options) as JsonObject;
184 if (node is not null)
185 UiThemeDeepSnapshot.MergeIntoJsonRoot(node, app);
186 return node?.ToJsonString(Options) ?? JsonSerializer.Serialize(theme, Options);
187 }
188
189 /// <summary>Строка для JSON: solid как <c>#RGB</c>/<c>#AARRGGBB</c>, <see cref="LinearGradientBrush"/> как <c>linear(...)</c>.</summary>
190 public static string? FormatBrushForJson(IBrush? brush) =>
191 brush is null ? null : FormatBrushForJson((object)brush);
192
193 /// <summary>Как <see cref="FormatBrushForJson(IBrush?)"/>, для произвольного объекта из ресурсов.</summary>
194 public static string? FormatBrushForJson(object? raw) => FormatBrushObject(raw);
195
196 /// <summary>Статический шаблон, если приложение ещё не поднято (тесты, сравнение формата).</summary>
197 private static string SerializeStaticFallback()
198 {
199 var theme = new
200 {
201 _snapshot = new { source = "static_fallback", detail = "Application.Current is null" },
202 main_window = new { background = "#F5F5F5" },
203 menu = new { background = "#F0F0F0", foreground = "#1A1A1A" },
204 menu_item = new { foreground = "#1A1A1A" },
205 button = new
206 {
207 background = "#E0E0E0",
208 foreground = "#1A1A1A",
209 border_brush = "#BBB",
210 border_thickness = 1,
211 hover_background = "#D0D0D0",
212 disabled_background = "#EEE",
213 disabled_foreground = "#888"
214 },
215 toolbar = new { background = "#E8E8E8" },
216 toolbar_text = new { foreground = "#333", error_foreground = "#C00" },
217 editor = new
218 {
219 background = "#FFF",
220 foreground = "#1A1A1A",
221 selection_brush = "#55225679",
222 selection_foreground = "#1A1A1A",
223 font_family = "Consolas, Cascadia Code, monospace",
224 font_size = 13
225 },
226 editor_column = new { border_brush = "#CCC", background = "#FFF", current_file_foreground = "#666" },
227 workspace_layout = new { border_brush = "#CCC" },
228 markdown_preview_panel = new { background = "#FAFAFA", border_brush = "#E0E0E0" },
229 solution_explorer = new { border_brush = "#CCC", header_foreground = "#1A1A1A", tree_item_foreground = "#1A1A1A" },
230 panel_chrome = new
231 {
232 title_foreground = "#1A1A1A",
233 accent_brush = "#5B4FC9",
234 header_background = "#E8E8EC",
235 header_separator = "#C8C8D0",
236 menu_glyph_foreground = "#666666"
237 },
238 build_output = new { background = "#F8F8F8", foreground = "#CCC", border_brush = "#CCC" },
239 chat_panel = new
240 {
241 background = "#FFF",
242 label_foreground = "#333",
243 secondary_foreground = "#666",
244 message_bubble_background = "#F0F0F0",
245 message_role_foreground = "#555",
246 message_content_foreground = "#1A1A1A",
247 send_button_background = "#4CAF50",
248 send_button_foreground = "White",
249 checkbox_foreground = "#333"
250 },
251 terminal = new
252 {
253 background = "#1E1E1E",
254 foreground = "#CCC",
255 input_background = "#2D2D2D",
256 font_family = "Consolas, monospace",
257 font_size = 12
258 },
259 mcp_banner = new { background = "#E3F2FD", foreground = "#1565C0" },
260 preview_window = new { background = "#F0F0F0", content_area_background = "#F0F0F0" },
261 power_cockpit = new
262 {
263 neon_border = "#00F0FF",
264 neon_accent = "#E090FF",
265 panel_background = "#070E18",
266 safety_dock_background = "#020814",
267 workspace_health_strip_background = "#040A14",
268 safety_observe = "#1A5088",
269 safety_confirm = "#9A7818",
270 safety_autonomous = "#6230B0",
271 emergency = "#FF3355"
272 },
273 solution_explorer_tree_power = (object?)null,
274 power_island_frame_brushes = (object?)null
275 };
276 var fallbackNode = JsonSerializer.SerializeToNode(theme, Options) as JsonObject;
277 if (fallbackNode is not null)
278 {
279 fallbackNode["cascade_theme_resolved"] = null;
280 fallbackNode["window_frame"] = null;
281 fallbackNode["layout_regions"] = null;
282 fallbackNode["dock_text_editors"] = null;
283 fallbackNode["dock_open_documents"] = null;
284 fallbackNode["top_levels"] = null;
285 }
286
287 return fallbackNode?.ToJsonString(Options) ?? JsonSerializer.Serialize(theme, Options);
288 }
289
290 private static string? TryBrushString(IResourceDictionary res, string key)
291 {
292 if (!res.TryGetValue(key, out var raw) || raw is null)
293 return null;
294 return FormatBrushObject(raw);
295 }
296
297 private static string? FormatBrushObject(object? raw)
298 {
299 switch (raw)
300 {
301 case null:
302 return null;
303 case LinearGradientBrush lg:
304 return SerializeLinearGradient(lg);
305 case ISolidColorBrush sc:
306 return ColorToHex(sc.Color);
307 default:
308 return null;
309 }
310 }
311
312 private static string ColorToHex(Color c)
313 {
314 if (c.A == byte.MaxValue)
315 return FormattableString.Invariant($"#{c.R:X2}{c.G:X2}{c.B:X2}");
316 return FormattableString.Invariant($"#{c.A:X2}{c.R:X2}{c.G:X2}{c.B:X2}");
317 }
318
319 private static string SerializeLinearGradient(LinearGradientBrush lg)
320 {
321 var sp = lg.StartPoint.Point;
322 var ep = lg.EndPoint.Point;
323 var stops = new List<string>();
324 foreach (var gs in lg.GradientStops)
325 stops.Add(string.Create(CultureInfo.InvariantCulture, $"{gs.Offset:R}:{ColorToHex(gs.Color)}"));
326 return string.Create(CultureInfo.InvariantCulture,
327 $"linear({sp.X:R},{sp.Y:R}->{ep.X:R},{ep.Y:R};{string.Join(",", stops)})");
328 }
329
330 /// <summary>Цвета страницы «Вывод сборки» в MFD (фон журнала и текст в духе <c>TerminalForeground</c> хоста стека).</summary>
331 public static (string background, string foreground) GetBuildOutputTheme()
332 {
333 const string defBg = "#F8F8F8";
334 const string defFg = "#CCC";
335
336 if (Application.Current is null)
337 return (defBg, defFg);
338
339 if (UiScheduler.Default.CheckAccess())
340 return ReadBuildOutput(Application.Current.Resources);
341
342 return UiScheduler.Default.Invoke(() =>
343 Application.Current is { } app
344 ? ReadBuildOutput(app.Resources)
345 : (defBg, defFg));
346 }
347
348 private static (string background, string foreground) ReadBuildOutput(IResourceDictionary res)
349 {
350 const string defBg = "#F8F8F8";
351 const string defFg = "#CCC";
352 var bg = TrySolidHexOnly(res, UiThemeApply.Keys.BuildOutputBackground) ?? defBg;
353 var fg = TrySolidHexOnly(res, UiThemeApply.Keys.TerminalForeground) ?? defFg;
354 return (bg, fg);
355 }
356
357 private static string? TrySolidHexOnly(IResourceDictionary res, string key)
358 {
359 if (!res.TryGetValue(key, out var raw) || raw is null)
360 return null;
361 return raw switch
362 {
363 ISolidColorBrush s => ColorToHex(s.Color),
364 _ => null
365 };
366 }
367}
368
View only · write via MCP/CIDE