Forge
csharpdeeb25a2
1#nullable enable
2using CascadeIDE.Views.SkiaKit;
3using SkiaSharp;
4
5namespace CascadeIDE.Views.Chat.Skia;
6
7/// <summary>
8/// Карточка темы в картотеке (overview). Отдельный chrome: <see cref="SkiaTopicCard"/>,
9/// не <see cref="SkiaChatBubbleKind.CardPanel"/> (product spine — см. README.md).
10/// </summary>
11internal sealed class SkiaChatTopicCard(
12 SkiaTopicCardModel model,
13 Guid threadId) : ISkiaChatEntity, IGridTileSkiaChatEntity
14{
15 public static SkiaChatTopicCard ForThread(
16 string title,
17 string summary,
18 string? tagFooter,
19 bool isFocused,
20 Guid threadId) =>
21 new(
22 SkiaTopicCard.Create(title, summary, tagFooter, isFocused, maxChars: 80),
23 threadId);
24
25 public static SkiaChatTopicCard ForSpine(
26 string lineTitle,
27 string currentFocus,
28 IReadOnlyList<string> milestones,
29 bool includeInAgentContext) =>
30 new(
31 SkiaTopicCard.CreateSpine(lineTitle, currentFocus, milestones, includeInAgentContext, maxChars: 80),
32 Guid.Empty);
33
34 public bool IsGridTile => threadId != Guid.Empty;
35
36 public SkiaChatMeasuredLayout Measure(SkiaChatMeasureContext context)
37 {
38 var maxChars = Math.Max(16, (int)(context.ContentWidth / 7.1f));
39 var measuredModel = RemeasureModel(maxChars);
40 var cardLayout = SkiaTopicCard.Measure(measuredModel);
41 return new SkiaChatMeasuredLayout(
42 cardLayout.Height,
43 cardLayout.GapAfter,
44 TopicCard: measuredModel,
45 TopicCardLayout: cardLayout);
46 }
47
48 public void Draw(SkiaChatDrawContext context, float top, in SkiaChatMeasuredLayout layout)
49 {
50 var model = layout.TopicCard!;
51 var cardLayout = layout.TopicCardLayout!.Value;
52 var rect = new SKRect(context.ContentLeft, top, context.ContentLeft + context.ContentWidth, top + layout.Height);
53 SkiaTopicCard.Draw(
54 context.Canvas,
55 context.Theme,
56 rect,
57 context.ContentLeft + cardLayout.ContentInsetX,
58 context.ContentWidth - cardLayout.ContentInsetX * 2,
59 model,
60 cardLayout,
61 new SkiaTopicCardDrawState(context.IsHovered, model.IsFocused));
62 }
63
64 public SkiaChatHit? CreateHit(in SkiaChatMeasuredLayout layout) =>
65 threadId == Guid.Empty
66 ? null
67 : new(MessageIndex: null, SelectThreadId: threadId, ResetDetailMode: false);
68
69 private SkiaTopicCardModel RemeasureModel(int maxChars)
70 {
71 var sections = model.Card.Sections;
72 if (threadId == Guid.Empty)
73 {
74 var title = sections.Count > 0 && sections[0].Lines.Count > 0
75 ? sections[0].Lines[0].TrimStart('↗', ' ')
76 : "";
77 var tagSection = sections.Count > 1 ? sections[1].Lines : [];
78 var summary = sections.Count > 2 ? string.Join(' ', sections[2].Lines) : "";
79 var milestones = tagSection
80 .Where(t => t is not "—" and not "в контексте агента" and not "не в контексте агента")
81 .ToList();
82 var include = tagSection.Contains("в контексте агента");
83 return SkiaTopicCard.CreateSpine(title, summary, milestones, include, maxChars);
84 }
85
86 var topicTitle = sections.Count > 0 && sections[0].Lines.Count > 0 ? sections[0].Lines[0] : "";
87 var summaryText = sections.Count > 2 ? string.Join(' ', sections[2].Lines) : "";
88 var tagLines = sections.Count > 1 ? sections[1].Lines : [];
89 var tags = tagLines.Count == 1 && tagLines[0] == "—"
90 ? null
91 : string.Join(" · ", tagLines);
92 return SkiaTopicCard.Create(topicTitle, summaryText, tags, model.IsFocused, maxChars);
93 }
94}
95
View only · write via MCP/CIDE