From 649f781033bae01d82b629963a4560744568f824 Mon Sep 17 00:00:00 2001 From: psyGamer Date: Sun, 27 Oct 2024 09:52:23 +0100 Subject: [PATCH] refactor(Studio): Migrate popup-menu to Skia --- Studio/CelesteStudio/Editing/Editor.cs | 18 +++--- Studio/CelesteStudio/Editing/PopupMenu.cs | 72 ++++++++++------------- Studio/CelesteStudio/Editing/Theme.cs | 23 +++++--- Studio/CelesteStudio/FontManager.cs | 19 +++--- 4 files changed, 63 insertions(+), 69 deletions(-) diff --git a/Studio/CelesteStudio/Editing/Editor.cs b/Studio/CelesteStudio/Editing/Editor.cs index 2c161bda..55090ff1 100644 --- a/Studio/CelesteStudio/Editing/Editor.cs +++ b/Studio/CelesteStudio/Editing/Editor.cs @@ -3917,26 +3917,24 @@ protected override void Draw(PaintEventArgs e, SKSurface surface, SKImageInfo im // Draw toast message box if (!string.IsNullOrWhiteSpace(toastMessage)) { - var lines = toastMessage.SplitDocumentLines(); + string[] lines = toastMessage.SplitDocumentLines(); - float width = FontManager.PopupFont.CharWidth() * lines.Select(line => line.Length).Aggregate(Math.Max); - float height = FontManager.PopupFont.LineHeight() * lines.Length; + var font = FontManager.SKPopupFont; + + float width = font.CharWidth() * lines.Select(line => line.Length).Aggregate(Math.Max); + float height = font.LineHeight() * lines.Length; float x = scrollablePosition.X + (scrollableSize.Width - width) / 2.0f; float y = scrollablePosition.Y + (scrollableSize.Height - height) / 2.0f; - float padding = Settings.Instance.Theme.PopupMenuBorderPadding; - fillPaint.ColorF = Settings.Instance.Theme.PopupMenuBg.ToSkia(); canvas.DrawRoundRect( x: x - padding, y: y - padding, w: width + padding * 2.0f, h: height + padding * 2.0f, rx: Settings.Instance.Theme.PopupMenuBorderRounding, ry: Settings.Instance.Theme.PopupMenuBorderRounding, - fillPaint); + Settings.Instance.Theme.PopupMenuBgPaint); - fillPaint.ColorF = Settings.Instance.Theme.PopupMenuFg.ToSkia(); - foreach (var line in lines) { - // TODO: Use PopupFont - canvas.DrawText(line, x, y+ Font.Offset(), Font, fillPaint); + foreach (string line in lines) { + canvas.DrawText(line, x, y + Font.Offset(), font, Settings.Instance.Theme.PopupMenuFgPaint); y += Font.LineHeight(); } } diff --git a/Studio/CelesteStudio/Editing/PopupMenu.cs b/Studio/CelesteStudio/Editing/PopupMenu.cs index 9d56b582..a943febb 100644 --- a/Studio/CelesteStudio/Editing/PopupMenu.cs +++ b/Studio/CelesteStudio/Editing/PopupMenu.cs @@ -1,9 +1,11 @@ +using CelesteStudio.Controls; using System; using System.Collections.Generic; using System.Linq; using CelesteStudio.Util; using Eto.Drawing; using Eto.Forms; +using SkiaSharp; using StudioCommunication.Util; namespace CelesteStudio.Editing; @@ -25,7 +27,7 @@ public record Entry { /// Spacing between the longest DisplayText and ExtraText of entries in characters private const int DisplayExtraPadding = 2; - private sealed class ContentDrawable : Drawable { + private sealed class ContentDrawable : SkiaDrawable { private readonly PopupMenu menu; // Specify a minimum-travel-distance to avoid very small mouse movements updating the selection @@ -42,15 +44,18 @@ public ContentDrawable(PopupMenu menu) { menu.Scroll += (_, _) => Invalidate(); } - protected override void OnPaint(PaintEventArgs e) { - e.Graphics.SetClip(GraphicsPath.GetRoundRect(new RectangleF(menu.ScrollPosition.X, menu.ScrollPosition.Y, menu.Width, menu.Height), Settings.Instance.Theme.PopupMenuBorderRounding)); - e.Graphics.FillRectangle(Settings.Instance.Theme.PopupMenuBg, menu.ScrollPosition.X, menu.ScrollPosition.Y, menu.Width, menu.Height); + protected override void Draw(PaintEventArgs e, SKSurface surface, SKImageInfo imageInfo) { + surface.Canvas.Clear(); if (menu.shownEntries.Length == 0) { return; } - var font = FontManager.PopupFont; + var backgroundRect = new SKRect(menu.ScrollPosition.X, menu.ScrollPosition.Y, menu.ScrollPosition.X + menu.Width, menu.ScrollPosition.Y + menu.Height); + surface.Canvas.ClipRoundRect(new SKRoundRect(backgroundRect, Settings.Instance.Theme.PopupMenuBorderRounding), antialias: true); + surface.Canvas.DrawRect(backgroundRect, Settings.Instance.Theme.PopupMenuBgPaint); + + var font = FontManager.SKPopupFont; int maxDisplayLen = menu.shownEntries.Select(entry => entry.DisplayText.Length).Aggregate(Math.Max); float width = menu.ContentWidth - Settings.Instance.Theme.PopupMenuBorderPadding * 2.0f; @@ -60,40 +65,30 @@ protected override void OnPaint(PaintEventArgs e) { int minRow = Math.Max(0, (int)(menu.ScrollPosition.Y / height) - rowCullOverhead); int maxRow = Math.Min(menu.shownEntries.Length - 1, (int)((menu.ScrollPosition.Y + menu.ClientSize.Height) / height) + rowCullOverhead); - using var displayEnabledBrush = new SolidBrush(Settings.Instance.Theme.PopupMenuFg); - using var displayDisabledBrush = new SolidBrush(Settings.Instance.Theme.PopupMenuFgDisabled); - using var extraBrush = new SolidBrush(Settings.Instance.Theme.PopupMenuFgExtra); - for (int row = minRow; row <= maxRow; row++) { var entry = menu.shownEntries[row]; + // Highlight selected entry if (row == menu.SelectedEntry && !entry.Disabled) { - e.Graphics.FillPath( - Settings.Instance.Theme.PopupMenuSelected, - GraphicsPath.GetRoundRect( - new RectangleF(Settings.Instance.Theme.PopupMenuBorderPadding, - row * height + Settings.Instance.Theme.PopupMenuBorderPadding + Settings.Instance.Theme.PopupMenuEntrySpacing / 2.0f, - width, - height - Settings.Instance.Theme.PopupMenuEntrySpacing), - Settings.Instance.Theme.PopupMenuEntryRounding)); + surface.Canvas.DrawRoundRect( + x: Settings.Instance.Theme.PopupMenuBorderPadding, + y: row * height + Settings.Instance.Theme.PopupMenuBorderPadding + Settings.Instance.Theme.PopupMenuEntrySpacing / 2.0f, + w: width, + h: height - Settings.Instance.Theme.PopupMenuEntrySpacing, + rx: Settings.Instance.Theme.PopupMenuEntryRounding, + ry: Settings.Instance.Theme.PopupMenuEntryRounding, + Settings.Instance.Theme.PopupMenuSelectedPaint); } - var displayBrush = entry.Disabled ? displayDisabledBrush : displayEnabledBrush; - e.Graphics.DrawText(font, displayBrush, - Settings.Instance.Theme.PopupMenuBorderPadding + Settings.Instance.Theme.PopupMenuEntryHorizontalPadding, - Settings.Instance.Theme.PopupMenuBorderPadding + row * height + Settings.Instance.Theme.PopupMenuEntryVerticalPadding + Settings.Instance.Theme.PopupMenuEntrySpacing / 2.0f, - entry.DisplayText); - e.Graphics.DrawText(font, extraBrush, - Settings.Instance.Theme.PopupMenuBorderPadding + Settings.Instance.Theme.PopupMenuEntryHorizontalPadding + font.CharWidth() * (maxDisplayLen + DisplayExtraPadding), - Settings.Instance.Theme.PopupMenuBorderPadding + row * height + Settings.Instance.Theme.PopupMenuEntryVerticalPadding + Settings.Instance.Theme.PopupMenuEntrySpacing / 2.0f, - entry.ExtraText); - } - - if (Eto.Platform.Instance.IsGtk) { - Size = new(menu.ContentWidth, menu.ContentHeight); + surface.Canvas.DrawText(entry.DisplayText, + x: Settings.Instance.Theme.PopupMenuBorderPadding + Settings.Instance.Theme.PopupMenuEntryHorizontalPadding, + y: Settings.Instance.Theme.PopupMenuBorderPadding + row * height + Settings.Instance.Theme.PopupMenuEntryVerticalPadding + Settings.Instance.Theme.PopupMenuEntrySpacing / 2.0f + font.Offset(), + font, entry.Disabled ? Settings.Instance.Theme.PopupMenuFgDisabledPaint : Settings.Instance.Theme.PopupMenuFgPaint); + surface.Canvas.DrawText(entry.ExtraText, + x: Settings.Instance.Theme.PopupMenuBorderPadding + Settings.Instance.Theme.PopupMenuEntryHorizontalPadding + font.CharWidth() * (maxDisplayLen + DisplayExtraPadding), + y: Settings.Instance.Theme.PopupMenuBorderPadding + row * height + Settings.Instance.Theme.PopupMenuEntryVerticalPadding + Settings.Instance.Theme.PopupMenuEntrySpacing / 2.0f + font.Offset(), + font, Settings.Instance.Theme.PopupMenuFgExtraPaint); } - - base.OnPaint(e); } protected override void OnMouseMove(MouseEventArgs e) { @@ -210,7 +205,7 @@ public int ContentHeight { get => contentHeight; } - public int EntryHeight => (int)(FontManager.PopupFont.LineHeight() + Settings.Instance.Theme.PopupMenuEntryVerticalPadding * 2.0f + Settings.Instance.Theme.PopupMenuEntrySpacing); + public int EntryHeight => (int)(FontManager.SKPopupFont.LineHeight() + Settings.Instance.Theme.PopupMenuEntryVerticalPadding * 2.0f + Settings.Instance.Theme.PopupMenuEntrySpacing); private Entry[] shownEntries = []; private readonly ContentDrawable drawable; @@ -248,7 +243,7 @@ public void Recalc() { // Calculate content bounds. Calculate height first to account for scroll bar contentHeight = shownEntries.Length * EntryHeight + Settings.Instance.Theme.PopupMenuBorderPadding * 2; - var font = FontManager.PopupFont; + var font = FontManager.SKPopupFont; int maxDisplayLen = shownEntries.Select(entry => entry.DisplayText.Length).Aggregate(Math.Max); int maxExtraLen = shownEntries.Select(entry => entry.ExtraText.Length).Aggregate(Math.Max); if (maxExtraLen != 0) { @@ -257,14 +252,7 @@ public void Recalc() { contentWidth = (int)(font.CharWidth() * (maxDisplayLen + maxExtraLen) + Settings.Instance.Theme.PopupMenuEntryHorizontalPadding * 2.0f + Settings.Instance.Theme.PopupMenuBorderPadding * 2); - if (Eto.Platform.Instance.IsGtk) { - // If the content isn't large enough to require a scrollbar, GTK will just have the background be black instead of transparent. - // Even weirder, once it was scrollable once, the black background will never come back... - // The proper size is set at the end of ContentDrawable.Paint() - drawable.Size = new(ContentWidth, ContentHeight + 1); - } else { - drawable.Size = new(ContentWidth, ContentHeight); - } + drawable.Size = new(ContentWidth, ContentHeight); drawable.Invalidate(); } diff --git a/Studio/CelesteStudio/Editing/Theme.cs b/Studio/CelesteStudio/Editing/Theme.cs index 0345e22f..f88e050f 100644 --- a/Studio/CelesteStudio/Editing/Theme.cs +++ b/Studio/CelesteStudio/Editing/Theme.cs @@ -6,12 +6,7 @@ namespace CelesteStudio.Editing; -public struct Style(Color foregroundColor, Color? backgroundColor = null, FontStyle fontStyle = FontStyle.None) { - public Color ForegroundColor = foregroundColor; - public Color? BackgroundColor = backgroundColor; - - public FontStyle FontStyle = fontStyle; - +public record struct Style(Color ForegroundColor, Color? BackgroundColor = null, FontStyle FontStyle = FontStyle.None) { public StylePaint CreatePaint() => new(CreateForegroundPaint(), CreateBackgroundPaint(), FontStyle); public SKPaint CreateForegroundPaint(SKPaintStyle style = SKPaintStyle.Fill) => @@ -27,7 +22,7 @@ public void Dispose() { } } -public struct Theme() { +public struct Theme { // Editor public Color Background; public Color Caret; @@ -77,7 +72,7 @@ public struct Theme() { // Cache SKPaints private StylePaint? _actionPaint, _anglePaint, _breakpointPaint, _savestateBreakpointPaint, _delimiter, _command, _frame, _comment; - private SKPaint? _commentBox; + private SKPaint? _commentBox, _popupMenuFgPaint, _popupMenuFgDisabledPaint, _popupMenuFgExtraPaint, _popupMenuBgPaint, _popupMenuSelectedPaint; public StylePaint ActionPaint => _actionPaint ??= Action.CreatePaint(); public StylePaint AnglePaint => _anglePaint ??= Angle.CreatePaint(); @@ -88,6 +83,11 @@ public struct Theme() { public StylePaint FramePaint => _frame ??= Frame.CreatePaint(); public StylePaint CommentPaint => _comment ??= Comment.CreatePaint(); public SKPaint CommentBoxPaint => _commentBox ??= Comment.CreateForegroundPaint(SKPaintStyle.Stroke); + public SKPaint PopupMenuFgPaint => _popupMenuFgPaint ??= new SKPaint { ColorF = PopupMenuFg.ToSkia(), Style = SKPaintStyle.Fill, IsAntialias = true }; + public SKPaint PopupMenuFgDisabledPaint => _popupMenuFgDisabledPaint ??= new SKPaint { ColorF = PopupMenuFgDisabled.ToSkia(), Style = SKPaintStyle.Fill, IsAntialias = true }; + public SKPaint PopupMenuFgExtraPaint => _popupMenuFgExtraPaint ??= new SKPaint { ColorF = PopupMenuFgExtra.ToSkia(), Style = SKPaintStyle.Fill, IsAntialias = true }; + public SKPaint PopupMenuBgPaint => _popupMenuBgPaint ??= new SKPaint { ColorF = PopupMenuBg.ToSkia(), Style = SKPaintStyle.Fill, IsAntialias = true }; + public SKPaint PopupMenuSelectedPaint => _popupMenuSelectedPaint ??= new SKPaint { ColorF = PopupMenuSelected.ToSkia(), Style = SKPaintStyle.Fill, IsAntialias = true }; public void InvalidateCache() { _actionPaint?.Dispose(); @@ -99,9 +99,14 @@ public void InvalidateCache() { _frame?.Dispose(); _comment?.Dispose(); _commentBox?.Dispose(); + _popupMenuFgPaint?.Dispose(); + _popupMenuFgDisabledPaint?.Dispose(); + _popupMenuFgExtraPaint?.Dispose(); + _popupMenuBgPaint?.Dispose(); + _popupMenuSelectedPaint?.Dispose(); _actionPaint = _anglePaint = _breakpointPaint = _savestateBreakpointPaint = _delimiter = _command = _frame = _comment = null; - _commentBox = null; + _commentBox = _popupMenuFgPaint = _popupMenuFgDisabledPaint = _popupMenuFgExtraPaint = _popupMenuBgPaint = _popupMenuSelectedPaint = null; } public bool DarkMode; diff --git a/Studio/CelesteStudio/FontManager.cs b/Studio/CelesteStudio/FontManager.cs index f962fce7..24dfbec4 100644 --- a/Studio/CelesteStudio/FontManager.cs +++ b/Studio/CelesteStudio/FontManager.cs @@ -17,20 +17,20 @@ public static class FontManager { public const string FontFamilyBuiltinDisplayName = "JetBrains Mono (builtin)"; #endif - private static Font? editorFontRegular, editorFontBold, editorFontItalic, editorFontBoldItalic, statusFont, popupFont; - private static SKFont? skEditorFontRegular, skEditorFontBold, skEditorFontItalic, skEditorFontBoldItalic; + private static Font? editorFontRegular, editorFontBold, editorFontItalic, editorFontBoldItalic, statusFont; + private static SKFont? skEditorFontRegular, skEditorFontBold, skEditorFontItalic, skEditorFontBoldItalic, skPopupFont; public static Font EditorFontRegular => editorFontRegular ??= CreateEditor(FontStyle.None); public static Font EditorFontBold => editorFontBold ??= CreateEditor(FontStyle.Bold); public static Font EditorFontItalic => editorFontItalic ??= CreateEditor(FontStyle.Italic); public static Font EditorFontBoldItalic => editorFontBoldItalic ??= CreateEditor(FontStyle.Bold | FontStyle.Italic); public static Font StatusFont => statusFont ??= CreateStatus(); - public static Font PopupFont => popupFont ??= CreatePopup(); - public static SKFont SKEditorFontRegular => skEditorFontRegular ??= CreateSKFont(Settings.Instance.FontFamily, Settings.Instance.EditorFontSize * Settings.Instance.FontZoom, FontStyle.None); + public static SKFont SKEditorFontRegular => skEditorFontRegular ??= CreateSKFont(Settings.Instance.FontFamily, Settings.Instance.EditorFontSize * Settings.Instance.FontZoom); public static SKFont SKEditorFontBold => skEditorFontBold ??= CreateSKFont(Settings.Instance.FontFamily, Settings.Instance.EditorFontSize * Settings.Instance.FontZoom, FontStyle.Bold); public static SKFont SKEditorFontItalic => skEditorFontItalic ??= CreateSKFont(Settings.Instance.FontFamily, Settings.Instance.EditorFontSize * Settings.Instance.FontZoom, FontStyle.Italic); public static SKFont SKEditorFontBoldItalic => skEditorFontBoldItalic ??= CreateSKFont(Settings.Instance.FontFamily, Settings.Instance.EditorFontSize * Settings.Instance.FontZoom, FontStyle.Bold | FontStyle.Italic); + public static SKFont SKPopupFont => skPopupFont ??= CreateSKFont(Settings.Instance.FontFamily, Settings.Instance.PopupFontSize); private static FontFamily? builtinFontFamily; public static Font CreateFont(string fontFamily, float size, FontStyle style = FontStyle.None) { @@ -51,7 +51,7 @@ public static Font CreateFont(string fontFamily, float size, FontStyle style = F } } - public static SKFont CreateSKFont(string fontFamily, float size, FontStyle style) { + public static SKFont CreateSKFont(string fontFamily, float size, FontStyle style = FontStyle.None) { // TODO: Don't hardcode this const float dpi = 96.0f / 72.0f; @@ -139,14 +139,17 @@ public static void OnFontChanged() { editorFontItalic?.Dispose(); editorFontBoldItalic?.Dispose(); statusFont?.Dispose(); - popupFont?.Dispose(); charWidthCache.Clear(); - editorFontRegular = editorFontBold = editorFontItalic = editorFontBoldItalic = statusFont = popupFont = null; + editorFontRegular = editorFontBold = editorFontItalic = editorFontBoldItalic = statusFont = null; skEditorFontRegular?.Dispose(); + skEditorFontBold?.Dispose(); + skEditorFontItalic?.Dispose(); + skEditorFontBoldItalic?.Dispose(); + skPopupFont?.Dispose(); - skEditorFontRegular = null; + skEditorFontRegular = skEditorFontBold = skEditorFontItalic = skEditorFontBoldItalic = skPopupFont = null; } private static Font CreateEditor(FontStyle style) => CreateFont(Settings.Instance.FontFamily, Settings.Instance.EditorFontSize * Settings.Instance.FontZoom, style);