SizeInfo.cs
  1  // Copyright (c) Microsoft Corporation
  2  // The Microsoft Corporation licenses this file to you under the MIT license.
  3  // See the LICENSE file in the project root for more information.
  4  
  5  using MouseJump.Common.Models.Styles;
  6  using BorderStyle = MouseJump.Common.Models.Styles.BorderStyle;
  7  
  8  namespace MouseJump.Common.Models.Drawing;
  9  
 10  /// <summary>
 11  /// Immutable version of a System.Drawing.Size object with some extra utility methods.
 12  /// </summary>
 13  public sealed class SizeInfo
 14  {
 15      public SizeInfo(decimal width, decimal height)
 16      {
 17          this.Width = width;
 18          this.Height = height;
 19      }
 20  
 21      public SizeInfo(Size size)
 22          : this(size.Width, size.Height)
 23      {
 24      }
 25  
 26      public decimal Width
 27      {
 28          get;
 29      }
 30  
 31      public decimal Height
 32      {
 33          get;
 34      }
 35  
 36      public SizeInfo Clamp(SizeInfo max)
 37      {
 38          return new(
 39              width: Math.Clamp(this.Width, 0, max.Width),
 40              height: Math.Clamp(this.Height, 0, max.Height));
 41      }
 42  
 43      public SizeInfo Clamp(decimal maxWidth, decimal maxHeight)
 44      {
 45          return new(
 46              width: Math.Clamp(this.Width, 0, maxWidth),
 47              height: Math.Clamp(this.Height, 0, maxHeight));
 48      }
 49  
 50      public SizeInfo Enlarge(BorderStyle border) =>
 51          new(
 52              this.Width + border.Horizontal,
 53              this.Height + border.Vertical);
 54  
 55      public SizeInfo Enlarge(PaddingStyle padding) =>
 56          new(
 57              this.Width + padding.Horizontal,
 58              this.Height + padding.Vertical);
 59  
 60      /// <summary>
 61      /// Rounds down the width and height of this size to the nearest whole number.
 62      /// </summary>
 63      /// <returns>A new <see cref="SizeInfo"/> instance with floored dimensions.</returns>
 64      public SizeInfo Floor()
 65      {
 66          return new SizeInfo(
 67              Math.Floor(this.Width),
 68              Math.Floor(this.Height));
 69      }
 70  
 71      /// <summary>
 72      /// Calculates the intersection of this size with another size, resulting in a size that represents
 73      /// the overlapping dimensions. Both sizes must be non-negative.
 74      /// </summary>
 75      /// <param name="size">The size to intersect with this instance.</param>
 76      /// <returns>A new <see cref="SizeInfo"/> instance representing the intersection of the two sizes.</returns>
 77      /// <exception cref="ArgumentException">Thrown when either this size or the specified size has negative dimensions.</exception>
 78      public SizeInfo Intersect(SizeInfo size)
 79      {
 80          if ((this.Width < 0) || (this.Height < 0) || (size.Width < 0) || (size.Height < 0))
 81          {
 82              throw new ArgumentException("Sizes must be non-negative");
 83          }
 84  
 85          return new(
 86              Math.Min(this.Width, size.Width),
 87              Math.Min(this.Height, size.Height));
 88      }
 89  
 90      /// <summary>
 91      /// Creates a new <see cref="SizeInfo"/> instance with the width and height negated, effectively inverting its dimensions.
 92      /// </summary>
 93      /// <returns>A new <see cref="SizeInfo"/> instance with inverted dimensions.</returns>
 94      public SizeInfo Invert() =>
 95          new(-this.Width, -this.Height);
 96  
 97      /// <summary>
 98      /// Creates a new <see cref="RectangleInfo"/> instance representing a rectangle with this size,
 99      /// positioned at the specified coordinates.
100      /// </summary>
101      /// <param name="x">The x-coordinate of the upper-left corner of the rectangle.</param>
102      /// <param name="y">The y-coordinate of the upper-left corner of the rectangle.</param>
103      /// <returns>A new <see cref="RectangleInfo"/> instance representing the positioned rectangle.</returns>
104      public RectangleInfo PlaceAt(decimal x, decimal y) =>
105          new(x, y, this.Width, this.Height);
106  
107      public SizeInfo Round() =>
108          this.Round(0);
109  
110      public SizeInfo Round(int decimals) => new(
111          Math.Round(this.Width, decimals),
112          Math.Round(this.Height, decimals));
113  
114      public SizeInfo Scale(decimal scalingFactor) => new(
115          this.Width * scalingFactor,
116          this.Height * scalingFactor);
117  
118      /// <summary>
119      /// Scales this size to fit within the bounds of another size, while maintaining the aspect ratio.
120      /// </summary>
121      /// <param name="bounds">The size to fit this size into.</param>
122      /// <returns>A new <see cref="SizeInfo"/> instance representing the scaled size.</returns>
123      public SizeInfo ScaleToFit(SizeInfo bounds, out decimal scalingRatio)
124      {
125          var widthRatio = bounds.Width / this.Width;
126          var heightRatio = bounds.Height / this.Height;
127          switch (widthRatio.CompareTo(heightRatio))
128          {
129              case < 0:
130                  scalingRatio = widthRatio;
131                  return new(bounds.Width, this.Height * widthRatio);
132              case 0:
133                  // widthRatio and heightRatio are the same, so just pick one
134                  scalingRatio = widthRatio;
135                  return bounds;
136              case > 0:
137                  scalingRatio = heightRatio;
138                  return new(this.Width * heightRatio, bounds.Height);
139          }
140      }
141  
142      /// <summary>
143      /// Calculates the scaling ratio needed to fit this size within the bounds of another size without distorting the aspect ratio.
144      /// </summary>
145      /// <param name="bounds">The size to fit this size into.</param>
146      /// <returns>The scaling ratio as a decimal.</returns>
147      /// <exception cref="ArgumentException">Thrown if the width or height of the bounds is zero.</exception>
148      public decimal ScaleToFitRatio(SizeInfo bounds)
149      {
150          if (bounds.Width == 0 || bounds.Height == 0)
151          {
152              throw new ArgumentException($"{nameof(bounds.Width)} or {nameof(bounds.Height)} cannot be zero", nameof(bounds));
153          }
154  
155          var widthRatio = bounds.Width / this.Width;
156          var heightRatio = bounds.Height / this.Height;
157          var scalingRatio = Math.Min(widthRatio, heightRatio);
158  
159          return scalingRatio;
160      }
161  
162      public SizeInfo Shrink(BorderStyle border) =>
163          new(this.Width - border.Horizontal, this.Height - border.Vertical);
164  
165      public SizeInfo Shrink(MarginStyle margin) =>
166          new(this.Width - margin.Horizontal, this.Height - margin.Vertical);
167  
168      public SizeInfo Shrink(PaddingStyle padding) =>
169          new(this.Width - padding.Horizontal, this.Height - padding.Vertical);
170  
171      public Size ToSize() => new((int)this.Width, (int)this.Height);
172  
173      public Point ToPoint() => new((int)this.Width, (int)this.Height);
174  
175      public override string ToString()
176      {
177          return "{" +
178              $"{nameof(this.Width)}={this.Width}," +
179              $"{nameof(this.Height)}={this.Height}" +
180              "}";
181      }
182  }