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 }