Forge
csharpdeeb25a2
1#nullable enable
2using CascadeIDE.Views.SkiaKit;
3using SkiaSharp;
4
5namespace CascadeIDE.Views.Chat;
6
7/// <summary>Данные карточки темы / spine для Skia overview (секции через <see cref="SkiaSectionedCard"/>).</summary>
8internal sealed record SkiaTopicCardModel(
9 SkiaSectionedCardModel Card,
10 bool IsFocused,
11 bool IsMainThread);
12
13/// <summary>Геометрия карточки после Measure.</summary>
14internal readonly record struct SkiaTopicCardLayout(
15 float Height,
16 float GapAfter,
17 float ContentInsetX)
18{
19 public const int MaxSummaryLines = 4;
20 public const float GapAfterDefault = 12f;
21}
22
23/// <summary>Состояние отрисовки (hover/focus).</summary>
24internal readonly record struct SkiaTopicCardDrawState(bool IsHovered, bool IsFocused);
25
26/// <summary>Skia-отрисовка карточки темы: делегирует в SkiaKit.</summary>
27internal static class SkiaTopicCard
28{
29 public static IReadOnlyList<string> PrepareSummaryLines(string summary, int maxChars)
30 {
31 var lines = SkiaTextLayout.Wrap(Trim(summary, 32_000), maxChars);
32 if (lines.Count == 0)
33 return ["Нет краткого описания"];
34 if (lines.Count == 1 && string.Equals(lines[0], "Пока без сообщений", StringComparison.Ordinal))
35 return ["Пока без сообщений — задай вопрос в теме"];
36 if (lines.Count > SkiaTopicCardLayout.MaxSummaryLines)
37 return lines.Take(SkiaTopicCardLayout.MaxSummaryLines).ToList();
38 return lines;
39 }
40
41 public static IReadOnlyList<string> ParseTagLines(string? footer)
42 {
43 if (string.IsNullOrWhiteSpace(footer))
44 return ["—"];
45
46 var tags = footer.Split(" · ", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
47 return tags.Length == 0 ? ["—"] : tags;
48 }
49
50 public static IReadOnlyList<string> NormalizeTagLines(IReadOnlyList<string> tags) =>
51 tags.Count == 0 ? ["—"] : tags;
52
53 public static SkiaTopicCardModel Create(
54 string title,
55 string summary,
56 string? tagFooter,
57 bool isFocused,
58 int maxChars)
59 {
60 var topicLines = new List<string> { title.Trim() };
61 var card = SkiaSectionedCard.FromThreeCompartments(
62 "ТЕМА",
63 topicLines,
64 "ТЕГИ",
65 ParseTagLines(tagFooter),
66 "САММАРИ",
67 PrepareSummaryLines(summary, maxChars));
68 return new(
69 card,
70 isFocused,
71 title.TrimStart().StartsWith("◆ ", StringComparison.Ordinal));
72 }
73
74 public static SkiaTopicCardModel CreateSpine(
75 string lineTitle,
76 string currentFocus,
77 IReadOnlyList<string> milestones,
78 bool includeInAgentContext,
79 int maxChars)
80 {
81 var tagLines = new List<string>(NormalizeTagLines(milestones));
82 tagLines.Add(includeInAgentContext ? "в контексте агента" : "не в контексте агента");
83 var summary = string.IsNullOrWhiteSpace(currentFocus)
84 ? "Задай фокус линии и вехи над чатом."
85 : currentFocus.Trim();
86 var card = SkiaSectionedCard.FromThreeCompartments(
87 "ТЕМА",
88 ["↗ " + lineTitle.Trim()],
89 "ТЕГИ",
90 tagLines,
91 "САММАРИ",
92 PrepareSummaryLines(summary, maxChars));
93 return new(card, IsFocused: false, IsMainThread: true);
94 }
95
96 public static SkiaTopicCardLayout Measure(SkiaTopicCardModel model) =>
97 new(
98 SkiaSectionedCard.MeasureTotalHeight(model.Card),
99 SkiaTopicCardLayout.GapAfterDefault,
100 SkiaSectionedCard.HorizontalPadding);
101
102 public static SKColor ResolveFillColor(SkiaChatTheme theme, SkiaTopicCardModel model) =>
103 model.IsFocused
104 ? SkiaKitColor.Blend(theme.BubbleAssistant, theme.SelectedBorder, 0.22f)
105 : SkiaKitColor.Blend(theme.Surface, theme.BubbleAssistant, 0.82f);
106
107 public static void Draw(
108 SKCanvas canvas,
109 SkiaChatTheme theme,
110 SKRect bounds,
111 float contentLeft,
112 float contentWidth,
113 SkiaTopicCardModel model,
114 SkiaTopicCardLayout layout,
115 SkiaTopicCardDrawState state)
116 {
117 var drawState = new SkiaSectionedCardDrawState(
118 ResolveFillColor(theme, model),
119 state.IsHovered,
120 state.IsFocused);
121 SkiaSectionedCard.Draw(canvas, theme, bounds, contentLeft, contentWidth, model.Card, in drawState);
122 }
123
124 private static string Trim(string text, int maxLen) =>
125 text.Length <= maxLen ? text : text[..maxLen] + "...";
126}
127
View only · write via MCP/CIDE