diff --git a/src/Uno.UI/UI/Xaml/Shapes/Shape.wasm.cs b/src/Uno.UI/UI/Xaml/Shapes/Shape.wasm.cs index 104723dd3dd8..14345d9106ea 100644 --- a/src/Uno.UI/UI/Xaml/Shapes/Shape.wasm.cs +++ b/src/Uno.UI/UI/Xaml/Shapes/Shape.wasm.cs @@ -1,11 +1,15 @@ -using System; +#nullable enable + +using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Numerics; using Windows.Foundation; +using Windows.UI; using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Media; +using Microsoft.UI.Xaml.Media.Imaging; using Microsoft.UI.Xaml.Wasm; using Uno; using Uno.Collections; @@ -15,6 +19,7 @@ using Uno.UI.Xaml; using RadialGradientBrush = Microsoft/* UWP don't rename */.UI.Xaml.Media.RadialGradientBrush; +using BrushDef = (Microsoft.UI.Xaml.UIElement Def, System.IDisposable? InnerSubscription); namespace Microsoft.UI.Xaml.Shapes { @@ -27,19 +32,23 @@ internal static int BBoxCacheSize get => _bboxCache.Capacity; set => _bboxCache.Capacity = value; } + + private record UpdateRenderPropertiesHashes(int? Fill, int? Stroke, int? StrokeWidth, int? StrokeDashArray); } partial class Shape { - private protected string _bboxCacheKey; + private protected string? _bboxCacheKey; private readonly SerialDisposable _fillBrushSubscription = new SerialDisposable(); private readonly SerialDisposable _strokeBrushSubscription = new SerialDisposable(); - private DefsSvgElement _defs; + private DefsSvgElement? _defs; private protected readonly SvgElement _mainSvgElement; private protected bool _shouldUpdateNative = !FeatureConfiguration.Shape.WasmDelayUpdateUntilFirstArrange; + private UpdateRenderPropertiesHashes _lastRenderHashes = new(null, null, StrokeWidth: 0d.GetHashCode(), null); + protected Shape() : base("svg", isSvg: true) { // This constructor shouldn't be used. It exists to match WinUI API surface. @@ -68,161 +77,140 @@ private protected void UpdateRender() // StrokeThickness can alter getBBox on Ellipse and Rectangle, but we dont use getBBox in these two. - OnFillBrushChanged(); // fill - OnStrokeBrushChanged(); // stroke - UpdateStrokeThickness(); // stroke-width - UpdateStrokeDashArray(); // stroke-dasharray - } + // nested subscriptions of SolidColorBrush::{ Color, Opacity } for Fill and Stroke + // are done in OnFillChanged/OnStrokeChanged which in turns calls OnFillBrushChanged/OnStrokeBrushChanged + // on brush changes and on nested properties changes. + + var hashes = new UpdateRenderPropertiesHashes + ( + Fill: GetHashOfInterestFor(GetActualFill()), + Stroke: GetHashOfInterestFor(Stroke), + StrokeWidth: ActualStrokeThickness.GetHashCode(), + StrokeDashArray: GetHashOfInterestFor(StrokeDashArray) + ); + + switch ( + _lastRenderHashes.Fill != hashes.Fill, + _lastRenderHashes.Stroke != hashes.Stroke, + _lastRenderHashes.StrokeWidth != hashes.StrokeWidth, + _lastRenderHashes.StrokeDashArray != hashes.StrokeDashArray + ) + { + case (true, false, false, false): UpdateSvgFill(); break; + case (false, true, false, false): UpdateSvgStroke(); break; + case (false, false, true, false): UpdateSvgStrokeWidth(); break; + case (false, false, false, true): UpdateSvgStrokeDashArray(); break; + case (true, true, false, false): UpdateSvgFillAndStroke(); break; + + case (false, false, false, false): return; + default: UpdateSvgEverything(); break; + }; + _lastRenderHashes = hashes; + + // todo@xy: we need to ensure dp-of-interests guarantees an arrange call if changed + } private void OnFillBrushChanged() { if (!_shouldUpdateNative) return; - // We don't request an update of the HitTest (UpdateHitTest()) since this element is never expected to be hit testable. - // Note: We also enforce that the default hit test == false is not altered in the OnHitTestVisibilityChanged. - - // Instead we explicitly set the IsHitTestVisible on each child SvgElement - var fill = Fill; - - // Known issue: The hit test is only linked to the Fill, but should also take in consideration the Stroke and the StrokeThickness. - // Note: _mainSvgElement and _defs are internal elements, so it's legit to alter the IsHitTestVisible here. - _mainSvgElement.IsHitTestVisible = fill != null; - if (_defs is not null) + var hash = GetHashOfInterestFor(GetActualFill()); + if (hash != _lastRenderHashes.Fill) { - _defs.IsHitTestVisible = fill != null; + UpdateSvgFill(); + _lastRenderHashes = _lastRenderHashes with { Fill = hash }; } + } + private void OnStrokeBrushChanged() + { + if (!_shouldUpdateNative) return; - var svgElement = _mainSvgElement; - switch (fill) + var hash = GetHashOfInterestFor(Stroke); + if (hash != _lastRenderHashes.Fill) { - case SolidColorBrush scb: - Uno.UI.Xaml.WindowManagerInterop.SetElementFill(svgElement.HtmlId, scb.ColorWithOpacity); - _fillBrushSubscription.Disposable = null; - break; - case ImageBrush ib: - var (imageFill, subscription) = ib.ToSvgElement(this); - var imageFillId = imageFill.HtmlId; - GetDefs().Add(imageFill); - svgElement.SetStyle("fill", $"url(#{imageFillId})"); - var removeDef = new DisposableAction(() => GetDefs().Remove(imageFill)); - _fillBrushSubscription.Disposable = new CompositeDisposable(removeDef, subscription); - break; - case GradientBrush gb: - var gradient = gb.ToSvgElement(); - var gradientId = gradient.HtmlId; - GetDefs().Add(gradient); - svgElement.SetStyle("fill", $"url(#{gradientId})"); - _fillBrushSubscription.Disposable = new DisposableAction( - () => GetDefs().Remove(gradient) - ); - break; - case RadialGradientBrush rgb: - var radialGradient = rgb.ToSvgElement(); - var radialGradientId = radialGradient.HtmlId; - GetDefs().Add(radialGradient); - svgElement.SetStyle("fill", $"url(#{radialGradientId})"); - _fillBrushSubscription.Disposable = new DisposableAction( - () => GetDefs().Remove(radialGradient) - ); - break; - case AcrylicBrush ab: - svgElement.SetStyle("fill", ab.FallbackColorWithOpacity.ToHexString()); - _fillBrushSubscription.Disposable = null; - break; - case null: - // The default is black if the style is not set in Web's' SVG. So if the Fill property is not set, - // we explicitly set the style to transparent in order to match the UWP behavior. - svgElement.SetStyle("fill", "transparent"); - _fillBrushSubscription.Disposable = null; - break; - default: - svgElement.ResetStyle("fill"); - _fillBrushSubscription.Disposable = null; - break; + UpdateSvgStroke(); + _lastRenderHashes = _lastRenderHashes with { Stroke = hash }; } } - private void OnStrokeBrushChanged() + private void UpdateSvgFill() { if (!_shouldUpdateNative) return; - var svgElement = _mainSvgElement; - var stroke = Stroke; + UpdateHitTestVisibility(); - switch (stroke) - { - case SolidColorBrush scb: - svgElement.SetStyle("stroke", scb.ColorWithOpacity.ToHexString()); - _strokeBrushSubscription.Disposable = null; - break; - case ImageBrush ib: - var (imageFill, subscription) = ib.ToSvgElement(this); - var imageFillId = imageFill.HtmlId; - GetDefs().Add(imageFill); - svgElement.SetStyle("stroke", $"url(#{imageFillId})"); - var removeDef = new DisposableAction(() => GetDefs().Remove(imageFill)); - _fillBrushSubscription.Disposable = new CompositeDisposable(removeDef, subscription); - break; - case GradientBrush gb: - var gradient = gb.ToSvgElement(); - var gradientId = gradient.HtmlId; - GetDefs().Add(gradient); - svgElement.SetStyle("stroke", $"url(#{gradientId})"); - _strokeBrushSubscription.Disposable = new DisposableAction( - () => GetDefs().Remove(gradient) - ); - break; - case RadialGradientBrush rgb: - var radialGradient = rgb.ToSvgElement(); - var radialGradientId = radialGradient.HtmlId; - GetDefs().Add(radialGradient); - svgElement.SetStyle("stroke", $"url(#{radialGradientId})"); - _strokeBrushSubscription.Disposable = new DisposableAction( - () => GetDefs().Remove(radialGradient) - ); - break; - case AcrylicBrush ab: - svgElement.SetStyle("stroke", ab.FallbackColorWithOpacity.ToHexString()); - _strokeBrushSubscription.Disposable = null; - break; - default: - svgElement.ResetStyle("stroke"); - _strokeBrushSubscription.Disposable = null; - break; - } + var (color, def) = GetBrushImpl(GetActualFill()); + + _fillBrushSubscription.Disposable = TryAppendBrushDef(def); + WindowManagerInterop.SetShapeFillStyle(_mainSvgElement.HtmlId, color?.ToCssIntegerAsInt(), def?.Def.HtmlId); } + private void UpdateSvgStroke() + { + if (!_shouldUpdateNative) return; + + var (color, def) = GetBrushImpl(Stroke); - private void UpdateStrokeThickness() + _strokeBrushSubscription.Disposable = TryAppendBrushDef(def); + WindowManagerInterop.SetShapeStrokeStyle(_mainSvgElement.HtmlId, color?.ToCssIntegerAsInt(), def?.Def.HtmlId); + } + private void UpdateSvgStrokeWidth() { if (!_shouldUpdateNative) return; - var svgElement = _mainSvgElement; - var strokeThickness = ActualStrokeThickness; + WindowManagerInterop.SetShapeStrokeWidthStyle(_mainSvgElement.HtmlId, ActualStrokeThickness); + } + private void UpdateSvgStrokeDashArray() + { + if (!_shouldUpdateNative) return; - if (strokeThickness != 1.0d) - { - svgElement.SetStyle("stroke-width", $"{strokeThickness}px"); - } - else - { - svgElement.ResetStyle("stroke-width"); - } + WindowManagerInterop.SetShapeStrokeDashArrayStyle(_mainSvgElement.HtmlId, StrokeDashArray?.ToArray() ?? Array.Empty()); } + private void UpdateSvgFillAndStroke() + { + if (!_shouldUpdateNative) return; + + var fillImpl = GetBrushImpl(GetActualFill()); + var strokeImpl = GetBrushImpl(Stroke); - private void UpdateStrokeDashArray() + _fillBrushSubscription.Disposable = TryAppendBrushDef(fillImpl.Def); + _strokeBrushSubscription.Disposable = TryAppendBrushDef(strokeImpl.Def); + WindowManagerInterop.SetShapeStylesFast1( + _mainSvgElement.HtmlId, + fillImpl.Color?.ToCssIntegerAsInt(), fillImpl.Def?.Def.HtmlId, + strokeImpl.Color?.ToCssIntegerAsInt(), strokeImpl.Def?.Def.HtmlId + ); + } + private void UpdateSvgEverything() { if (!_shouldUpdateNative) return; - var svgElement = _mainSvgElement; + var fillImpl = GetBrushImpl(GetActualFill()); + var strokeImpl = GetBrushImpl(Stroke); - if (StrokeDashArray is not { } strokeDashArray) - { - svgElement.ResetStyle("stroke-dasharray"); - } - else + _fillBrushSubscription.Disposable = TryAppendBrushDef(fillImpl.Def); + _strokeBrushSubscription.Disposable = TryAppendBrushDef(strokeImpl.Def); + WindowManagerInterop.SetShapeStylesFast2( + _mainSvgElement.HtmlId, + fillImpl.Color?.ToCssIntegerAsInt(), fillImpl.Def?.Def.HtmlId, + strokeImpl.Color?.ToCssIntegerAsInt(), strokeImpl.Def?.Def.HtmlId, ActualStrokeThickness, StrokeDashArray?.ToArray() ?? Array.Empty() + ); + } + + + private void UpdateHitTestVisibility() + { + // We don't request an update of the HitTest (UpdateHitTest()) since this element is never expected to be hit testable. + // Note: We also enforce that the default hit test == false is not altered in the OnHitTestVisibilityChanged. + + // Instead we explicitly set the IsHitTestVisible on each child SvgElement + var fill = Fill; + + // Known issue: The hit test is only linked to the Fill, but should also take in consideration the Stroke and the StrokeThickness. + // Note: _mainSvgElement and _defs are internal elements, so it's legit to alter the IsHitTestVisible here. + _mainSvgElement.IsHitTestVisible = fill != null; + if (_defs is not null) { - var str = string.Join(",", strokeDashArray.Select(d => $"{d.ToStringInvariant()}px")); - svgElement.SetStyle("stroke-dasharray", str); + _defs.IsHitTestVisible = fill != null; } } @@ -260,7 +248,7 @@ private static Rect GetPathBoundingBox(Shape shape) return result; } - private protected void Render(Shape shape, Size? size = null, double scaleX = 1d, double scaleY = 1d, double renderOriginX = 0d, double renderOriginY = 0d) + private protected void Render(Shape? shape, Size? size = null, double scaleX = 1d, double scaleY = 1d, double renderOriginX = 0d, double renderOriginY = 0d) { Debug.Assert(shape == this); var scale = Matrix3x2.CreateScale((float)scaleX, (float)scaleY); @@ -281,9 +269,106 @@ internal override bool HitTest(Point relativePosition) } // lazy impl, and _cacheKey can be invalidated by setting to null - private string GetBBoxCacheKey() => _bboxCacheKey ?? (_bboxCacheKey = GetBBoxCacheKeyImpl()); + private string? GetBBoxCacheKey() => _bboxCacheKey ?? (_bboxCacheKey = GetBBoxCacheKeyImpl()); // note: perf is of concern here. avoid $"string interpolation" and current-culture .ToString, and use string.concat and ToStringInvariant - private protected abstract string GetBBoxCacheKeyImpl(); + private protected abstract string? GetBBoxCacheKeyImpl(); + + private Brush GetActualFill() + { + // The default is black if the style is not set in Web's' SVG. So if the Fill property is not set, + // we explicitly set the style to transparent in order to match the UWP behavior. + + return Fill ?? SolidColorBrushHelper.Transparent; + } + + private (Color? Color, BrushDef? Def) GetBrushImpl(Brush brush) => brush switch // todo@xy: fix the name... + { + SolidColorBrush scb => (scb.ColorWithOpacity, null), + ImageBrush ib => (null, ib.ToSvgElement(this)), + AcrylicBrush ab => (ab.FallbackColorWithOpacity, null), + LinearGradientBrush lgb => (null, (lgb.ToSvgElement(), null)), + RadialGradientBrush rgb => (null, (rgb.ToSvgElement(), null)), + // The default is black if the style is not set in Web's' SVG. So if the Fill property is not set, + // we explicitly set the style to transparent in order to match the UWP behavior. + null => (null, null), + + _ => default, + }; + private IDisposable? TryAppendBrushDef(BrushDef? def) + { + if (def is not { } d) return null; + + GetDefs().Add(d.Def); + return new DisposableAction(() => + { + GetDefs().Remove(d.Def); + d.InnerSubscription?.Dispose(); + }); + } + + private static int? GetHashOfInterestFor(Brush brush) + { + int GetLGBHash(LinearGradientBrush lgb) + { + var hash = new HashCode(); + hash.Add(lgb.StartPoint); + hash.Add(lgb.EndPoint); + if (lgb.GradientStops is { Count: > 0 }) + { + foreach (var stop in lgb.GradientStops) + { + hash.Add(stop); + } + } + + return hash.ToHashCode(); + } + int GetRGBHash(RadialGradientBrush rgb) + { + var hash = new HashCode(); + hash.Add(rgb.Center); + hash.Add(rgb.RadiusX); + hash.Add(rgb.RadiusX); + if (rgb.GradientStops is { Count: > 0 }) + { + foreach (var stop in rgb.GradientStops) + { + hash.Add(stop); + } + } + + return hash.ToHashCode(); + } + + return brush switch + { + SolidColorBrush scb => scb.ColorWithOpacity.GetHashCode(), + // We don't care about the nested properties of ImageBrush, + // because their changes will be updated through ImageBrush::ToSvgElement subscriptions. + // So an object's reference hash is good here. + ImageBrush ib => ib.GetHashCode(), + LinearGradientBrush lgb => GetLGBHash(lgb), + RadialGradientBrush rgb => GetRGBHash(rgb), + AcrylicBrush ab => ab.FallbackColorWithOpacity.GetHashCode(), + + _ => null, + }; + } + private static int? GetHashOfInterestFor(DoubleCollection doubles) + { + if (doubles is not { Count: > 0 }) + { + return null; + } + + var hash = new HashCode(); + foreach (var item in doubles) + { + hash.Add(item); + } + + return hash.ToHashCode(); + } } } diff --git a/src/Uno.UI/UI/Xaml/Window/WindowManagerInterop.wasm.cs b/src/Uno.UI/UI/Xaml/Window/WindowManagerInterop.wasm.cs index 1de561f6ef4a..8a433d97c4c9 100644 --- a/src/Uno.UI/UI/Xaml/Window/WindowManagerInterop.wasm.cs +++ b/src/Uno.UI/UI/Xaml/Window/WindowManagerInterop.wasm.cs @@ -22,7 +22,7 @@ namespace Uno.UI.Xaml /// /// An interop layer to invoke methods found in the WindowManager.ts file. /// - internal partial class WindowManagerInterop + public partial class WindowManagerInterop { //When users set double.MaxValue to scroll to the end of the page Javascript doesn't scroll. private const double MAX_SCROLLING_OFFSET = 1_000_000_000_000_000_000; @@ -418,29 +418,28 @@ private struct WindowManagerSetSelectionHighlightParams #endregion - #region SetElementFill + #region SetShapeStyles... - internal static void SetElementFill(IntPtr htmlId, Color color) - { - var colorAsInteger = color.ToCssInteger(); + // error SYSLIB1072: Type uint is not supported by source-generated JavaScript interop. + // ^ we can't use uint here for color, so int will do. - var parms = new WindowManagerSetElementFillParams() - { - HtmlId = htmlId, - Color = colorAsInteger, - }; + internal static void SetShapeFillStyle(IntPtr htmlId, int? color, IntPtr? paintRef) => NativeMethods.SetShapeFillStyle(htmlId, color, paintRef); - TSInteropMarshaller.InvokeJS("Uno:setElementFillNative", parms); - } + internal static void SetShapeStrokeStyle(IntPtr htmlId, int? color, IntPtr? paintRef) => NativeMethods.SetShapeFillStyle(htmlId, color, paintRef); - [TSInteropMessage] - [StructLayout(LayoutKind.Sequential, Pack = 4)] - private struct WindowManagerSetElementFillParams - { - public IntPtr HtmlId; + internal static void SetShapeStrokeWidthStyle(IntPtr htmlId, double strokeWidth) => NativeMethods.SetShapeStrokeWidthStyle(htmlId, strokeWidth); + + internal static void SetShapeStrokeDashArrayStyle(IntPtr htmlId, double[] strokeDashArray) => NativeMethods.SetShapeStrokeDashArrayStyle(htmlId, strokeDashArray); + + internal static void SetShapeStylesFast1(IntPtr htmlId, int? fillColor, IntPtr? fillPaintRef, int? strokeColor, IntPtr? strokePaintRef) => + NativeMethods.SetShapeStylesFast1(htmlId, fillColor, fillPaintRef, strokeColor, strokePaintRef); + + internal static void SetShapeStylesFast2( + IntPtr htmlId, + int? fillColor, IntPtr? fillPaintRef, + int? strokeColor, IntPtr? strokePaintRef, double strokeWidth, double[] strokeDashArray) => + NativeMethods.SetShapeStylesFast2(htmlId, fillColor, fillPaintRef, strokeColor, strokePaintRef, strokeWidth, strokeDashArray); - public uint Color; - } #endregion #region RemoveView @@ -765,6 +764,9 @@ internal static UIElement TryGetElementInCoordinate(Point point) internal static partial class NativeMethods { + private const string StaticThis = "globalThis.Uno.UI.WindowManager"; + private const string InstancedThis = "globalThis.Uno.UI.WindowManager.current"; + [JSImport("globalThis.Uno.UI.WindowManager.current.arrangeElementNativeFast")] internal static partial void ArrangeElement( IntPtr htmlId, @@ -888,6 +890,27 @@ internal static partial void ArrangeElement( [JSImport("globalThis.Uno.UI.WindowManager.current.getBBox")] internal static partial double[] GetBBox(IntPtr htmlId); + + [JSImport($"{InstancedThis}.setShapeFillStyle")] + internal static partial void SetShapeFillStyle(IntPtr htmlId, int? color, IntPtr? paintRef); + + [JSImport($"{InstancedThis}.setShapeStrokeStyle")] + internal static partial void SetShapeStrokeStyle(IntPtr htmlId, int? color, IntPtr? paintRef); + + [JSImport($"{InstancedThis}.setShapeStrokeWidthStyle")] + internal static partial void SetShapeStrokeWidthStyle(IntPtr htmlId, double strokeWidth); + + [JSImport($"{InstancedThis}.setShapeStrokeDashArrayStyle")] + internal static partial void SetShapeStrokeDashArrayStyle(IntPtr htmlId, double[] strokeDashArray); + + [JSImport($"{InstancedThis}.setShapeStylesFast1")] + internal static partial void SetShapeStylesFast1(IntPtr htmlId, int? fillColor, IntPtr? fillPaintRef, int? strokeColor, IntPtr? strokePaintRef); + + [JSImport($"{InstancedThis}.setShapeStylesFast2")] + internal static partial void SetShapeStylesFast2( + IntPtr htmlId, + int? fillColor, IntPtr? fillPaintRef, + int? strokeColor, IntPtr? strokePaintRef, double strokeWidth, double[] strokeDashArray); } } } diff --git a/src/Uno.UI/ts/WindowManager.ts b/src/Uno.UI/ts/WindowManager.ts index c39687471958..c762cd1166ca 100644 --- a/src/Uno.UI/ts/WindowManager.ts +++ b/src/Uno.UI/ts/WindowManager.ts @@ -629,21 +629,6 @@ namespace Uno.UI { return this.setSelectionHighlight(params.HtmlId, params.BackgroundColor, params.ForegroundColor); } - /** - * Sets the fill property of the specified element - */ - public setElementFillNative(pParam: number): boolean { - const params = WindowManagerSetElementFillParams.unmarshal(pParam); - this.setElementFillInternal(params.HtmlId, params.Color); - return true; - } - - private setElementFillInternal(elementId: number, color: number): void { - const element = this.getView(elementId); - - element.style.setProperty("fill", this.numberToCssColor(color)); - } - /** * Sets the background color property of the specified element */ @@ -1598,6 +1583,73 @@ namespace Uno.UI { private onBodyKeyUp(event: KeyboardEvent) { WindowManager.keyTrackingMethod(event.key, false); } + + private getCssColorOrUrlRef(color: number, paintRef: number): string { + if (paintRef != null) { + return `url(#${paintRef})`; + } + else if (color != null) { + // JSInvoke doesnt allow passing of uint, so we had to deal with int's "sign-ness" here + // (-1 >>> 0) is a quick hack to turn signed negative into "unsigned" positive + // padded to 8-digits 'RRGGBBAA', so the value doesnt get processed as 'RRGGBB' or 'RGB'. + return `#${(color >>> 0).toString(16).padStart(8, '0')}`; + } + else { + return ''; + } + } + + public setShapeFillStyle(elementId: number, color: number | null, paintRef: number | null): void { + let e = this.getView(elementId); + if (e instanceof SVGElement) { + + e.style.fill = this.getCssColorOrUrlRef(color, paintRef); + } + } + + public setShapeStrokeStyle(elementId: number, color: any, paintRef: number): void { + let e = this.getView(elementId); + if (e instanceof SVGElement) { + + e.style.stroke = this.getCssColorOrUrlRef(color, paintRef); + } + } + + public setShapeStrokeWidthStyle(elementId: number, strokeWidth: number): void { + let e = this.getView(elementId); + if (e instanceof SVGElement) { + + e.style.strokeWidth = `${strokeWidth}px`; + } + } + + public setShapeStrokeDashArrayStyle(elementId: number, strokeDashArray: number[]): void { + let e = this.getView(elementId); + if (e instanceof SVGElement) { + + e.style.strokeDasharray = strokeDashArray.join(','); + } + } + + public setShapeStylesFast1(elementId: number, fillColor: any, fillPaintRef: any, strokeColor: any, strokePaintRef: any): void { + let e = this.getView(elementId); + if (e instanceof SVGElement) { + + e.style.fill = this.getCssColorOrUrlRef(fillColor, fillPaintRef); + e.style.stroke = this.getCssColorOrUrlRef(strokeColor, strokePaintRef); + } + } + + public setShapeStylesFast2(elementId: number, fillColor: any, fillPaintRef: any, strokeColor: any, strokePaintRef: any, strokeWidth: number, strokeDashArray: any[]): void { + let e = this.getView(elementId); + if (e instanceof SVGElement) { + + e.style.fill = this.getCssColorOrUrlRef(fillColor, fillPaintRef); + e.style.stroke = this.getCssColorOrUrlRef(strokeColor, strokePaintRef); + e.style.strokeWidth = `${strokeWidth}px`; + e.style.strokeDasharray = strokeDashArray.join(','); + } + } } if (typeof define === "function") { diff --git a/src/Uno.UWP/UI/Color.cs b/src/Uno.UWP/UI/Color.cs index f18f100218bc..74e9d2d309ad 100644 --- a/src/Uno.UWP/UI/Color.cs +++ b/src/Uno.UWP/UI/Color.cs @@ -40,7 +40,9 @@ public partial struct Color : IFormattable internal Color(byte a, byte r, byte g, byte b) { - _color = 0; // Required for field initialization rules in C# + // Required for field initialization rules in C# + _color = 0; + _b = b; _g = g; _r = r; @@ -54,6 +56,7 @@ internal Color(uint color) _g = 0; _r = 0; _a = 0; + _color = color; } diff --git a/src/Uno.UWP/UI/Color.wasm.cs b/src/Uno.UWP/UI/Color.wasm.cs index a092578c023b..ade9d04b2323 100644 --- a/src/Uno.UWP/UI/Color.wasm.cs +++ b/src/Uno.UWP/UI/Color.wasm.cs @@ -18,14 +18,28 @@ internal string ToCssString() => + ")"; /// - /// Get color value in CSS format "rgba(r, g, b, a)" + /// Get color value in "rrggbbaa" as integer value /// + /// + /// IMPORTANT: We MUST NOT just naively prefix this with # in js without appropriate padding, + /// because of 6-digits (00GGBBAA as RRGGBB) and 3-digits (00000BAA as RGB) color notations. + /// internal uint ToCssInteger() => - (uint)(R << 24) - | (uint)(G << 16) - | (uint)(B << 8) - | A; + // = AARRGGBB << 8 | AA + // = RRGGBB00 | AA + // = RRGGBBAA + AsUInt32() << 8 | A; + + // [JSImport] doesnt allow for `uint` params, so we pass an `int` instead + internal int ToCssIntegerAsInt() => unchecked((int)ToCssInteger()); + /// + /// Get color value in "#rrggbb" or "#rrggbbaa" notation + /// + /// + /// The #rrggbbaa hex color notation requires modern browsers. It is not available in older versions of Internet Explorer. + /// See also: https://caniuse.com/css-rrggbbaa + /// internal string ToHexString() { var builder = new StringBuilder(10);