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

XFESystem

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

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

namespace XFESystem.Browser;

public sealed partial class BrowserService
{
    private readonly long paintIdentity = BrowserPaintIdentity.Next();
    private BrowserViewport viewport = new(760, 380);
    private BrowserDisplayList? displayList;
    private readonly BrowserLayoutEngine layoutEngine = new();
    private IBrowserTextMetrics textMetrics = new BrowserFallbackTextMetrics();
    private IDesktopWebCanvas? metricCanvas;
    private INode? hoveredNode, pointerDownNode;
    private bool layoutDirty = true;
    private int tracePumps, tracePaints;
    public long Revision { get; private set; }
    public long LayoutPassCount { get; private set; }
    public long ScrollLayoutReuseCount { get; private set; }
    public long RasterPaintCount { get; private set; }
    public long CachedPaintCount { get; private set; }
    public long PackagedScriptCacheHits => runtime?.PackagedScriptCacheHits ?? 0;
    public long PackagedScriptPreparations => runtime?.PackagedScriptPreparations ?? 0;
    public string LastInputTrace { get; private set; } = "";
    public void ResizeViewport(int width, int height)
    {
        var size = new BrowserViewport(Math.Max(1, width), Math.Max(1,height), viewport.ScrollX, viewport.ScrollY);
        if (size != viewport) { viewport = size; layoutDirty = true; dom?.DispatchEvent(null, "resize", new() { Bubbles = false, Cancelable = false }); }
    }
    public void SetCanvas(IDesktopCanvas canvas)
    {
        if (canvas is IDesktopWebCanvas web && !ReferenceEquals(web, metricCanvas))
        { metricCanvas = web; textMetrics = new CanvasMetrics(web); layoutDirty = true; }
    }
    public void CompleteEditorPreparationHandoff(IDesktopCanvas canvas, bool equivalentTextMetrics)
    {
        if (!equivalentTextMetrics || metricCanvas is null || canvas is not IDesktopWebCanvas web)
        { SetCanvas(canvas); return; }
        // The worker stopped accessing its page before publication. The native
        // platform has now verified matching assets/scale and transferred the
        // private caches. Switching equivalent metrics needs no CSS reflow.
        metricCanvas = web; textMetrics = new CanvasMetrics(web);
    }
    internal BrowserPaintSnapshot CaptureEditorPaintSnapshot() => BrowserPaintSnapshot.Capture(EnsureLayout(),
        node => dom is not null && dom.TryCanvas(node, out var surface) ? surface : null);
    public double DocumentHeight => document is null ? 0 : EnsureLayout().DocumentHeight;
    public bool TextCursor => document is not null && hoveredNode is not null && EnsureLayout().GetComputedStyle(hoveredNode)?.GetPropertyValue("cursor") is "text" or "vertical-text";
    public BrowserDisplayList Layout(BrowserViewport size, IBrowserTextMetrics? metrics = null) => WithGate(() =>
    {
        if (metrics is not null && !ReferenceEquals(metrics, textMetrics))
        { textMetrics = metrics; layoutDirty = true; }
        UpdateViewport(size);
        return EnsureLayout();
    });

    private void UpdateViewport(BrowserViewport size)
    {
        if (viewport == size) return;
        bool resize = viewport.Width != size.Width || viewport.Height != size.Height;
        if (!resize && !layoutDirty && displayList is not null && displayList.CanReuseForScroll(size))
        { displayList = displayList.WithScroll(size); ScrollLayoutReuseCount++; }
        else layoutDirty = true;
        viewport = size;
    }

    private void InvalidateDocument() { layoutDirty = true; Revision++; }
    private void InvalidateInteraction(BrowserInteractionState before, BrowserInteractionState after)
    {
        if (layoutDirty || displayList is null || layoutEngine.InteractionChangesStyles(before, after)) InvalidateDocument();
    }
    private BrowserDisplayList EnsureLayout()
    {
        if (document is null) throw new InvalidOperationException("No HTML document is loaded.");
        if (layoutDirty || displayList is null)
        {
            LayoutPassCount++;
            long layoutStarted = awaitingDocumentPresentation ? System.Diagnostics.Stopwatch.GetTimestamp() : 0;
            displayList = layoutEngine.Layout(document, viewport,
                linkedStyles.Select(item => new BrowserStyleSheet(item.Css, item.Owner)).ToArray(), textMetrics,
                dom is null ? null : dom.NodeIdentity, cspDeclared, new(hoveredNode, pointerDownNode, dom?.FocusedNode));
            if (awaitingDocumentPresentation)
            {
                documentLayoutTicks += System.Diagnostics.Stopwatch.GetTimestamp() - layoutStarted;
                documentStylesheetParseTicks += layoutEngine.LastStylesheetParseTicks;
            }
            layoutDirty = false;
        }
        return displayList;
    }

    public void Paint(IDesktopCanvas canvas, DesktopBounds bounds, double scrollY) => WithGate(() =>
    {
        if (document is null)
        { canvas.Text(CurrentPage.Text, 0xD85050, bounds.X + 8, bounds.Y + 8); return 0; }
        var size = new BrowserViewport(bounds.Width, bounds.Height, 0, Math.Max(0, scrollY));
        if (awaitingDocumentPresentation)
        {
            documentPaintBounds = bounds; documentPaintScale = canvas.ScalePercent;
            documentPaintCanvas = canvas as IDesktopPresentedPixelsCanvas;
            documentPaintSequenceBefore = documentPaintCanvas?.FramesPresented ?? -1;
        }
        if (size != viewport) { bool resize = size.Width != viewport.Width || size.Height != viewport.Height; UpdateViewport(size); if (resize) dom?.DispatchEvent(null, "resize", new() { Bubbles = false, Cancelable = false }); }
        SetCanvas(canvas);
        try
        {
            var frameCache = canvas as IDesktopWebFrameCache;
            if (frameCache is not null && !Intersects(bounds, frameCache.WebPaintClip)) return 0;
            if (!layoutDirty && displayList is not null && frameCache is not null &&
                frameCache.TryRestoreWebFrame(paintIdentity, Revision, LayoutPassCount, bounds, viewport.ScrollX, viewport.ScrollY))
            { CachedPaintCount++; if (awaitingDocumentPresentation) documentPaintAwaitingPresentation = true; return 0; }
            bool tracing = currentAddress?.AbsoluteUri == PackagedBrowserResources.MonacoPageAddress && tracePaints++ < 8;
            if (tracing) Trace?.Invoke("XFE_BROWSER_PAINT_BEGIN\n");
            var list = EnsureLayout();
            if (tracing) Trace?.Invoke("XFE_BROWSER_LAYOUT_READY|" + list.Commands.Count + "\n");
            long rasterStarted = awaitingDocumentPresentation ? System.Diagnostics.Stopwatch.GetTimestamp() : 0;
            BrowserRasterPainter.Paint(canvas, bounds, list, node => dom is not null && dom.TryCanvas(node, out var surface) ? surface : null);
            if (awaitingDocumentPresentation) documentRasterTicks += System.Diagnostics.Stopwatch.GetTimestamp() - rasterStarted;
            RasterPaintCount++;
            long cacheStarted = awaitingDocumentPresentation ? System.Diagnostics.Stopwatch.GetTimestamp() : 0;
            frameCache?.StoreWebFrame(paintIdentity, Revision, LayoutPassCount, bounds, viewport.ScrollX, viewport.ScrollY);
            if (awaitingDocumentPresentation) documentFrameCacheTicks += System.Diagnostics.Stopwatch.GetTimestamp() - cacheStarted;
            if (awaitingDocumentPresentation) documentPaintAwaitingPresentation = true;
            if (tracing) Trace?.Invoke("XFE_BROWSER_PAINT_END\n");
        }
        catch (Exception error) when (error is not OutOfMemoryException and not StackOverflowException and not DesktopRenderYieldException)
        {
            Trace?.Invoke("XFE_BROWSER_PAINT_FAILED|" + BoundedText.Safe(error.Message, 256) + "\n");
            canvas.Fill(0xFFFFFF, bounds.X, bounds.Y, bounds.Width, bounds.Height);
            canvas.Text("Rendering failed: " + BoundedText.Safe(error.Message, 70), 0xA03030, bounds.X + 8, bounds.Y + 8);
        }
        return 0;
    });
    private static bool Intersects(DesktopBounds first, DesktopBounds second) =>
        first.X < second.X + second.Width && second.X < first.X + first.Width &&
        first.Y < second.Y + second.Height && second.Y < first.Y + first.Height;

    private bool WithInputGate(Func<bool> action) => WithGate(() =>
    {
        try { return action(); }
        catch (Exception error) when (error is not OutOfMemoryException and not StackOverflowException)
        {
            try { Diagnostic("Browser event failed: " + error.Message); } catch (BrowserLimitException) { }
            try { RefreshAfterInput(); } catch (BrowserLimitException) { }
            LastInputTrace += "|Error|" + BoundedText.Safe(error.Message,256);
            return true;
        }
    });

    public bool Pointer(BrowserPointerInput input) => WithInputGate(() =>
    {
        if (document is null || dom is null) return false;
        long revision = Revision;
        var hit = EnsureLayout().HitTest(input.X, input.Y)?.Node;
        if (hit is IText) hit = hit.ParentElement;
        if (input.Type is "mousedown" or "mouseup") LastInputTrace = input.Type + "|" + input.X + "|" + input.Y + "|" + hit?.NodeName + "|" + (hit as IElement)?.Id;
        var init = new BrowserDomEventInit { ClientX = input.X, ClientY = input.Y, Button = input.Button, Buttons = input.Buttons,
            DeltaY = input.DeltaY, CtrlKey = input.Control, ShiftKey = input.Shift, AltKey = input.Alt };
        if (hit != hoveredNode)
        {
            if (hoveredNode is not null) { dom.DispatchEvent(hoveredNode, "mouseout", init); dom.DispatchEvent(hoveredNode, "mouseleave", init with { Bubbles = false }); }
            var beforeHover = new BrowserInteractionState(hoveredNode, pointerDownNode, dom.FocusedNode);
            hoveredNode = hit;
            InvalidateInteraction(beforeHover, new(hoveredNode, pointerDownNode, dom.FocusedNode));
            if (hit is not null) { dom.DispatchEvent(hit, "mouseover", init); dom.DispatchEvent(hit, "mouseenter", init with { Bubbles = false }); }
        }
        bool pointerAllowed = input.Type is "mousedown" or "mouseup" or "mousemove"
            ? dom.DispatchEvent(hit ?? document, input.Type.Replace("mouse", "pointer", StringComparison.Ordinal), init) : true;
        bool allowed = pointerAllowed && dom.DispatchEvent(hit ?? document, input.Type, init);
        if (input.Type == "pointercancel") { pointerDownNode = null; hoveredNode = null; InvalidateDocument(); }
        if (input.Type == "mousedown")
        {
            var beforeActive = new BrowserInteractionState(hoveredNode, pointerDownNode, dom.FocusedNode);
            pointerDownNode = hit;
            InvalidateInteraction(beforeActive, new(hoveredNode, pointerDownNode, dom.FocusedNode));
            if (allowed) dom.FocusNode(hit);
        }
        if (input.Type == "mouseup")
        {
            bool sameTarget = hit is not null && ReferenceEquals(hit, pointerDownNode);
            bool clickAllowed = sameTarget && dom.DispatchEvent(hit, "click", init);
            LastInputTrace += "|Same|" + sameTarget + "|ClickAllowed|" + clickAllowed;
            if (clickAllowed)
            {
                var anchor = (hit as IElement)?.Closest("a[href]");
                if (anchor is not null) Navigate(ResourceAddress(anchor.GetAttribute("href")!).AbsoluteUri);
            }
            var beforeRelease = new BrowserInteractionState(hoveredNode, pointerDownNode, dom.FocusedNode);
            pointerDownNode = null;
            InvalidateInteraction(beforeRelease, new(hoveredNode, pointerDownNode, dom.FocusedNode));
        }
        CaptureEventOutput(); DrainResources(); if (Revision != revision) RefreshAfterInput(); return !allowed;
    });

    public bool Key(BrowserKeyboardInput input) => WithInputGate(() =>
    {
        if (document is null || dom is null) return false;
        var target = dom.FocusedNode ?? document.Body ?? (INode)document;
        var init = new BrowserDomEventInit { Key = input.Key, Code = input.Code, CtrlKey = input.Control, ShiftKey = input.Shift, AltKey = input.Alt, Data = input.Text };
        bool allowed = dom.DispatchEvent(target, input.Type, init);
        if (allowed && input.Type == "keydown")
        {
            if (!input.Control && !input.Alt && input.Text.Length != 0)
            {
                if (dom.DispatchEvent(target, "keypress", init) && dom.DispatchEvent(target, "beforeinput", init with { InputType = "insertText" })) dom.ApplyTextInput(input.Text);
            }
            else if (input.Key == "Backspace" || input.Key == "Delete")
            {
                string type = input.Key == "Backspace" ? "deleteContentBackward" : "deleteContentForward";
                if (dom.DispatchEvent(target, "beforeinput", init with { InputType = type })) dom.ApplyTextInput("", input.Key == "Backspace" ? -1 : 1);
            }
            else dom.ApplyControlKey(input.Key, input.Shift, input.Control);
        }
        CaptureEventOutput(); DrainResources(); RefreshAfterInput(); return true;
    });

    private long nextRetainedStatePrune;
    public bool AdvanceTime(long monotonicMilliseconds) => WithInputGate(() =>
    {
        if(monotonicMilliseconds>=nextRetainedStatePrune)
        {dom?.PruneRetainedState();nextRetainedStatePrune=monotonicMilliseconds+5_000;}
        if (documentEnhancementsPending)
        {
            long previousRevision = Revision;
            if (AdvanceDocumentLoading())
            {
                if (Revision != previousRevision) RefreshAfterInput();
                return Revision != previousRevision;
            }
            return false;
        }
        if (runtime is null) return false;
        long before = Revision;
        bool tracing = currentAddress?.AbsoluteUri == PackagedBrowserResources.MonacoPageAddress && tracePumps++ < 20;
        if (tracing) Trace?.Invoke("XFE_BROWSER_PUMP_BEGIN|"+monotonicMilliseconds+"|Tasks|"+runtime.PendingTaskCount+"\n");
        var result = runtime.Pump(monotonicMilliseconds, activeLimits.MaximumEventCallbacksPerPump);
        if (tracing) Trace?.Invoke("XFE_BROWSER_PUMP_END|"+result.Success+"|"+result.Value+"\n");
        if (!result.Success) { Diagnostic("Event loop failed: " + result.Error); InvalidateDocument(); Trace?.Invoke("XFE_BROWSER_EVENT_LOOP_ERROR|"+BoundedText.Safe(result.Error,1024)+"\n"); }
        if (result.ConsoleOutput.Length != 0) { Diagnostic(result.ConsoleOutput); InvalidateDocument(); }
        DrainResources();
        if (before != Revision) RefreshAfterInput();
        return before != Revision;
    });

    private void RefreshAfterInput()
    {
        if (document is null || currentAddress is null) return;
        try { CurrentPage = RenderCurrent(pageDiagnostics?.ToString().TrimEnd('\n') ?? ""); }
        catch (BrowserLimitException error) { Diagnostic(error.Message); }
    }
    private void CaptureEventOutput()
    {
        string output = runtime?.DrainConsoleOutput() ?? "";
        if (output.Length != 0) { Diagnostic(output); InvalidateDocument(); }
    }
    private sealed class CanvasMetrics(IDesktopWebCanvas canvas) : IBrowserTextMetrics
    { public double Measure(string text, BrowserFont font) => canvas.MeasureWebText(text, font.Size, font.Family, font.Weight); }
}

public static partial class BrowserRasterPainter
{
    public static void Paint(IDesktopCanvas canvas, DesktopBounds viewport, BrowserDisplayList list, Func<INode, BrowserCanvasSurface?>? surfaces = null)
    {
        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
        {
            foreach (var item in list.Commands)
            {
                if (item.Opacity <= 0 && item.Kind is not (BrowserDrawKind.PushClip or BrowserDrawKind.PopClip)) continue;
                var r = item.Bounds.Translate(viewport.X - list.Viewport.ScrollX, viewport.Y - list.Viewport.ScrollY);
                if (item.Kind == BrowserDrawKind.PushClip) { clips.Push(clip); clip = clip.Intersect(r); rounded.Push(item.RoundedClip,viewport.X-list.Viewport.ScrollX,viewport.Y-list.Viewport.ScrollY); 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 (item.Node is not null && surfaces?.Invoke(item.Node) 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();
    }
    private static void PaintCanvas(IDesktopCanvas canvas, BrowserRect bounds, BrowserRect visible, BrowserCanvasSurface surface, double opacity)
    {
        if (surface.Width <= 0 || surface.Height <= 0 || bounds.Width <= 0 || bounds.Height <= 0) return;
        var pixels = surface.Pixels.Span;
        int left = (int)Math.Floor(visible.Left), right = (int)Math.Ceiling(visible.Right), top = (int)Math.Floor(visible.Top), bottom = (int)Math.Ceiling(visible.Bottom);
        for (int row = top; row < bottom; row++)
        {
            int sourceY = Math.Clamp((int)((row - bounds.Y) * surface.Height / bounds.Height), 0, surface.Height - 1);
            for (int x = left; x < right;)
            {
                int sourceX = Math.Clamp((int)((x - bounds.X) * surface.Width / bounds.Width), 0, surface.Width - 1);
                uint color = pixels[sourceY * surface.Width + sourceX]; int end = x + 1;
                while (end < right && pixels[sourceY * surface.Width + Math.Clamp((int)((end - bounds.X) * surface.Width / bounds.Width), 0, surface.Width - 1)] == color) end++;
                int alpha = (int)((color >> 24) * opacity);
                if (alpha >= 255) canvas.Fill(color & 0xFFFFFF, x, row, end - x, 1);
                else if (alpha > 0 && canvas is IDesktopCoverageCanvas coverage)
                {
                    int nx = (int)Math.Round(x * canvas.ScalePercent / 100d), ny = (int)Math.Round(row * canvas.ScalePercent / 100d);
                    int nw = Math.Max(1,(int)Math.Round(end * canvas.ScalePercent / 100d)-nx), nh = Math.Max(1,(int)Math.Round((row+1) * canvas.ScalePercent / 100d)-ny);
                    var mask = new byte[nw]; Array.Fill(mask,(byte)alpha);
                    for (int sy = 0; sy < nh; sy++) coverage.BlendCoverage(color & 0xFFFFFF, nx, ny + sy, nw,1,mask);
                }
                x = end;
            }
        }
    }
    private static DesktopBounds ToBounds(BrowserRect r) => new((int)Math.Floor(r.X), (int)Math.Floor(r.Y), Math.Max(0, (int)Math.Ceiling(r.Right) - (int)Math.Floor(r.X)), Math.Max(0, (int)Math.Ceiling(r.Bottom) - (int)Math.Floor(r.Y)));
}