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 readonly IDesktopPreparedWebText?[] text;
    private BrowserPaintSnapshot(BrowserViewport viewport, BrowserDrawCommand[] commands,
        BrowserCanvasSurface?[] surfaces, IDesktopPreparedWebText?[] text, double documentWidth, double documentHeight, long canvasBytes, bool hasPreparedText)
    {
        Viewport = viewport;
        this.commands = Array.AsReadOnly(commands);
        this.surfaces = surfaces;
        this.text = text;
        DocumentWidth = documentWidth; DocumentHeight = documentHeight; CanvasBytes = canvasBytes;
        HasPreparedText = hasPreparedText;
    }
    public BrowserViewport Viewport { get; }
    public IReadOnlyList<BrowserDrawCommand> Commands => commands;
    public double DocumentWidth { get; }
    public double DocumentHeight { get; }
    public long CanvasBytes { get; }
    public int PreparedTextCount => text.Count(item => item is not null);
    public bool HasPreparedText { 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,
        IDesktopWebTextPreparationCanvas? textPreparation = null, Func<string, BrowserCanvasSurface?>? sourceImages = null)
    {
        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 text = new IDesktopPreparedWebText?[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
            };
            // Only visible runs need native glyph coverage. Long documents do
            // not create a full-document raster or a contiguous page bitmap.
            if (textPreparation is not null && source.Kind == BrowserDrawKind.Text && source.Opacity > 0 &&
                source.Text.Length != 0 && (source.Transform ?? BrowserAffineTransform.Identity).Bounds(source.Bounds).Intersect(new(list.Viewport.ScrollX, list.Viewport.ScrollY,
                    list.Viewport.Width, list.Viewport.Height)) is { Width: > 0, Height: > 0 })
                text[index] = textPreparation.PrepareWebText(source.Text, source.Font.Size, source.Font.Family, source.Font.Weight, source.Opacity);
            BrowserCanvasSurface? surface = source.Kind == BrowserDrawKind.Image ? sourceImages?.Invoke(source.ImageSource) :
                source.Kind == BrowserDrawKind.Canvas && source.Node is not null ? sourceSurfaces?.Invoke(source.Node) : null;
            if (surface is null) 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, text, list.DocumentWidth, list.DocumentHeight, checked(pixels * sizeof(uint)), textPreparation is not null);
    }

    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];
    internal IDesktopPreparedWebText? TextAt(int commandIndex) => text[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);
        PaintCore(canvas, viewport, snapshot.Viewport, snapshot.Commands, snapshot.SurfaceAt, snapshot.TextAt, snapshot.HasPreparedText);
    }
    private static void PaintCore(IDesktopCanvas canvas, DesktopBounds viewport, BrowserViewport pageViewport,
        IReadOnlyList<BrowserDrawCommand> commands, Func<int, BrowserCanvasSurface?> surfaceAt,
        Func<int, IDesktopPreparedWebText?>? textAt, bool hasPreparedText)
    {
        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);
        var preparedViewport = hasPreparedText ? viewport with
        { Width = Math.Min(viewport.Width, (int)Math.Floor(pageViewport.Width)), Height = Math.Min(viewport.Height, (int)Math.Floor(pageViewport.Height)) } : viewport;
        clip = clip.Intersect(new(preparedViewport.X, preparedViewport.Y, preparedViewport.Width, preparedViewport.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;
        }
        double dx = viewport.X - pageViewport.ScrollX, dy = viewport.Y - pageViewport.ScrollY;
        web?.PushClip(preparedViewport);
        canvas.Fill(0xFFFFFF, viewport.X, viewport.Y, viewport.Width, viewport.Height);
        try
        {
            for (int index = 0; index < commands.Count; index++)
            {
                var item = commands[index];
                if (item.Opacity <= 0 && item.Kind is not (BrowserDrawKind.PushClip or BrowserDrawKind.PopClip)) continue;
                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 matrix = item.Transform ?? BrowserAffineTransform.Identity;
                var paintedBounds = matrix.Bounds(item.Bounds).Translate(dx, dy);
                var visible = paintedBounds.Intersect(clip);
                if (visible.Width <= 0 || visible.Height <= 0) continue;
                IDesktopCanvas target = rounded.Active && canvas is IDesktopCoverageCanvas ? rounded : canvas;
                BrowserAffineRasterCanvas? transformed = null;
                if (!matrix.IsIdentity)
                {
                    var screenMatrix = BrowserAffineTransform.Translation(dx, dy) * matrix * BrowserAffineTransform.Translation(-dx, -dy);
                    transformed = new(target, r, clip, screenMatrix);
                    target = transformed; visible = transformed.SourceClip;
                }
                if (item.Decoration is not null) BrowserBoxRasterPainter.Paint(target, item, r, visible);
                else if (item.Kind is BrowserDrawKind.Canvas or BrowserDrawKind.Image)
                { if (surfaceAt(index) is { } surface) PaintCanvas(target, r, visible, surface, item.Opacity); }
                else if (item.Kind == BrowserDrawKind.Text)
                {
                    if (textAt?.Invoke(index) is { } prepared && prepared.ScalePercent == canvas.ScalePercent && target is IDesktopCoverageCanvas coverageText)
                        prepared.Paint(coverageText, item.Color, r.X, r.Y + (r.Height - prepared.LineHeight) / 2);
                    else
                    {
                        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 && target is IDesktopCoverageCanvas && (transformed is not null || rounded.Active))
                            PaintTextMask((IDesktopCoverageCanvas)target, 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 (transformed is null && 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);
                }
                transformed?.Flush();
            }
        }
        catch { while (clips.TryPop(out _)) web?.PopClip(); web?.PopClip(); throw; }
        while (clips.TryPop(out _)) web?.PopClip(); web?.PopClip();
    }
    private static void PaintTextMask(IDesktopCoverageCanvas target, IDesktopWebCanvas web, BrowserDrawCommand command, BrowserRect bounds)
    {
        double scale = target.ScalePercent / 100d;
        var mask = web.RasterizeWebText(command.Text, command.Font.Size * scale, command.Font.Family, command.Font.Weight);
        if (command.Opacity < 1)
            for (int index = 0; index < mask.Alpha.Length; index++) mask.Alpha[index] = (byte)Math.Clamp(Math.Round(mask.Alpha[index] * command.Opacity), 0, 255);
        target.BlendCoverage(command.Color, (int)Math.Round(bounds.X * scale), (int)Math.Round(bounds.Y * scale), mask.Width, mask.Height, mask.Alpha);
    }
}