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 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? surfaces = null) { var web = canvas as IDesktopWebCanvas; var rounded = new BrowserClipRasterCanvas(canvas); var clips = new Stack(); 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))); }