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

XFESystem

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

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

namespace XFESystem.Browser;

public sealed record BrowserBoxShadow(double X, double Y, double Blur, double Spread, CssColor Color, bool Inset = false);
public sealed record BrowserBoxDecoration(BrowserEdges Radius, BrowserEdges Border, CssColor Background,
    CssColor Top, CssColor Right, CssColor Bottom, CssColor Left, IReadOnlyList<BrowserBoxShadow> Shadows, BrowserRect Bounds,
    IReadOnlyList<BrowserCssGradient> Gradients)
{
    public BrowserRect PaintBounds
    {
        get
        {
            BrowserRect area = Bounds;
            foreach (var shadow in Shadows.Where(shadow => !shadow.Inset))
            {
                double extent = shadow.Blur * 1.5 + shadow.Spread + 1;
                area = area.Union(new(Bounds.X + shadow.X - extent, Bounds.Y + shadow.Y - extent, Math.Max(0, Bounds.Width + extent * 2), Math.Max(0, Bounds.Height + extent * 2)));
            }
            return area;
        }
    }
    internal static BrowserBoxDecoration? Create(CssComputedStyle style, BrowserRect bounds, BrowserEdges border)
    {
        if (!style.Properties.ContainsKey("border-radius") && !style.Properties.ContainsKey("border-top-left-radius") && !style.Properties.ContainsKey("border-top-right-radius") &&
            !style.Properties.ContainsKey("border-bottom-left-radius") && !style.Properties.ContainsKey("border-bottom-right-radius") && style.Get("box-shadow","none") == "none" && style.Get("background-image","none") == "none") return null;
        string[] corners = ["top-left", "top-right", "bottom-right", "bottom-left"];
        string[] tokens = CssValue.Split(style.Get("border-radius", "0"), ' ').ToArray();
        if (tokens.Length is < 1 or > 4) tokens = ["0"];
        string[] values = [tokens[0], tokens.Length > 1 ? tokens[1] : tokens[0], tokens.Length > 2 ? tokens[2] : tokens[0], tokens.Length > 3 ? tokens[3] : tokens.Length > 1 ? tokens[1] : tokens[0]];
        var radii = new double[4];
        for (int i = 0; i < 4; i++) radii[i] = Math.Max(0, CssValue.Length(style.Get("border-" + corners[i] + "-radius", values[i]), Math.Min(bounds.Width, bounds.Height), style.Font.Size, style.Viewport, 16) ?? 0);
        double factor = 1;
        void Limit(double available, double total) { if (total > 0) factor = Math.Min(factor, available / total); }
        Limit(bounds.Width, radii[0] + radii[1]); Limit(bounds.Width, radii[3] + radii[2]);
        Limit(bounds.Height, radii[0] + radii[3]); Limit(bounds.Height, radii[1] + radii[2]);
        var shadows = new List<BrowserBoxShadow>();
        foreach (string part in CssValue.Split(style.Get("box-shadow", "none"), ','))
        {
            var words = CssValue.Split(part.Trim(), ' ').ToArray(); var lengths = new List<double>(); CssColor color = style.Color; bool inset = false;
            foreach (string word in words)
                if (word == "inset") inset = true;
                else if (CssValue.Length(word, 0, style.Font.Size, style.Viewport, 16) is { } length) lengths.Add(length);
                else if (word != "none") color = CssValue.Color(word, style.Color);
            if (lengths.Count is >= 2 and <= 4 && shadows.Count < 8)
                shadows.Add(new(lengths[0], lengths[1], Math.Clamp(lengths.Count > 2 ? lengths[2] : 0, 0, 128), Math.Clamp(lengths.Count > 3 ? lengths[3] : 0, -128, 128), color, inset));
        }
        var gradients = BrowserCssGradient.Parse(style.Get("background-image", "none"), style,bounds);
        if (radii.All(radius => radius == 0) && shadows.Count == 0 && gradients.Count == 0) return null;
        CssColor Color(string side) => CssValue.Color(style.Get("border-" + side + "-color", "currentcolor"), style.Color);
        return new(new(radii[0] * factor, radii[1] * factor, radii[2] * factor, radii[3] * factor), border, style.Background,
            Color("top"), Color("right"), Color("bottom"), Color("left"), shadows.ToArray(), bounds, gradients);
    }
}

/// <summary>Original device-pixel box rasterizer shared by the native and screenshot hosts.</summary>
public static class BrowserBoxRasterPainter
{
    public static void Paint(IDesktopCanvas canvas, BrowserDrawCommand command, BrowserRect bounds, BrowserRect visible)
    {
        if (command.Decoration is not { } box) return;
        if (canvas is not IDesktopCoverageCanvas coverage)
        { var area = box.Bounds.Translate(bounds.X-command.Bounds.X,bounds.Y-command.Bounds.Y).Intersect(visible); if(box.Background.Alpha>0)canvas.Fill(box.Background.Rgb,(int)area.X,(int)area.Y,(int)area.Width,(int)area.Height); return; }
        double scale = canvas.ScalePercent / 100d;
        BrowserRect actual = box.Bounds.Translate(bounds.X - command.Bounds.X, bounds.Y - command.Bounds.Y);
        var native = new BrowserRect(actual.X * scale, actual.Y * scale, actual.Width * scale, actual.Height * scale);
        var clip = new BrowserRect(visible.X * scale, visible.Y * scale, visible.Width * scale, visible.Height * scale);
        var radius = Scale(box.Radius, scale); var border = Scale(box.Border, scale);
        // Shadow samples under a later fully opaque background cannot affect
        // the completed pixels. Stay inside a conservative straight-sided core;
        // fractional edges and every ancestor rounded clip keep the old path.
        BrowserRect? shadowOcclusion = null;
        if (box.Background.Alpha == 1 && command.Opacity == 1 && canvas is not BrowserClipRasterCanvas)
        {
            double inset = Math.Max(.5, Math.Max(Math.Max(radius.Top, radius.Right), Math.Max(radius.Bottom, radius.Left)));
            if (native.Width > inset * 2 && native.Height > 1)
                shadowOcclusion = new(native.Left + inset, native.Top + .5, native.Width - inset * 2, native.Height - 1);
        }
        foreach (var shadow in box.Shadows.Reverse())
        {
            if (shadow.Inset) continue;
            var rect = new BrowserRect(native.X + (shadow.X - shadow.Spread) * scale, native.Y + (shadow.Y - shadow.Spread) * scale,
                Math.Max(0, native.Width + shadow.Spread * scale * 2), Math.Max(0, native.Height + shadow.Spread * scale * 2));
            double blur = shadow.Blur * scale, extent = blur * 1.5;
            var area = new BrowserRect(rect.X - extent - 1, rect.Y - extent - 1, rect.Width + extent * 2 + 2, rect.Height + extent * 2 + 2).Intersect(clip);
            DrawRows(coverage, area, shadow.Color.Rgb, (x, y) =>
            {
                double distance = Distance(rect, radius, x, y);
                return shadow.Color.Alpha * command.Opacity * (blur <= 0 ? Math.Clamp(.5 - distance, 0, 1) : .5 * (1 - Erf(distance / Math.Max(.5, blur / 2) / Math.Sqrt(2))));
            }, shadowOcclusion);
        }
        // Solid background and border are composited independently so transparent
        // borders retain the CSS background beneath them.
        if (box.Background.Alpha > 0) DrawSolidRows(coverage, native.Intersect(clip), box.Background.Rgb,
            native, radius, box.Background.Alpha, command.Opacity);
        foreach (var gradient in box.Gradients.Reverse()) DrawGradientRows(coverage, native.Intersect(clip), native, radius, gradient, command.Opacity);
        var inner = new BrowserRect(native.X + border.Left, native.Y + border.Top, Math.Max(0, native.Width - border.Horizontal), Math.Max(0, native.Height - border.Vertical));
        var innerRadius = new BrowserEdges(Math.Max(0, radius.Top - Math.Max(border.Top, border.Left)), Math.Max(0, radius.Right - Math.Max(border.Top, border.Right)),
            Math.Max(0, radius.Bottom - Math.Max(border.Bottom, border.Right)), Math.Max(0, radius.Left - Math.Max(border.Bottom, border.Left)));
        if (border.Top + border.Right + border.Bottom + border.Left > 0)
            foreach (int side in new[] { 0, 1, 2, 3 })
            {
                CssColor color = side switch { 0 => box.Top, 1 => box.Right, 2 => box.Bottom, _ => box.Left };
                BrowserRect sideBounds = side switch
                {
                    0 => new(native.X, native.Y, native.Width, Math.Max(border.Top, Math.Max(radius.Top, radius.Right))),
                    1 => new(native.Right - Math.Max(border.Right, Math.Max(radius.Right, radius.Bottom)), native.Y, Math.Max(border.Right, Math.Max(radius.Right, radius.Bottom)), native.Height),
                    2 => new(native.X, native.Bottom - Math.Max(border.Bottom, Math.Max(radius.Left, radius.Bottom)), native.Width, Math.Max(border.Bottom, Math.Max(radius.Left, radius.Bottom))),
                    _ => new(native.X, native.Y, Math.Max(border.Left, Math.Max(radius.Top, radius.Left)), native.Height)
                };
                DrawRows(coverage, sideBounds.Intersect(clip), color.Rgb, (x, y) =>
                {
                    // Corner ownership follows the closest normalized border side.
                    double t = border.Top > 0 ? (y - native.Top) / border.Top : double.MaxValue;
                    double r = border.Right > 0 ? (native.Right - x) / border.Right : double.MaxValue;
                    double b = border.Bottom > 0 ? (native.Bottom - y) / border.Bottom : double.MaxValue;
                    double l = border.Left > 0 ? (x - native.Left) / border.Left : double.MaxValue;
                    double minimum = Math.Min(Math.Min(t, r), Math.Min(b, l));
                    int owner = t == minimum ? 0 : r == minimum ? 1 : b == minimum ? 2 : 3;
                    return owner != side ? 0 : Math.Max(0, Coverage(native, radius, x, y) - Coverage(inner, innerRadius, x, y)) * color.Alpha * command.Opacity;
                });
            }
    }
    private static BrowserEdges Scale(BrowserEdges value, double scale) => new(value.Top * scale, value.Right * scale, value.Bottom * scale, value.Left * scale);
    public static double Coverage(BrowserRect bounds, BrowserEdges radii, double x, double y)
    {
        if (bounds.Width <= 0 || bounds.Height <= 0) return 0;
        double radius = x < bounds.X + bounds.Width / 2 ? y < bounds.Y + bounds.Height / 2 ? radii.Top : radii.Left : y < bounds.Y + bounds.Height / 2 ? radii.Right : radii.Bottom;
        double qx = Math.Abs(x - bounds.X - bounds.Width / 2) - bounds.Width / 2 + radius;
        double qy = Math.Abs(y - bounds.Y - bounds.Height / 2) - bounds.Height / 2 + radius;
        // Almost every box pixel belongs to a straight edge or its fully opaque
        // interior. The expensive distance is needed only near a round corner.
        if (qx <= 0 || qy <= 0)
            return Math.Clamp(.5 - (Math.Max(qx, qy) - radius), 0, 1);
        double squared = qx * qx + qy * qy;
        if (radius >= .5 && squared < (radius - .5) * (radius - .5)) return 1;
        if (squared > (radius + .5) * (radius + .5)) return 0;
        return Math.Clamp(.5 - (Math.Sqrt(squared) - radius), 0, 1);
    }
    private static double Distance(BrowserRect rect, BrowserEdges radii, double x, double y)
    {
        if (rect.Width <= 0 || rect.Height <= 0) return double.MaxValue;
        double radius = x < rect.X + rect.Width / 2 ? y < rect.Y + rect.Height / 2 ? radii.Top : radii.Left : y < rect.Y + rect.Height / 2 ? radii.Right : radii.Bottom;
        double qx = Math.Abs(x - rect.X - rect.Width / 2) - rect.Width / 2 + radius;
        double qy = Math.Abs(y - rect.Y - rect.Height / 2) - rect.Height / 2 + radius;
        return Math.Sqrt(Math.Max(0, qx) * Math.Max(0, qx) + Math.Max(0, qy) * Math.Max(0, qy)) + Math.Min(Math.Max(qx, qy), 0) - radius;
    }
    private static double Erf(double x)
    {
        double sign = x < 0 ? -1 : 1; x = Math.Abs(x); double t = 1 / (1 + .3275911 * x);
        return sign * (1 - (((((1.061405429 * t - 1.453152027) * t) + 1.421413741) * t - .284496736) * t + .254829592) * t * Math.Exp(-x * x));
    }
    private static void DrawSolidRows(IDesktopCoverageCanvas coverage, BrowserRect area, uint rgb,
        BrowserRect box, BrowserEdges radius, double alpha, double opacity)
    {
        int left=(int)Math.Floor(area.Left),right=(int)Math.Ceiling(area.Right),top=(int)Math.Floor(area.Top),bottom=(int)Math.Ceiling(area.Bottom);
        if(right<=left||bottom<=top)return;
        var row=new byte[right-left];
        byte interior=(byte)Math.Clamp(Math.Round(alpha*opacity*255),0,255);
        double largest=Math.Max(Math.Max(radius.Top,radius.Right),Math.Max(radius.Bottom,radius.Left));
        for(int y=top;y<bottom;y++)
        {
            if(coverage is IDesktopRenderYieldCanvas yielding && yielding.RenderYieldRequested)throw new DesktopRenderYieldException();
            // Only the guaranteed fully covered interior is constant. Fractional
            // edges and rounded corners retain the original device-pixel formula.
            int start=right,end=right;
            if(y+.5>=box.Top+.5&&y+.5<=box.Bottom-.5)
            {
                bool middle=y+.5>=box.Top+largest&&y+.5<=box.Bottom-largest;
                double inset=middle?0:largest;
                start=Math.Clamp((int)Math.Ceiling(box.Left+inset),left,right);
                end=Math.Clamp((int)Math.Floor(box.Right-inset),start,right);
            }
            bool any=false;
            for(int x=left;x<start;x++) {row[x-left]=(byte)Math.Clamp(Math.Round(Coverage(box,radius,x+.5,y+.5)*alpha*opacity*255),0,255);any|=row[x-left]!=0;}
            row.AsSpan(start-left,end-start).Fill(interior);any|=end>start&&interior!=0;
            for(int x=end;x<right;x++) {row[x-left]=(byte)Math.Clamp(Math.Round(Coverage(box,radius,x+.5,y+.5)*alpha*opacity*255),0,255);any|=row[x-left]!=0;}
            if(any)coverage.BlendCoverage(rgb,left,y,row.Length,1,row);
        }
    }
    private static void DrawRows(IDesktopCoverageCanvas coverage, BrowserRect area, uint rgb, Func<double, double, double> sample,
        BrowserRect? occluded = null)
    {
        int left = (int)Math.Floor(area.Left), right = (int)Math.Ceiling(area.Right), top = (int)Math.Floor(area.Top), bottom = (int)Math.Ceiling(area.Bottom);
        if (right <= left || bottom <= top) return;
        var row = new byte[right - left];
        for (int y = top; y < bottom; y++)
        {
            if(coverage is IDesktopRenderYieldCanvas yielding && yielding.RenderYieldRequested)throw new DesktopRenderYieldException();
            int skipStart = right, skipEnd = right;
            if (occluded is { } hidden && y + .5 >= hidden.Top && y + .5 <= hidden.Bottom)
            {
                skipStart = (int)Math.Clamp(Math.Ceiling(hidden.Left - .5), left, right);
                skipEnd = (int)Math.Clamp(Math.Floor(hidden.Right - .5) + 1, skipStart, right);
            }
            // Do not even submit the covered span. Besides avoiding sqrt/exp,
            // this avoids walking an artificial zero-alpha mask in the driver.
            for (int segment = 0; segment < 2; segment++)
            {
                int start = segment == 0 ? left : skipEnd, end = segment == 0 ? skipStart : right;
                bool any = false;
                for (int x = start; x < end; x++) { row[x - left] = (byte)Math.Clamp(Math.Round(sample(x + .5, y + .5) * 255), 0, 255); any |= row[x - left] != 0; }
                if (any) coverage.BlendCoverage(rgb, start, y, end - start, 1, row.AsSpan(start - left, end - start));
            }
        }
    }
    private static void DrawGradientRows(IDesktopCoverageCanvas coverage, BrowserRect area, BrowserRect box, BrowserEdges radius, BrowserCssGradient gradient, double opacity)
    {
        int left=(int)Math.Floor(area.Left),right=(int)Math.Ceiling(area.Right),top=(int)Math.Floor(area.Top),bottom=(int)Math.Ceiling(area.Bottom);
        if(right<=left||bottom<=top)return;
        var row=new byte[right-left]; var colors=new uint[row.Length];
        var sampler = gradient.Prepare(box.Width, box.Height);
        for(int y=top;y<bottom;y++)
        {
            if(coverage is IDesktopRenderYieldCanvas yielding && yielding.RenderYieldRequested)throw new DesktopRenderYieldException();
            bool any = false;
            for(int x=left;x<right;x++)
            {
                var color=sampler.Sample(x+.5-box.X,y+.5-box.Y); colors[x-left]=color.Rgb;
                // Preserve the original interpolation and nonzero-alpha
                // arithmetic. A transparent sample needs no rounded coverage.
                row[x-left] = color.Alpha == 0 || opacity == 0 ? (byte)0 :
                    (byte)Math.Clamp(Math.Round(color.Alpha*opacity*Coverage(box,radius,x+.5,y+.5)*255),0,255);
                any |= row[x-left] != 0;
            }
            if (!any) continue;
            for(int start=0;start<row.Length;)
            {
                int end=start+1; bool groupHasInk = row[start] != 0;
                while(end<row.Length&&colors[end]==colors[start]) { groupHasInk |= row[end] != 0; end++; }
                if (groupHasInk) coverage.BlendCoverage(colors[start],left+start,y,end-start,1,row.AsSpan(start,end-start));
                start=end;
            }
        }
    }
}