| 1 | #nullable enable |
| 2 | |
| 3 | using SkiaSharp; |
| 4 | |
| 5 | namespace CascadeIDE.Views.SkiaKit; |
| 6 | |
| 7 | /// <summary>Семантика pill-рамки (CodeAnchors, slash TCI, attach-chip, …).</summary> |
| 8 | internal enum SkiaStatusChipSeverity |
| 9 | { |
| 10 | None = 0, |
| 11 | Success = 1, |
| 12 | Warning = 2, |
| 13 | Error = 3, |
| 14 | Pending = 4, |
| 15 | Info = 5, |
| 16 | } |
| 17 | |
| 18 | /// <summary>Палитра pill: рамка, заливка, иконка, акцентный текст снаружи/внутри.</summary> |
| 19 | internal readonly record struct SkiaStatusChipColors( |
| 20 | SKColor Border, |
| 21 | SKColor Fill, |
| 22 | SKColor Icon, |
| 23 | SKColor Accent); |
| 24 | |
| 25 | /// <summary> |
| 26 | /// Примитив Skia: скруглённая рамка + опциональная иконка (как attach-chip / slash TCI). |
| 27 | /// Текст рисует вызывающий код с акцентом из палитры. |
| 28 | /// </summary> |
| 29 | internal static class SkiaStatusChip |
| 30 | { |
| 31 | public const float PadX = 8f; |
| 32 | public const float PadY = 4f; |
| 33 | public const float IconBox = 14f; |
| 34 | public const float IconGap = 5f; |
| 35 | public const float Corner = 6f; |
| 36 | public const float MinHeight = 22f; |
| 37 | public const float BorderStroke = 1.25f; |
| 38 | |
| 39 | /// <summary>Ширина зоны иконки слева от начала текста (legacy left placement).</summary> |
| 40 | public const float IconLeadingOverhang = PadX + IconBox + IconGap; |
| 41 | |
| 42 | /// <summary>Доп. отступ слева в layout при <see cref="SkiaStatusChipIconPlacement.Left"/>.</summary> |
| 43 | public const float LeadingChipLayoutGutter = IconLeadingOverhang + BorderStroke + 2f; |
| 44 | |
| 45 | public static float ContentWidthPadding(SkiaStatusChipIconPlacement iconPlacement) => |
| 46 | PadX * 2f + IconZoneWidth(iconPlacement); |
| 47 | |
| 48 | public static float ComputeContentWidth(float labelWidth, SkiaStatusChipIconPlacement iconPlacement) => |
| 49 | ContentWidthPadding(iconPlacement) + labelWidth; |
| 50 | |
| 51 | /// <summary>Pill вокруг slash-текста, начинающегося в <paramref name="textLeft"/>.</summary> |
| 52 | public static SKRect ComputeRectAroundTextStart( |
| 53 | float textLeft, |
| 54 | float textTop, |
| 55 | float lineHeight, |
| 56 | float labelWidth, |
| 57 | SkiaStatusChipIconPlacement iconPlacement = SkiaStatusChipIconPlacement.Right) |
| 58 | { |
| 59 | var chipW = ComputeContentWidth(labelWidth, iconPlacement); |
| 60 | var chipH = Math.Max(lineHeight + PadY * 2f, MinHeight); |
| 61 | var chipTop = textTop + (lineHeight - chipH) * 0.5f; |
| 62 | var chipLeft = iconPlacement switch |
| 63 | { |
| 64 | SkiaStatusChipIconPlacement.Left => textLeft - PadX - IconBox - IconGap, |
| 65 | _ => textLeft - PadX, |
| 66 | }; |
| 67 | return new SKRect(chipLeft, chipTop, chipLeft + chipW, chipTop + chipH); |
| 68 | } |
| 69 | |
| 70 | public static float ContentLeftInRect( |
| 71 | SKRect chipRect, |
| 72 | SkiaStatusChipIconPlacement iconPlacement = SkiaStatusChipIconPlacement.Left) => |
| 73 | iconPlacement switch |
| 74 | { |
| 75 | SkiaStatusChipIconPlacement.Right => chipRect.Left + PadX, |
| 76 | SkiaStatusChipIconPlacement.HighlightOnly => chipRect.Left + PadX, |
| 77 | _ => chipRect.Left + PadX + IconBox + IconGap, |
| 78 | }; |
| 79 | |
| 80 | public static SKPoint IconCenterInRect( |
| 81 | SKRect chipRect, |
| 82 | SkiaStatusChipIconPlacement iconPlacement = SkiaStatusChipIconPlacement.Left) => |
| 83 | iconPlacement switch |
| 84 | { |
| 85 | SkiaStatusChipIconPlacement.Left => new(chipRect.Left + PadX + IconBox * 0.5f, chipRect.MidY), |
| 86 | SkiaStatusChipIconPlacement.Right => new(chipRect.Right - PadX - IconBox * 0.5f, chipRect.MidY), |
| 87 | _ => new(chipRect.MidX, chipRect.MidY), |
| 88 | }; |
| 89 | |
| 90 | public static float ComputeContentWidth(float labelWidth) => |
| 91 | ComputeContentWidth(labelWidth, SkiaStatusChipIconPlacement.Left); |
| 92 | |
| 93 | public static SKRect ExpandClipForChip(SKRect clipRect, SKRect chipRect) |
| 94 | { |
| 95 | var union = SKRect.Union(clipRect, chipRect); |
| 96 | union.Inflate(BorderStroke + 1f, BorderStroke + 1f); |
| 97 | return union; |
| 98 | } |
| 99 | |
| 100 | public static void DrawFrame(SKCanvas canvas, SKRect chipRect, in SkiaStatusChipColors colors) |
| 101 | { |
| 102 | using var fillPaint = new SKPaint { Color = colors.Fill, IsAntialias = true, Style = SKPaintStyle.Fill }; |
| 103 | canvas.DrawRoundRect(chipRect, Corner, Corner, fillPaint); |
| 104 | using var borderPaint = new SKPaint |
| 105 | { |
| 106 | Color = colors.Border, |
| 107 | IsAntialias = true, |
| 108 | Style = SKPaintStyle.Stroke, |
| 109 | StrokeWidth = BorderStroke, |
| 110 | }; |
| 111 | canvas.DrawRoundRect(chipRect, Corner, Corner, borderPaint); |
| 112 | } |
| 113 | |
| 114 | public static void DrawIcon( |
| 115 | SKCanvas canvas, |
| 116 | SKPoint center, |
| 117 | SkiaStatusChipSeverity severity, |
| 118 | SKColor color, |
| 119 | float fontSize) |
| 120 | { |
| 121 | var glyph = GlyphFor(severity); |
| 122 | if (glyph.Length == 0) |
| 123 | return; |
| 124 | |
| 125 | DrawIconGlyph(canvas, center, glyph, color, fontSize); |
| 126 | } |
| 127 | |
| 128 | public static void DrawIconGlyph( |
| 129 | SKCanvas canvas, |
| 130 | SKPoint center, |
| 131 | string glyph, |
| 132 | SKColor color, |
| 133 | float fontSize) |
| 134 | { |
| 135 | if (string.IsNullOrEmpty(glyph)) |
| 136 | return; |
| 137 | |
| 138 | using var font = new SKFont( |
| 139 | SKTypeface.FromFamilyName("Segoe UI Symbol", SKFontStyle.Normal), |
| 140 | fontSize); |
| 141 | using var paint = new SKPaint { IsAntialias = true, Color = color }; |
| 142 | canvas.DrawText(glyph, center.X, center.Y + 4f, SKTextAlign.Center, font, paint); |
| 143 | } |
| 144 | |
| 145 | public static string GlyphFor(SkiaStatusChipSeverity severity) => |
| 146 | severity switch |
| 147 | { |
| 148 | SkiaStatusChipSeverity.Success => "\u2713", |
| 149 | SkiaStatusChipSeverity.Warning => "\u26A0", |
| 150 | SkiaStatusChipSeverity.Error => "\u2715", |
| 151 | SkiaStatusChipSeverity.Pending => "\u23F1", |
| 152 | SkiaStatusChipSeverity.Info => "\u2139", |
| 153 | _ => "", |
| 154 | }; |
| 155 | |
| 156 | public static void DrawChrome( |
| 157 | SKCanvas canvas, |
| 158 | SKRect chipRect, |
| 159 | ISkiaKitPaintTheme theme, |
| 160 | SkiaStatusChipSeverity severity, |
| 161 | float iconFontSize, |
| 162 | SkiaStatusChipIconPlacement iconPlacement = SkiaStatusChipIconPlacement.Right) |
| 163 | { |
| 164 | if (severity == SkiaStatusChipSeverity.None) |
| 165 | return; |
| 166 | |
| 167 | var colors = ResolveColors(theme, severity); |
| 168 | DrawFrame(canvas, chipRect, colors); |
| 169 | if (iconPlacement != SkiaStatusChipIconPlacement.HighlightOnly) |
| 170 | DrawIcon(canvas, IconCenterInRect(chipRect, iconPlacement), severity, colors.Icon, iconFontSize); |
| 171 | } |
| 172 | |
| 173 | public static SkiaStatusChipColors ResolveColors( |
| 174 | ISkiaKitPaintTheme theme, |
| 175 | SkiaStatusChipSeverity severity, |
| 176 | SKColor? mutedContent = null) => |
| 177 | ResolveColors(theme, severity, mutedContent ?? theme.EmptyHint); |
| 178 | |
| 179 | private static float IconZoneWidth(SkiaStatusChipIconPlacement iconPlacement) => |
| 180 | iconPlacement == SkiaStatusChipIconPlacement.HighlightOnly ? 0f : IconBox + IconGap; |
| 181 | |
| 182 | private static SkiaStatusChipColors ResolveColors( |
| 183 | ISkiaKitPaintTheme theme, |
| 184 | SkiaStatusChipSeverity severity, |
| 185 | SKColor mutedContent) => |
| 186 | severity switch |
| 187 | { |
| 188 | SkiaStatusChipSeverity.Success => new( |
| 189 | new SKColor(72, 160, 110, 200), |
| 190 | SkiaKitColor.Blend(theme.Surface, new SKColor(72, 160, 110), 0.12f), |
| 191 | new SKColor(100, 200, 140), |
| 192 | new SKColor(120, 200, 255)), |
| 193 | SkiaStatusChipSeverity.Warning => new( |
| 194 | new SKColor(200, 150, 70, 210), |
| 195 | SkiaKitColor.Blend(theme.Surface, new SKColor(200, 150, 70), 0.14f), |
| 196 | new SKColor(230, 180, 90), |
| 197 | new SKColor(140, 190, 255)), |
| 198 | SkiaStatusChipSeverity.Error => new( |
| 199 | new SKColor(190, 90, 90, 200), |
| 200 | SkiaKitColor.Blend(theme.Surface, new SKColor(190, 90, 90), 0.12f), |
| 201 | new SKColor(220, 110, 110), |
| 202 | SkiaKitColor.Blend(theme.EmptyHint, theme.Content, 0.35f)), |
| 203 | SkiaStatusChipSeverity.Pending => new( |
| 204 | SkiaKitColor.Blend(theme.Border, mutedContent, 0.5f), |
| 205 | SkiaKitColor.Blend(theme.Surface, theme.Border, 0.2f), |
| 206 | mutedContent, |
| 207 | SkiaKitColor.Blend(theme.Content, theme.HoverBorder, 0.45f)), |
| 208 | SkiaStatusChipSeverity.Info => new( |
| 209 | SkiaKitColor.Blend(theme.Border, theme.EmptyHint, 0.5f), |
| 210 | SkiaKitColor.Blend(theme.Surface, theme.Border, 0.2f), |
| 211 | theme.EmptyHint, |
| 212 | SkiaKitColor.Blend(theme.Content, theme.HoverBorder, 0.45f)), |
| 213 | _ => new(theme.Border, theme.Surface, theme.EmptyHint, theme.Content), |
| 214 | }; |
| 215 | } |
| 216 | |