XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFESystem

【Rust】XFE系统,基于Rust和汇编

公开
关注 0 Fork 0 Star 0
UTF-8
using AngleSharp.Dom;
using XFESystem.Desktop;

namespace XFESystem.Browser;

/// <summary>
/// Immutable paint-only publication. No DOM node, layout box, text-metrics
/// provider or live canvas backing store crosses the worker/UI boundary.
/// </summary>
public sealed class BrowserPaintSnapshot
{
    private readonly IReadOnlyList<BrowserDrawCommand> commands;
    private readonly BrowserCanvasSurface?[] surfaces;
    private BrowserPaintSnapshot(BrowserViewport viewport, BrowserDrawCommand[] commands,
        BrowserCanvasSurface?[] surfaces, double documentWidth, double documentHeight, long canvasBytes)
    {
        Viewport = viewport;
        this.commands = Array.AsReadOnly(commands);
        this.surfaces = surfaces;
        DocumentWidth = documentWidth; DocumentHeight = documentHeight; CanvasBytes = canvasBytes;
    }
    public BrowserViewport Viewport { get; }
    public IReadOnlyList<BrowserDrawCommand> Commands => commands;
    public double DocumentWidth { get; }
    public double DocumentHeight { get; }
    public long CanvasBytes { get; }

    /// <summary>Called only by the current exclusive page owner, before release publication.</summary>
    public static BrowserPaintSnapshot Capture(BrowserDisplayList list,
        Func<INode, BrowserCanvasSurface?>? sourceSurfaces = null,
        int maximumCommands = 100_000, long maximumCanvasPixels = 16_777_216)
    {
        ArgumentNullException.ThrowIfNull(list);
        if (maximumCommands < 1 || maximumCommands > 100_000 || maximumCanvasPixels < 0 || maximumCanvasPixels > 16_777_216)
            throw new ArgumentOutOfRangeException(nameof(maximumCommands), "Invalid paint snapshot limits.");
        if (list.Commands.Count > maximumCommands) throw new BrowserLimitException("The paint snapshot command limit was exceeded.");
        var commands = new BrowserDrawCommand[list.Commands.Count];
        var surfaces = new BrowserCanvasSurface?[commands.Length];
        var copied = new Dictionary<BrowserCanvasSurface, BrowserCanvasSurface>();
        long pixels = 0;
        for (int index = 0; index < commands.Length; index++)
        {
            var source = list.Commands[index];
            // Strings and value geometry are immutable. Every nested mutable
            // list is independently copied; the node is deliberately dropped.
            commands[index] = source with
            {
                Node = null,
                Decoration = CopyDecoration(source.Decoration),
                RoundedClip = source.RoundedClip is { } rounded ? rounded with { } : null
            };
            if (source.Kind != BrowserDrawKind.Canvas || source.Node is null || sourceSurfaces?.Invoke(source.Node) is not { } surface) continue;
            if (!copied.TryGetValue(surface, out var frozen))
            {
                long size = checked((long)surface.Width * surface.Height);
                if (surface.Width < 0 || surface.Height < 0 || size > maximumCanvasPixels - pixels)
                    throw new BrowserLimitException("The paint snapshot canvas pixel limit was exceeded.");
                frozen = surface.CopyForPaintSnapshot();
                copied.Add(surface, frozen); pixels += size;
            }
            surfaces[index] = frozen;
        }
        return new(list.Viewport, commands, surfaces, list.DocumentWidth, list.DocumentHeight, checked(pixels * sizeof(uint)));
    }

    private static BrowserBoxDecoration? CopyDecoration(BrowserBoxDecoration? source) => source is null ? null : source with
    {
        Shadows = Array.AsReadOnly(source.Shadows.Select(shadow => shadow with { }).ToArray()),
        Gradients = Array.AsReadOnly(source.Gradients.Select(gradient => gradient with
        {
            Stops = Array.AsReadOnly(gradient.Stops.Select(stop => stop with { }).ToArray())
        }).ToArray())
    };

    internal BrowserCanvasSurface? SurfaceAt(int commandIndex) => surfaces[commandIndex];
    public void Paint(IDesktopCanvas canvas, DesktopBounds viewport) => BrowserRasterPainter.Paint(canvas, viewport, this);
}

public static partial class BrowserRasterPainter
{
    /// <summary>Only the frozen snapshot and UI-owned renderer are accessed here.</summary>
    public static void Paint(IDesktopCanvas canvas, DesktopBounds viewport, BrowserPaintSnapshot snapshot)
    {
        ArgumentNullException.ThrowIfNull(snapshot);
        var web = canvas as IDesktopWebCanvas;
        var rounded = new BrowserClipRasterCanvas(canvas);
        var clips = new Stack<BrowserRect>();
        var clip = new BrowserRect(viewport.X, viewport.Y, viewport.Width, viewport.Height);
        if (canvas is IDesktopWebFrameCache frameCache)
        {
            var scene = frameCache.WebPaintClip;
            clip = clip.Intersect(new(scene.X, scene.Y, scene.Width, scene.Height));
            if (clip.Width <= 0 || clip.Height <= 0) return;
        }
        web?.PushClip(viewport);
        canvas.Fill(0xFFFFFF, viewport.X, viewport.Y, viewport.Width, viewport.Height);
        try
        {
            for (int index = 0; index < snapshot.Commands.Count; index++)
            {
                var item = snapshot.Commands[index];
                if (item.Opacity <= 0 && item.Kind is not (BrowserDrawKind.PushClip or BrowserDrawKind.PopClip)) continue;
                double dx = viewport.X - snapshot.Viewport.ScrollX, dy = viewport.Y - snapshot.Viewport.ScrollY;
                var r = item.Bounds.Translate(dx, dy);
                if (item.Kind == BrowserDrawKind.PushClip)
                { clips.Push(clip); clip = clip.Intersect(r); rounded.Push(item.RoundedClip, dx, dy); web?.PushClip(ToBounds(clip)); continue; }
                if (item.Kind == BrowserDrawKind.PopClip)
                { if (clips.TryPop(out var old)) { clip = old; rounded.Pop(); web?.PopClip(); } continue; }
                var visible = r.Intersect(clip);
                if (visible.Width <= 0 || visible.Height <= 0) continue;
                IDesktopCanvas target = rounded.Active && canvas is IDesktopCoverageCanvas ? rounded : canvas;
                if (item.Decoration is not null) BrowserBoxRasterPainter.Paint(target, item, r, visible);
                else if (item.Kind == BrowserDrawKind.Canvas)
                { if (snapshot.SurfaceAt(index) is { } surface) PaintCanvas(target, r, visible, surface, item.Opacity); }
                else if (item.Kind == BrowserDrawKind.Text)
                {
                    double leading = web is null ? 0 : (r.Height - web.MeasureWebLineHeight(item.Font.Size, item.Font.Family, item.Font.Weight)) / 2;
                    if (web is not null && rounded.Active && canvas is IDesktopCoverageCanvas) rounded.WebText(web, item, r.Translate(0, leading));
                    else if (web is not null) web.WebText(item.Text, item.Color, r.X, r.Y + leading, item.Font.Size, item.Font.Family, item.Font.Weight, item.Opacity);
                    else if (visible == r) canvas.Text(item.Text, item.Color, (int)r.X, (int)r.Y);
                }
                else
                {
                    var b = ToBounds(visible);
                    if (item.Opacity < 1 && target is IDesktopCoverageCanvas coverage)
                    {
                        int left = (int)Math.Floor(visible.X * canvas.ScalePercent / 100d), top = (int)Math.Floor(visible.Y * canvas.ScalePercent / 100d);
                        int width = Math.Max(1, (int)Math.Ceiling(visible.Width * canvas.ScalePercent / 100d)), height = Math.Max(1, (int)Math.Ceiling(visible.Height * canvas.ScalePercent / 100d));
                        var row = new byte[width]; Array.Fill(row, (byte)Math.Clamp(item.Opacity * 255, 0, 255));
                        for (int y = 0; y < height; y++) coverage.BlendCoverage(item.Color, left, top + y, width, 1, row);
                    }
                    else target.Fill(item.Color, b.X, b.Y, b.Width, b.Height);
                }
            }
        }
        catch { while (clips.TryPop(out _)) web?.PopClip(); web?.PopClip(); throw; }
        while (clips.TryPop(out _)) web?.PopClip(); web?.PopClip();
    }
}