maui-linux/Rendering/SkiaRenderingEngine.cs

159 lines
4.1 KiB
C#

// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using SkiaSharp;
using Microsoft.Maui.Platform.Linux.Window;
using System.Runtime.InteropServices;
namespace Microsoft.Maui.Platform.Linux.Rendering;
/// <summary>
/// Manages Skia rendering to an X11 window.
/// </summary>
public class SkiaRenderingEngine : IDisposable
{
private readonly X11Window _window;
private SKBitmap? _bitmap;
private SKCanvas? _canvas;
private SKImageInfo _imageInfo;
private bool _disposed;
private bool _fullRedrawNeeded = true;
public static SkiaRenderingEngine? Current { get; private set; }
public ResourceCache ResourceCache { get; }
public int Width => _imageInfo.Width;
public int Height => _imageInfo.Height;
public SkiaRenderingEngine(X11Window window)
{
_window = window;
ResourceCache = new ResourceCache();
Current = this;
CreateSurface(window.Width, window.Height);
_window.Resized += OnWindowResized;
_window.Exposed += OnWindowExposed;
}
private void CreateSurface(int width, int height)
{
_bitmap?.Dispose();
_canvas?.Dispose();
_imageInfo = new SKImageInfo(
Math.Max(1, width),
Math.Max(1, height),
SKColorType.Bgra8888,
SKAlphaType.Premul);
_bitmap = new SKBitmap(_imageInfo);
_canvas = new SKCanvas(_bitmap);
_fullRedrawNeeded = true;
}
private void OnWindowResized(object? sender, (int Width, int Height) size)
{
CreateSurface(size.Width, size.Height);
}
private void OnWindowExposed(object? sender, EventArgs e)
{
_fullRedrawNeeded = true;
}
public void InvalidateAll()
{
_fullRedrawNeeded = true;
}
public void Render(SkiaView rootView)
{
if (_canvas == null || _bitmap == null)
return;
_canvas.Clear(SKColors.White);
// Measure first, then arrange
var availableSize = new SKSize(Width, Height);
rootView.Measure(availableSize);
rootView.Arrange(new SKRect(0, 0, Width, Height));
// Draw the view tree
rootView.Draw(_canvas);
// Draw popup overlays (dropdowns, calendars, etc.) on top
SkiaView.DrawPopupOverlays(_canvas);
_canvas.Flush();
// Present to X11 window
PresentToWindow();
}
private void PresentToWindow()
{
if (_bitmap == null) return;
var pixels = _bitmap.GetPixels();
if (pixels == IntPtr.Zero) return;
_window.DrawPixels(pixels, _imageInfo.Width, _imageInfo.Height, _imageInfo.RowBytes);
}
public SKCanvas? GetCanvas() => _canvas;
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
_window.Resized -= OnWindowResized;
_window.Exposed -= OnWindowExposed;
_canvas?.Dispose();
_bitmap?.Dispose();
ResourceCache.Dispose();
if (Current == this) Current = null;
}
_disposed = true;
}
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
}
public class ResourceCache : IDisposable
{
private readonly Dictionary<string, SKTypeface> _typefaces = new();
private bool _disposed;
public SKTypeface GetTypeface(string fontFamily, SKFontStyle style)
{
var key = $"{fontFamily}_{style.Weight}_{style.Width}_{style.Slant}";
if (!_typefaces.TryGetValue(key, out var typeface))
{
typeface = SKTypeface.FromFamilyName(fontFamily, style) ?? SKTypeface.Default;
_typefaces[key] = typeface;
}
return typeface;
}
public void Clear()
{
foreach (var tf in _typefaces.Values) tf.Dispose();
_typefaces.Clear();
}
public void Dispose()
{
if (!_disposed) { Clear(); _disposed = true; }
}
}