| 1 | #nullable enable |
| 2 | using CascadeIDE.Features.Chat; |
| 3 | using CascadeIDE.Models; |
| 4 | using CascadeIDE.Services; |
| 5 | using SkiaSharp; |
| 6 | |
| 7 | namespace CascadeIDE.Views.Chat.Skia; |
| 8 | |
| 9 | /// <summary>Skia toolbar Intercom (ADR 0123 фаза 1).</summary> |
| 10 | internal static class SkiaChatChromeRenderer |
| 11 | { |
| 12 | public const float ToolbarHeight = 36f; |
| 13 | |
| 14 | /// <summary>Toolbar с подзаголовком (тема / линия / сообщений).</summary> |
| 15 | public const float ToolbarWithStatusHeight = 52f; |
| 16 | |
| 17 | /// <summary>Заголовок режима overview (картотека) — не в скролле ленты.</summary> |
| 18 | public const float OverviewCatalogBandHeight = 44f; |
| 19 | |
| 20 | public static float ResolveToolbarHeight(bool forwardHost, bool showStatusSubtitle) => |
| 21 | forwardHost ? (showStatusSubtitle ? ToolbarWithStatusHeight : ToolbarHeight) : 0f; |
| 22 | |
| 23 | public static float ResolveTopChromeHeight(bool forwardHost, bool showOverviewCatalog, bool showStatusSubtitle) => |
| 24 | ResolveTopChromeHeight(forwardHost, showOverviewCatalog, showStatusSubtitle, overviewMode: showOverviewCatalog, topicCount: 0); |
| 25 | |
| 26 | public static float ResolveTopChromeHeight( |
| 27 | bool forwardHost, |
| 28 | bool showOverviewCatalog, |
| 29 | bool showStatusSubtitle, |
| 30 | bool overviewMode, |
| 31 | int topicCount, |
| 32 | bool hasScopeStrip = false) => |
| 33 | SkiaIntercomNavigationChrome.ResolveTopChromeHeight( |
| 34 | forwardHost, |
| 35 | showOverviewCatalog, |
| 36 | showStatusSubtitle, |
| 37 | overviewMode, |
| 38 | topicCount, |
| 39 | hasScopeStrip); |
| 40 | |
| 41 | public static void Draw( |
| 42 | SKCanvas canvas, |
| 43 | float width, |
| 44 | SkiaChatTheme theme, |
| 45 | string title, |
| 46 | bool overviewMode, |
| 47 | bool isLoading, |
| 48 | string? loadingText, |
| 49 | string? statusSubtitle, |
| 50 | bool showNavigatorToggle, |
| 51 | bool navigatorVisible, |
| 52 | out SKRect overviewButtonBounds, |
| 53 | out SKRect navigatorToggleBounds, |
| 54 | IntercomFontsSettings? fonts = null) |
| 55 | { |
| 56 | navigatorToggleBounds = default; |
| 57 | fonts ??= IntercomFontDefaults.Intercom; |
| 58 | var titlePt = fonts.ResolveChromeTitlePt(); |
| 59 | var subtitlePt = fonts.ResolveChromeSubtitlePt(); |
| 60 | var toolbarHeight = ResolveToolbarHeight(forwardHost: true, showStatusSubtitle: !string.IsNullOrWhiteSpace(statusSubtitle)); |
| 61 | canvas.DrawRect(new SKRect(0, 0, width, toolbarHeight), new SKPaint |
| 62 | { |
| 63 | Color = theme.Surface, |
| 64 | IsStroke = false, |
| 65 | }); |
| 66 | canvas.DrawLine(0, toolbarHeight - 0.5f, width, toolbarHeight - 0.5f, new SKPaint |
| 67 | { |
| 68 | Color = theme.Border, |
| 69 | StrokeWidth = 1, |
| 70 | IsAntialias = true, |
| 71 | }); |
| 72 | |
| 73 | using var titleFont = new SKFont(SKTypeface.FromFamilyName(fonts.ResolveProseFamily(), SKFontStyle.Bold), titlePt); |
| 74 | using var titlePaint = new SKPaint { IsAntialias = true, Color = theme.Content }; |
| 75 | canvas.DrawText(title, 12, 12 + titlePt * 0.85f, SKTextAlign.Left, titleFont, titlePaint); |
| 76 | |
| 77 | if (!string.IsNullOrWhiteSpace(statusSubtitle)) |
| 78 | { |
| 79 | using var statusFont = new SKFont(SKTypeface.FromFamilyName(fonts.ResolveProseFamily()), subtitlePt); |
| 80 | using var statusPaint = new SKPaint { IsAntialias = true, Color = theme.MutedContent }; |
| 81 | canvas.DrawText( |
| 82 | Truncate(statusSubtitle, 96), |
| 83 | 12, |
| 84 | 12 + titlePt * 0.85f + subtitlePt + 6f, |
| 85 | SKTextAlign.Left, |
| 86 | statusFont, |
| 87 | statusPaint); |
| 88 | } |
| 89 | |
| 90 | var topicsLabel = overviewMode ? "Темы ✓" : "Темы"; |
| 91 | var btnPt = Math.Max(10f, subtitlePt + 1f); |
| 92 | using var btnFont = new SKFont(SKTypeface.FromFamilyName(fonts.ResolveProseFamily()), btnPt); |
| 93 | using var btnPaint = new SKPaint { IsAntialias = true, Color = theme.Role }; |
| 94 | var topicsWidth = btnFont.MeasureText(topicsLabel) + 16; |
| 95 | var right = width - 12f; |
| 96 | overviewButtonBounds = new SKRect(right - topicsWidth, 6, right, 28); |
| 97 | right = overviewButtonBounds.Left - 8f; |
| 98 | |
| 99 | if (showNavigatorToggle) |
| 100 | { |
| 101 | var navLabel = navigatorVisible ? "Nav ✓" : "☰ Nav"; |
| 102 | var navWidth = btnFont.MeasureText(navLabel) + 16; |
| 103 | navigatorToggleBounds = new SKRect(right - navWidth, 6, right, 28); |
| 104 | right = navigatorToggleBounds.Left - 8f; |
| 105 | using var navBg = new SKPaint { IsAntialias = true, Color = SkiaKit.SkiaKitColor.Blend(theme.Surface, theme.Border, 0.35f) }; |
| 106 | canvas.DrawRoundRect(navigatorToggleBounds, 6, 6, navBg); |
| 107 | canvas.DrawText(navLabel, navigatorToggleBounds.MidX, 20, SKTextAlign.Center, btnFont, btnPaint); |
| 108 | } |
| 109 | if (isLoading && !string.IsNullOrWhiteSpace(loadingText)) |
| 110 | { |
| 111 | using var chipFont = new SKFont(SKTypeface.FromFamilyName(fonts.ResolveProseFamily()), subtitlePt); |
| 112 | var chipText = Truncate(loadingText, 28); |
| 113 | var chipWidth = chipFont.MeasureText(chipText) + 14; |
| 114 | var chipRect = new SKRect(right - chipWidth, 8, right, 28); |
| 115 | DrawChip(canvas, chipRect, chipText, theme, chipFont); |
| 116 | } |
| 117 | |
| 118 | using var btnBg = new SKPaint { IsAntialias = true, Color = SkiaKit.SkiaKitColor.Blend(theme.Surface, theme.Border, 0.35f) }; |
| 119 | canvas.DrawRoundRect(overviewButtonBounds, 6, 6, btnBg); |
| 120 | canvas.DrawText(topicsLabel, overviewButtonBounds.MidX, 20, SKTextAlign.Center, btnFont, btnPaint); |
| 121 | } |
| 122 | |
| 123 | /// <summary>Полоса «Картотека тем» под toolbar — заголовок режима, не карточка в ленте.</summary> |
| 124 | public static void DrawOverviewCatalogBand( |
| 125 | SKCanvas canvas, |
| 126 | float width, |
| 127 | float top, |
| 128 | SkiaChatTheme theme, |
| 129 | int topicCount, |
| 130 | IntercomFontsSettings? fonts = null) |
| 131 | { |
| 132 | fonts ??= IntercomFontDefaults.Intercom; |
| 133 | var headingPt = fonts.ResolveChromeHeadingPt(); |
| 134 | var hintPt = fonts.ResolveChromeSubtitlePt(); |
| 135 | var bottom = top + OverviewCatalogBandHeight; |
| 136 | using (var fill = new SKPaint |
| 137 | { |
| 138 | Color = SkiaKit.SkiaKitColor.Blend(theme.Surface, theme.Border, 0.08f), |
| 139 | IsAntialias = true, |
| 140 | Style = SKPaintStyle.Fill, |
| 141 | }) |
| 142 | canvas.DrawRect(new SKRect(0, top, width, bottom), fill); |
| 143 | |
| 144 | using (var line = new SKPaint |
| 145 | { |
| 146 | Color = theme.Border, |
| 147 | StrokeWidth = 1, |
| 148 | IsAntialias = true, |
| 149 | }) |
| 150 | canvas.DrawLine(0, bottom - 0.5f, width, bottom - 0.5f, line); |
| 151 | |
| 152 | using var modeFont = new SKFont(SKTypeface.FromFamilyName(fonts.ResolveProseFamily(), SKFontStyle.Bold), headingPt); |
| 153 | using var modePaint = new SKPaint { IsAntialias = true, Color = theme.Content }; |
| 154 | canvas.DrawText("Картотека тем", 12, top + headingPt * 0.85f + 6f, SKTextAlign.Left, modeFont, modePaint); |
| 155 | |
| 156 | var hint = ChatThreadOverviewPresentation.FormatCatalogHint(topicCount) + " · " + |
| 157 | ChatThreadOverviewPresentation.CatalogFooter; |
| 158 | using var hintFont = new SKFont(SKTypeface.FromFamilyName(fonts.ResolveProseFamily()), hintPt); |
| 159 | using var hintPaint = new SKPaint { IsAntialias = true, Color = theme.MutedContent }; |
| 160 | canvas.DrawText(hint, 12, top + headingPt * 0.85f + hintPt + 14f, SKTextAlign.Left, hintFont, hintPaint); |
| 161 | } |
| 162 | |
| 163 | private static void DrawChip(SKCanvas canvas, SKRect rect, string text, SkiaChatTheme theme, SKFont font) |
| 164 | { |
| 165 | using var bg = new SKPaint { IsAntialias = true, Color = SkiaKit.SkiaKitColor.Blend(theme.BubbleAssistant, theme.Border, 0.25f) }; |
| 166 | canvas.DrawRoundRect(rect, 8, 8, bg); |
| 167 | using var paint = new SKPaint { IsAntialias = true, Color = theme.MutedContent }; |
| 168 | canvas.DrawText(text, rect.Left + 7, 21, SKTextAlign.Left, font, paint); |
| 169 | } |
| 170 | |
| 171 | private static string Truncate(string text, int maxChars) => |
| 172 | text.Length <= maxChars ? text : text[..maxChars] + "…"; |
| 173 | } |
| 174 | |