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)));
}