Skip to content

Commit

Permalink
refactor(Studio): Migrate popup-menu to Skia
Browse files Browse the repository at this point in the history
  • Loading branch information
psyGamer committed Oct 27, 2024
1 parent f010238 commit 649f781
Show file tree
Hide file tree
Showing 4 changed files with 63 additions and 69 deletions.
18 changes: 8 additions & 10 deletions Studio/CelesteStudio/Editing/Editor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
}
Expand Down
72 changes: 30 additions & 42 deletions Studio/CelesteStudio/Editing/PopupMenu.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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
Expand All @@ -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;
Expand All @@ -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) {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand All @@ -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();
}

Expand Down
23 changes: 14 additions & 9 deletions Studio/CelesteStudio/Editing/Theme.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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) =>
Expand All @@ -27,7 +22,7 @@ public void Dispose() {
}
}

public struct Theme() {
public struct Theme {
// Editor
public Color Background;
public Color Caret;
Expand Down Expand Up @@ -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();
Expand All @@ -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();
Expand All @@ -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;
Expand Down
19 changes: 11 additions & 8 deletions Studio/CelesteStudio/FontManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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;

Expand Down Expand Up @@ -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);
Expand Down

0 comments on commit 649f781

Please sign in to comment.