Forge
csharpdeeb25a2
1#nullable enable
2
3using CascadeIDE.Models;
4using CascadeIDE.Services;
5using SkiaSharp;
6
7namespace CascadeIDE.Views.Chat.Skia;
8
9/// <summary>
10/// Единые метрики flat feed (ADR 0123): role rail, колонка тела, prose/slash — без разрозненных inset/baseline в сущностях.
11/// </summary>
12internal readonly struct SkiaChatFeedLayout
13{
14 public const float TextBaselineFactor = 0.85f;
15 public const float MinColumnWidth = 80f;
16 public const float CharsPerEm = 7.1f;
17 private const float ReferenceProsePt = 11f;
18
19 /// <summary>Чат в лобовом Forward; плотнее метрики. Не ширина колонки (split — отдельная топология).</summary>
20 public bool ForwardHost { get; }
21 public IntercomFontsSettings Fonts { get; }
22
23 public string ProseFamily { get; }
24 public string MonoFamily { get; }
25 public string ChipFamily { get; }
26 public string ChipIdFamily { get; }
27 public string SlashMetaFamily { get; }
28 public string SlashArgsFamily { get; }
29 public string RoleFamily { get; }
30 public string GutterFamily { get; }
31
32 private float Scale { get; }
33
34 public float RoleRailWidth => (ForwardHost ? 52f : 68f) * Scale;
35 public float RoleLabelInset => 6f;
36 public float TextInset => 8f;
37 public float BodyTopPad => ForwardHost ? 6f : 10f;
38 public float SegmentGap => ForwardHost ? 4f : 8f;
39 public float RowEdgePad => 3f;
40
41 public float ProseFontSize { get; }
42 public float ProseLineHeight => (ForwardHost ? 15f : 18f) * Scale;
43 public float ProseMeasureWidthTrim => TextInset * 2f;
44 public float AttachChipFontSize => 11f * Scale;
45
46 public float SlashAccentWidth => 3f;
47 public float SlashAccentGap => 4f;
48 public float SlashMetaBodyGap => SegmentGap;
49 public float SlashIconReserve =>
50 SkiaSlashCommandStatusIconRenderer.IconSize
51 + SkiaSlashCommandStatusIconRenderer.IconMargin
52 + SlashAccentGap;
53
54 public float SlashMetaFontSize => (ForwardHost ? 10.5f : 11.5f) * Scale;
55 public float SlashArgsFontSize => ProseFontSize;
56 public float SlashMetaLineHeight => (ForwardHost ? 15f : 17f) * Scale;
57 public float SlashArgsLineHeight => (ForwardHost ? 14f : 16f) * Scale;
58 public float SlashBadgeFontSize => 10.5f * Scale;
59
60 public float RoleLabelFontSize => (ForwardHost ? 10f : 11f) * Scale;
61 public float GutterOrdinalFontSize => 10f * Scale;
62
63 private SkiaChatFeedLayout(bool forwardHost, IntercomFontsSettings fonts)
64 {
65 ForwardHost = forwardHost;
66 Fonts = fonts;
67 ProseFontSize = fonts.ResolveProsePt(forwardHost);
68 Scale = ProseFontSize / ReferenceProsePt;
69 ProseFamily = fonts.ResolveProseFamily();
70 MonoFamily = fonts.ResolveMonoFamily();
71 ChipFamily = fonts.ResolveChipFamily();
72 ChipIdFamily = fonts.ResolveChipIdFamily();
73 SlashMetaFamily = fonts.ResolveSlashMetaFamily();
74 SlashArgsFamily = fonts.ResolveSlashArgsFamily();
75 RoleFamily = fonts.ResolveRoleFamily();
76 GutterFamily = fonts.ResolveGutterFamily();
77 }
78
79 public static SkiaChatFeedLayout For(bool forwardHost, IntercomFontsSettings? fonts = null) =>
80 new(forwardHost, fonts ?? IntercomFontDefaults.Intercom);
81
82 public bool ShouldShowRoleRail(string? roleLabel, bool suppressTitle) =>
83 !suppressTitle && !string.IsNullOrWhiteSpace(roleLabel);
84
85 public FeedBodyColumn BodyColumn(float contentLeft, float contentWidth, bool includeRoleRail)
86 {
87 if (!includeRoleRail)
88 return new FeedBodyColumn(contentLeft, Math.Max(MinColumnWidth, contentWidth));
89
90 var left = contentLeft + RoleRailWidth;
91 var width = Math.Max(MinColumnWidth, contentWidth - RoleRailWidth);
92 return new FeedBodyColumn(left, width);
93 }
94
95 public SkiaChatMeasureContext NarrowMeasureContext(SkiaChatMeasureContext context, bool includeRoleRail)
96 {
97 if (!includeRoleRail)
98 return context;
99
100 var column = BodyColumn(0f, context.ContentWidth, includeRoleRail: true);
101 return context.WithContentWidth(column.Width);
102 }
103
104 public int MaxCharsForWidth(float width) =>
105 Math.Max(12, (int)(width / CharsPerEm));
106
107 public SKRect RowRect(float contentLeft, float contentWidth, float top, float height) =>
108 new(contentLeft, top, contentLeft + contentWidth, top + height);
109
110 public SKRect SegmentRect(in FeedBodyColumn column, float top, float height) =>
111 new(column.Left, top, column.Right, top + height);
112
113 public float FirstLineBaselineY(float rowTop, float titleHeight = 0f) =>
114 rowTop + titleHeight + BodyTopPad + ProseFontSize * TextBaselineFactor;
115
116 public float RoleLabelBaselineY(float rowTop) =>
117 rowTop + BodyTopPad + RoleLabelFontSize * TextBaselineFactor;
118
119 public float SlashMetaBaselineY(float rowTop) =>
120 rowTop + BodyTopPad + SlashMetaFontSize * TextBaselineFactor;
121
122 public SKPoint RichTextPaintOrigin(float textLeft, float firstLineBaselineY, float fontSize) =>
123 new(textLeft, firstLineBaselineY - fontSize * TextBaselineFactor);
124
125 public SlashTextColumn SlashTextColumn(float contentLeft, float contentWidth)
126 {
127 var body = BodyColumn(contentLeft, contentWidth, includeRoleRail: true);
128 var left = body.Left + SlashAccentWidth + SlashAccentGap;
129 var right = body.Right - SlashIconReserve;
130 var width = Math.Max(MinColumnWidth, right - left);
131 return new SlashTextColumn(left, right, width);
132 }
133
134 public float SlashRowHeight(bool hasArgs, bool hasDetail, float detailHeight)
135 {
136 var height = SlashMetaLineHeight + SlashMetaBodyGap;
137 if (hasArgs)
138 height += SlashArgsLineHeight + SlashMetaBodyGap;
139 if (hasDetail)
140 height += SlashMetaBodyGap + detailHeight;
141 return height;
142 }
143
144 public float SlashMetaBandTop(float rowTop) => rowTop;
145
146 public float SlashArgsBandTop(float rowTop) => rowTop + SlashMetaLineHeight + SlashMetaBodyGap;
147
148 public float SlashDetailBaselineY(float rowTop, bool hasArgs)
149 {
150 var bandTop = hasArgs
151 ? SlashArgsBandTop(rowTop) + SlashArgsLineHeight + SlashMetaBodyGap
152 : SlashMetaBandTop(rowTop) + SlashMetaLineHeight + SlashMetaBodyGap;
153 return bandTop + ProseFontSize * TextBaselineFactor;
154 }
155
156 public float RoleLabelMaxWidth => RoleRailWidth - RoleLabelInset * 2f;
157}
158
159internal readonly struct FeedBodyColumn(float left, float width)
160{
161 public float Left { get; } = left;
162 public float Width { get; } = width;
163 public float Right => Left + Width;
164
165 public float TextLeft(SkiaChatFeedLayout layout) => Left + layout.TextInset;
166}
167
168internal readonly struct SlashTextColumn(float left, float right, float width)
169{
170 public float Left { get; } = left;
171 public float Right { get; } = right;
172 public float Width { get; } = width;
173}
174
View only · write via MCP/CIDE