diff --git a/src/Core/src/VisualDiagnostics/AdornerModel.cs b/src/Core/src/VisualDiagnostics/AdornerModel.cs new file mode 100755 index 000000000000..85b809294dc0 --- /dev/null +++ b/src/Core/src/VisualDiagnostics/AdornerModel.cs @@ -0,0 +1,212 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using Microsoft.Maui.Graphics; +using Microsoft.Maui.Platform; + +namespace Microsoft.Maui +{ + internal class AdornerModel + { + Rect boundingBox = new Rect(); + List marginZones = new List(); + + public Rect BoundingBox => boundingBox; + public IReadOnlyList MarginZones => marginZones; + + + public void Update(Rect rect, Thickness margin, Matrix4x4 transformToRoot, double density) + { + double unitsPerPixel = 1 / density; + boundingBox = DpiHelper.RoundToPixel(rect, unitsPerPixel); + + marginZones.Clear(); + + if (margin == new Thickness()) + { + return; + } + + // If transform to root is more than just an offset (i.e. element or + // some of its ancestors are scaled/rotated/etc.) then no margins to + // render. This matches WPF, UWP and WinUI adorners. + if (!Tolerances.AreClose(transformToRoot.M11, 1) || + !Tolerances.AreClose(transformToRoot.M22, 1) || + !Tolerances.AreClose(transformToRoot.M12, 0) || + !Tolerances.AreClose(transformToRoot.M21, 0)) + { + return; + } + + // Create up to 4 rectangles for margins. Keep in mind that some of + // margin values can be negative, e.g. Margin="-10, 20, 30, -40". + + // Left + if (!Tolerances.NearZero(margin.Left)) + { + Rect rc = new Rect(); + rc.Left = Math.Min(rect.Left - margin.Left, rect.Left); + rc.Width = Math.Abs(margin.Left); + rc.Top = rect.Top; + rc.Bottom = rect.Bottom; + TryAddMarginZone(rc, unitsPerPixel); + } + + // Right + if (!Tolerances.NearZero(margin.Right)) + { + Rect rc = new Rect(); + rc.Left = Math.Min(rect.Right, rect.Right + margin.Right); + rc.Width = Math.Abs(margin.Right); + rc.Top = rect.Top; + rc.Bottom = rect.Bottom; + TryAddMarginZone(rc, unitsPerPixel); + } + + // Top + if (!Tolerances.NearZero(margin.Top)) + { + Rect rc = new Rect(); + rc.Left = rect.Left - Math.Max(0, margin.Left); + rc.Right = rect.Right + Math.Max(0, margin.Right); + rc.Top = Math.Min(rect.Top - margin.Top, rect.Top); + rc.Height = Math.Abs(margin.Top); + TryAddMarginZone(rc, unitsPerPixel); + } + + // Bottom + if (!Tolerances.NearZero(margin.Bottom)) + { + Rect rc = new Rect(); + rc.Left = rect.Left - Math.Max(0, margin.Left); + rc.Right = rect.Right + Math.Max(0, margin.Right); + rc.Top = Math.Min(rect.Bottom + margin.Bottom, rect.Bottom); + rc.Height = Math.Abs(margin.Bottom); + TryAddMarginZone(rc, unitsPerPixel); + } + } + + void TryAddMarginZone(Rect rect, double unitsPerPixel) + { + Rect rc = DpiHelper.RoundToPixel(rect, unitsPerPixel); + if (!rc.IsEmpty) + marginZones.Add(rc); + } + + /// + /// DPI related utilities. + /// + private static class DpiHelper + { + /// + /// Rounds unit value to nearest pixel. + /// + /// + /// Rounded value in units. + /// + public static double RoundToPixel(double units, double unitsPerPixel) + { + double pixels = units / unitsPerPixel; + double floorPixels = (double)Math.Floor(pixels); + pixels = Tolerances.LessThan(pixels, floorPixels + 0.5) ? + floorPixels : (double)Math.Ceiling(pixels); + return pixels * unitsPerPixel; + } + + /// + /// Rounds point X and Y coordinates to nearest pixel. + /// + public static Point RoundToPixel(Point point, double unitsPerPixel) + { + Point pixelPoint = new Point( + DpiHelper.RoundToPixel(point.X, unitsPerPixel), + DpiHelper.RoundToPixel(point.Y, unitsPerPixel)); + return pixelPoint; + } + + /// + /// Rounds rectangle corner coordinates to nearest pixel. + /// + public static Rect RoundToPixel(Rect rect, double unitsPerPixel) + { + double left = DpiHelper.RoundToPixel(rect.Left, unitsPerPixel); + double top = DpiHelper.RoundToPixel(rect.Top, unitsPerPixel); + double right = DpiHelper.RoundToPixel(rect.Right, unitsPerPixel); + double bottom = DpiHelper.RoundToPixel(rect.Bottom, unitsPerPixel); + return new Rect(left, top, right - left, bottom - top); + } + } + + /// + /// Helper utilities to compare double values. + /// + private static class Tolerances + { + const double Epsilon = 2.2204460492503131e-016; + const double ZeroThreshold = 2.2204460492503131e-015; + + public static bool AreClose(Point point1, Point point2) + { + if (Tolerances.AreClose(point1.X, point2.X) && Tolerances.AreClose(point1.Y, point2.Y)) + { + return true; + } + + return false; + } + + public static bool NearZero(double value) + { + return Math.Abs(value) < Tolerances.ZeroThreshold; + } + + public static bool AreClose(double value1, double value2) + { + //in case they are Infinities (then epsilon check does not work) + if (value1 == value2) return true; + // This computes (|value1-value2| / (|value1| + |value2| + 10.0)) < Tolerances.Epsilon + double eps = (Math.Abs(value1) + Math.Abs(value2) + 10.0) * Tolerances.Epsilon; + double delta = value1 - value2; + return (-eps < delta) && (eps > delta); + } + + public static bool GreaterThan(double value1, double value2) + { + if (value1 > value2) + { + return !Tolerances.AreClose(value1, value2); + } + return false; + } + + public static bool GreaterThanOrClose(double value1, double value2) + { + if (value1 <= value2) + { + return Tolerances.AreClose(value1, value2); + } + return true; + } + + public static bool LessThan(double value1, double value2) + { + if (value1 < value2) + { + return !Tolerances.AreClose(value1, value2); + } + return false; + } + + public static bool LessThanOrClose(double value1, double value2) + { + if (value1 >= value2) + { + return Tolerances.AreClose(value1, value2); + } + return true; + } + } + } +} + diff --git a/src/Core/src/VisualDiagnostics/RectangleAdorner.cs b/src/Core/src/VisualDiagnostics/RectangleAdorner.cs old mode 100644 new mode 100755 index 3f8f725a411f..160a2cbcb866 --- a/src/Core/src/VisualDiagnostics/RectangleAdorner.cs +++ b/src/Core/src/VisualDiagnostics/RectangleAdorner.cs @@ -1,4 +1,7 @@ -using Microsoft.Maui.Graphics; +using System.Diagnostics; +using System.Linq; +using System.Numerics; +using Microsoft.Maui.Graphics; namespace Microsoft.Maui { @@ -7,6 +10,9 @@ namespace Microsoft.Maui /// public class RectangleAdorner : IAdorner { + + readonly AdornerModel model = new(); + /// /// Initializes a new instance of the class. /// @@ -22,7 +28,13 @@ public RectangleAdorner(IView view, float density = 1, Point? offset = null, Col Offset = offset ?? Point.Zero; VisualView = view; Density = density; - DrawnRectangle = Rect.Zero; + + // Sanity check + if (Density < 0.1) + { + Density = 1; + Debug.Fail($"Invalid density {density}"); + } } /// @@ -37,27 +49,54 @@ public RectangleAdorner(IView view, float density = 1, Point? offset = null, Col public Color StrokeColor { get; } - public Rect DrawnRectangle { get; private set; } + public Rect DrawnRectangle => model.BoundingBox; /// - public virtual bool Contains(Point point) => - DrawnRectangle.Contains(point); + public virtual bool Contains(Point point) + { + if (model.BoundingBox.Contains(point)) + { + return true; + } + + if (model.MarginZones.Any(r => r.Contains(point))) + { + return true; + } + + return false; + } /// public virtual void Draw(ICanvas canvas, RectF dirtyRect) { + UpdateModel(); + + // Draw highlight rectangle canvas.FillColor = FillColor; canvas.StrokeColor = StrokeColor; + canvas.FillRectangle(model.BoundingBox); + } + + void UpdateModel() + { + Rect box = VisualView.GetBoundingBox(); - var boundingBox = VisualView.GetBoundingBox(); - var x = (boundingBox.X / Density) + Offset.X; - var y = (boundingBox.Y / Density) + Offset.Y; - var width = boundingBox.Width / Density; - var height = boundingBox.Height / Density; + box.X = box.X + Offset.X; + box.Y = box.Y + Offset.Y; - DrawnRectangle = new Rect(x, y, width, height); + Matrix4x4 transform = VisualView.GetViewTransform(); + Thickness margin; + if (VisualView is IView view) + { + margin = view.Margin; + } + else + { + margin = new Thickness(); + } - canvas.FillRectangle(DrawnRectangle); + model.Update(box, margin, transform, Density); } } } \ No newline at end of file