diff --git a/src/Common/Font.cs b/src/Common/Font.cs index 86e7776..3c576f5 100644 --- a/src/Common/Font.cs +++ b/src/Common/Font.cs @@ -1,6 +1,8 @@ using System; +using System.ComponentModel; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; using System.Threading.Tasks; @@ -8,14 +10,29 @@ namespace GeneXus.Drawing; +[TypeConverter(typeof(FontConverter))] [Serializable] public class Font : IDisposable, ICloneable { internal readonly FontFamily m_family; internal readonly float m_size; + internal readonly string m_original; - internal readonly string m_original = null; - + /// + /// Initializes a new System.Drawing.Font that uses the specified existing + /// and enumeration. + /// + /// The existing from which to create the new + /// + /// The to apply to the new S. Multiple + /// values of the enumeration can be combined with the OR operator. + /// + public Font(Font prototype, FontStyle newStyle) + { + m_family = prototype.FontFamily; + // TODO set newStyle + } + /// /// Initializes a new using the specified and size. /// @@ -29,12 +46,20 @@ public Font(FontFamily family, float size = 12) /// Initializes a new using the specified family name and size. /// public Font(string familyName, float size = 12) - : this(SystemFonts.Select(f => f.m_family).FirstOrDefault(f => f.MatchFamily(familyName)) - ?? throw new ArgumentException("missing font family", nameof(familyName)), size) + : this(SystemFonts.Select(f => f.m_family).FirstOrDefault(ff => ff is SkiaFontFamily sff && sff.MatchFamily(familyName)) ?? new UnknownFontFamily(familyName), size) { m_original = familyName; } + /// + /// Initializes a new using the specified family name, size, styl. + /// + public Font(string familyName, float size, FontStyle style, GraphicsUnit unit) + : this(familyName, size) + { + // TODO set style and unit + } + /// /// Cleans up resources for this . /// @@ -45,7 +70,7 @@ public Font(string familyName, float size = 12) /// public override string ToString() { - string suffix = m_family.m_index > 0 ? $"#{m_family.m_index}" : string.Empty; + string suffix = m_family is SkiaFontFamily { m_index: > 0 } sff ? $"#{sff.m_index}" : string.Empty; return $"[{GetType().Name}: Name={Name}{suffix}, Size={Size}]"; } @@ -69,16 +94,9 @@ public void Dispose() #region IClonable /// - /// Creates an exact copy of this . + /// Creates an exact copy of this . /// - public object Clone() - { - var index = m_family.m_index; - var bytes = m_family.m_data.ToArray(); - var data = SKData.CreateCopy(bytes); - var family = new FontFamily(data, index); - return new Font(family); - } + public object Clone() => new Font((FontFamily)m_family.Clone()); #endregion @@ -88,19 +106,17 @@ public object Clone() /// /// Creates a with the coordinates of the specified . /// - public static explicit operator SKFont(Font font) => font.m_family.GetFont(font.m_size); + public static explicit operator SKFont(Font font) => (font.m_family as SkiaFontFamily)?.GetFont(font.m_size); #endregion #region Properties - private SKFont m_font => m_family.GetFont(m_size); - /// /// Gets the face name of this . /// - public string Name => $"{m_family.Name} {m_family.Face}"; + public string Name => m_family is SkiaFontFamily sff ? $"{sff.Name} {sff.Face}" : m_family.Name; /// /// Gets the name of the originally specified. @@ -176,22 +192,24 @@ public FontStyle Style /// /// Gets a value that indicates whether this has the italic style applied. /// - public bool Italic => m_family.m_typeface.IsItalic; + public bool Italic => m_family.IsItalic; /// /// Gets a value that indicates whether this is bold. /// - public bool Bold => m_family.m_typeface.IsBold; + public bool Bold => m_family.IsBold; + private SKFontMetrics Metrics => m_family is SkiaFontFamily sff ? sff.GetFont(m_size).Metrics : new SKFontMetrics(); + /// /// Gets a value indicating whether this is underlined. /// - public bool Underline => m_font.Metrics.UnderlineThickness > 0 && m_font.Metrics.UnderlinePosition == 0f; + public bool Underline => Metrics is { UnderlineThickness: > 0, UnderlinePosition: 0f }; /// /// Gets a value indicating whether this is strikeout (has a line through it). /// - public bool Strikeout => m_font.Metrics.StrikeoutThickness > 0 && m_font.Metrics.StrikeoutPosition == 0f; + public bool Strikeout => Metrics is { StrikeoutThickness: > 0, StrikeoutPosition: 0f }; /// /// Gets a value indicating whether the is a member of SystemFonts. @@ -217,8 +235,29 @@ public static ICollection SystemFonts } } - private static ICollection s_SystemFonts = null; + private static ICollection s_SystemFonts; + /// + /// Gets the em-size, in points, of this . + /// + /// The em-size, in points, of this + [Browsable(false)] + public float SizeInPoints + { + get + { + // TODO calculate it for other units + Debug.Assert(Unit == GraphicsUnit.Point); + return Size; + } + } + + /// + /// Gets the unit of measure for this . + /// + /// A that represents the unit of measure for this . + public GraphicsUnit Unit => GraphicsUnit.Point; + #endregion @@ -227,7 +266,7 @@ public static ICollection SystemFonts /// /// Returns the line spacing of this . /// - public float GetHeight() => m_font.Metrics.Descent - m_font.Metrics.Ascent + m_font.Metrics.Leading; + public float GetHeight() => Metrics.Descent - Metrics.Ascent + Metrics.Leading; /// /// Returns a collection in the specified location. @@ -240,7 +279,7 @@ public static ICollection GetFonts(string location) { if (FONT_EXTENSIONS.Contains(Path.GetExtension(fontFile))) { - var family = new FontFamily(fontFile); + var family = FontFamilyFactory.Create(fontFile); var font = new Font(family); fonts.Add(font); } diff --git a/src/Common/FontConverter.cs b/src/Common/FontConverter.cs new file mode 100644 index 0000000..f487661 --- /dev/null +++ b/src/Common/FontConverter.cs @@ -0,0 +1,527 @@ +using System; +using System.Collections; +using System.ComponentModel; +using System.ComponentModel.Design.Serialization; +using System.Globalization; +using System.Linq; +using System.Reflection; + +namespace GeneXus.Drawing; + +/// +/// Converts System.Drawing.Font objects from one data type to another. +/// +public class FontConverter : TypeConverter +{ + internal class UnitName + { + internal readonly string m_name; + internal readonly GraphicsUnit m_unit; + + internal static readonly UnitName[] Names = { + new("world", GraphicsUnit.World), + new("display", GraphicsUnit.Display), + new("px", GraphicsUnit.Pixel), + new("pt", GraphicsUnit.Point), + new("in", GraphicsUnit.Inch), + new("doc", GraphicsUnit.Document), + new("mm", GraphicsUnit.Millimeter) + }; + + private UnitName(string name, GraphicsUnit unit) + { + m_name = name; + m_unit = unit; + } + } + + /// + /// is a type converter that is used + /// to convert a font name to and from various other representations. + /// + private sealed class FontNameConverter : TypeConverter + { + private StandardValuesCollection _values; + + /// + /// Determines if this converter can convert an object in the given source type to + /// the native type of the converter. + /// + /// + /// An that can be used to extract additional + /// information about the environment this converter is being invoked from. This + /// may be null, so you should always check. Also, properties on the context object + /// may return null. + /// + /// The type you wish to convert from. + /// true if the converter can perform the conversion; otherwise, false. + public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) + { + return sourceType == typeof(string) || base.CanConvertFrom(context, sourceType); + } + + /// + /// Converts the given object to the converter's native type. + /// + /// + /// An that can be used to extract additional + /// information about the environment this converter is being invoked from. This + /// may be null, so you should always check. Also, properties on the context object + /// may return null. + /// + /// A to use to perform the conversion + /// The object to convert. + /// The converted object. + /// The conversion cannot be completed. + public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) + { + if (value is string s) + return MatchFontName(s, context); + + return base.ConvertFrom(context, culture, value); + } + + /// + /// Retrieves a collection containing a set of standard values for the data type + /// this converter is designed for. + /// + /// + /// An that can be used to extract additional + /// information about the environment this converter is being invoked from. This + /// may be null, so you should always check. Also, properties on the context object + /// may return null. + /// + /// A collection containing a standard set of valid values, or null. The default is null. + public override StandardValuesCollection GetStandardValues(ITypeDescriptorContext context) + { + if (_values != null) + return _values; + + FontFamily[] families = FontFamily.Families; + Hashtable hashtable = new(); + foreach (FontFamily fontFamily in families) + { + string name = fontFamily.Name; + hashtable[name.ToLower(CultureInfo.InvariantCulture)] = name; + } + + object[] array = new object[hashtable.Values.Count]; + hashtable.Values.CopyTo(array, 0); + Array.Sort(array, Comparer.Default); + _values = new StandardValuesCollection(array); + + return _values; + } + + /// + /// Determines if the list of standard values returned from the Overload:System.Drawing.FontConverter.FontNameConverter.GetStandardValues + /// method is an exclusive list. + /// + /// + /// An that can be used to extract additional + /// information about the environment this converter is being invoked from. This + /// may be null, so you should always check. Also, properties on the context object + /// may return null. + /// + /// true if the collection returned from + /// is an exclusive list of possible values; otherwise, false. The default is false. + public override bool GetStandardValuesExclusive(ITypeDescriptorContext context) + { + return false; + } + + /// + /// Determines if this object supports a standard set of values that can be picked from a list. + /// + /// + /// An that can be used to extract additional + /// information about the environment this converter is being invoked from. This + /// may be null, so you should always check. Also, properties on the context object + /// may return null. + /// + /// + /// true if Overload:System.Drawing.FontConverter.FontNameConverter.GetStandardValues + /// should be called to find a common set of values the object supports; otherwise, + /// false. + /// + public override bool GetStandardValuesSupported(ITypeDescriptorContext context) + { + return true; + } + + private string MatchFontName(string name, ITypeDescriptorContext context) + { + string candidate = null; + string lowerName = name.ToLower(CultureInfo.InvariantCulture); + foreach (string fontName in GetStandardValues(context)) + { + string lowerFontName = fontName.ToLower(CultureInfo.InvariantCulture); + if (lowerFontName.Equals(lowerName)) + return fontName; + + if (lowerFontName.StartsWith(name) && (candidate == null || lowerFontName.Length <= candidate.Length)) + candidate = fontName; + } + + return candidate ?? name; + } + + private void OnInstalledFontsChanged(object sender, EventArgs e) + { + _values = null; + } + } + + /// + /// Converts font units to and from other unit types. + /// + private class FontUnitConverter : EnumConverter + { + /// + /// Initializes a new instance of the class. + /// + public FontUnitConverter() + : base(typeof(GraphicsUnit)) + { + } + + /// + /// Returns a collection of standard values valid for the type. + /// + /// An that provides a format context. + /// A collection containing a standard set of valid values, or null. The default is null. + public override StandardValuesCollection GetStandardValues(ITypeDescriptorContext context) + { + if (base.Values == null) + { + base.GetStandardValues(context); + ArrayList arrayList = new(base.Values); + arrayList.Remove(GraphicsUnit.Display); + base.Values = new StandardValuesCollection(arrayList); + } + + return base.Values; + } + } + + private FontNameConverter _fontNameConverter; + + /// + /// Determines whether this converter can convert an object in the specified source + /// type to the native type of the converter. + /// + /// + /// A formatter context. This object can be used to get additional information about + /// the environment this converter is being called from. This may be null, so you + /// should always check. Also, properties on the context object may also return null. + /// + /// The type you want to convert from + /// This method returns true if this object can perform the conversion. + public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) + { + return sourceType == typeof(string) || base.CanConvertFrom(context, sourceType); + } + + /// + /// Gets a value indicating whether this converter can convert an object to the given + /// destination type using the context. + /// + /// An object that provides a format context. + /// A object that represents the type you want to convert to. + /// This method returns true if this converter can perform the conversion; otherwise, false. + public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) + { + return destinationType == typeof(string) || base.CanConvertTo(context, destinationType); + } + + /// + /// Converts the specified object to the native type of the converter. + /// + /// + /// A formatter context. This object can be used to get additional information about + /// the environment this converter is being called from. This may be null, so you + /// should always check. Also, properties on the context object may also return null. + /// + /// A object that specifies the culture used to represent the font. + /// The object to convert. + /// The converted object. + /// The conversion could not be performed. + public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) + { + if (value is not string text) + return base.ConvertFrom(context, culture, value); + + string trimmedText = text.Trim(); + if (trimmedText.Length == 0) + return null; + + culture ??= CultureInfo.CurrentCulture; + + char separator = culture.TextInfo.ListSeparator[0]; + string familyName = trimmedText; + float emSize = 8.25f; + FontStyle fontStyle = FontStyle.Regular; + GraphicsUnit unit = GraphicsUnit.Point; + + int separatorIndex = trimmedText.IndexOf(separator); + if (separatorIndex > 0) + { + familyName = trimmedText.Substring(0, separatorIndex); + if (separatorIndex < trimmedText.Length - 1) + { + string sizeAndUnit; + string style = null; + + int styleIndex = trimmedText.IndexOf("style="); + if (styleIndex != -1) + { + style = trimmedText.Substring(styleIndex, trimmedText.Length - styleIndex); + if (!style.StartsWith("style=")) + throw GetFormatException(trimmedText, separator); + + sizeAndUnit = trimmedText.Substring(separatorIndex + 1, styleIndex - separatorIndex - 1); + } + else + { + sizeAndUnit = trimmedText.Substring(separatorIndex + 1, trimmedText.Length - separatorIndex - 1); + } + + ParseSizeTokens(sizeAndUnit, separator, out string sizeText, out string unitText); + if (sizeText != null) + { + try + { + emSize = (float)TypeDescriptor.GetConverter(typeof(float)).ConvertFromString(context, culture, sizeText); + } + catch + { + throw GetFormatException(trimmedText, separator); + } + } + + if (unitText != null) + { + unit = ParseGraphicsUnits(unitText); + } + + if (style != null) + { + int indexEqual = style.IndexOf("="); + style = style.Substring(indexEqual + 1, style.Length - "style=".Length); + foreach (string stylePart in style.Split(separator)) + { + try + { + fontStyle |= (FontStyle)Enum.Parse(typeof(FontStyle), stylePart.Trim(), ignoreCase: true); + } + catch (Exception ex) + { + if (ex is InvalidEnumArgumentException) + throw; + + throw GetFormatException(trimmedText, separator); + } + + const FontStyle validFontStyle = FontStyle.Bold | FontStyle.Italic | FontStyle.Underline | FontStyle.Strikeout; + if ((fontStyle | validFontStyle) != validFontStyle) + { + throw new InvalidEnumArgumentException("style", (int)fontStyle, typeof(FontStyle)); + } + } + } + } + } + + _fontNameConverter ??= new FontNameConverter(); + familyName = (string)_fontNameConverter.ConvertFrom(context, culture, familyName); + return new Font(familyName, emSize, fontStyle, unit); + } + + /// + /// Converts the specified object to another type. + /// + /// + /// A formatter context. This object can be used to get additional information about + /// the environment this converter is being called from. This may be null, so you + /// should always check. Also, properties on the context object may also return null. + /// + /// + /// A object that specifies the culture used to represent the object. + /// + /// The object to convert. + /// The data type to convert the object to. + /// The converted object. + /// The conversion was not successful. + public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType) + { + if (destinationType == null) + throw new ArgumentNullException(nameof(destinationType)); + + if (destinationType == typeof(string)) + { + if (value is not Font font) + return string.Empty; + + culture ??= CultureInfo.CurrentCulture; + + string separator = culture.TextInfo.ListSeparator + " "; + int parts = 2; + if (font.Style != 0) + parts++; + + string[] array = new string[parts]; + array[0] = font.Name; + array[1] = TypeDescriptor.GetConverter(font.Size).ConvertToString(context, culture, font.Size) + GetGraphicsUnitText(font.Unit); + if (font.Style != 0) + array[2] = "style=" + font.Style.ToString("G"); + + return string.Join(separator, array); + } + + return base.ConvertTo(context, culture, value, destinationType); + } + + /// + /// Creates an object of this type by using a specified set of property values for the object. + /// + /// A type descriptor through which additional context can be provided. + /// + /// A dictionary of new property values. The dictionary contains a series of name-value + /// pairs, one for each property returned from the + /// method. + /// + /// + /// The newly created object, or null if the object could not be created. The default + /// implementation returns null. Useful for creating non-changeable objects that have changeable properties. + /// + public override object CreateInstance(ITypeDescriptorContext context, IDictionary propertyValues) + { + if (propertyValues == null) + throw new ArgumentNullException(nameof(propertyValues)); + + object familyObj = propertyValues["Name"] ?? "Tahoma"; + object sizeObj = propertyValues["Size"] ?? 8f; + object unitObj = propertyValues["Unit"] ?? GraphicsUnit.Point; + object isBoldObj = propertyValues["Bold"] ?? false; + object isItalicObj = propertyValues["Italic"] ?? false; + object isStrikeoutObj = propertyValues["Strikeout"] ?? false; + object isUnderlineObj = propertyValues["Underline"] ?? false; + if (familyObj is not string family + || sizeObj is not float size + || unitObj is not GraphicsUnit unit + || isBoldObj is not bool isBold + || isItalicObj is not bool isItalic + || isStrikeoutObj is not bool isStrikeout + || isUnderlineObj is not bool isUnderline) + { + throw new ArgumentException("Property value with incorrect type"); + } + + FontStyle style = FontStyle.Regular; + if (isBold) + style |= FontStyle.Bold; + + if (isItalic) + style |= FontStyle.Italic; + + if (isStrikeout) + style |= FontStyle.Strikeout; + + if (isUnderline) + style |= FontStyle.Underline; + + return new Font(family, size, style, unit); + } + + /// + /// Determines whether changing a value on this object should require a call to the + /// method to create a new value. + /// + /// A type descriptor through which additional context can be provided. + /// + /// This method returns true if the CreateInstance object should be called when a + /// change is made to one or more properties of this object; otherwise, false. + /// + public override bool GetCreateInstanceSupported(ITypeDescriptorContext context) + { + return true; + } + + private static ArgumentException GetFormatException(string text, char separator) + { + string validFormat = string.Format(CultureInfo.CurrentCulture, "name{0} size[units[{0} style=style1[{0} style2{0} ...]]]", separator); + return new ArgumentException($"Failed to parse font text, input: {text}, valid format: {validFormat}"); + } + + private static string GetGraphicsUnitText(GraphicsUnit units) + { + return UnitName.Names.FirstOrDefault(n => n.m_unit == units)?.m_name ?? ""; + } + + /// + /// Retrieves the set of properties for this type. By default, a type does not have + /// any properties to return. + /// + /// A type descriptor through which additional context can be provided. + /// The value of the object to get the properties for. + /// An array of objects that describe the properties. + /// + /// The set of properties that should be exposed for this data type. If no properties + /// should be exposed, this may return null. The default implementation always returns + /// null. An easy implementation of this method can call the + /// method for the correct data type. + /// + public override PropertyDescriptorCollection GetProperties(ITypeDescriptorContext context, object value, Attribute[] attributes) + { + PropertyDescriptorCollection properties = TypeDescriptor.GetProperties(typeof(Font), attributes); + return properties.Sort(new[] { "Name", "Size", "Unit", "Weight" }); + } + + /// + /// Determines whether this object supports properties. The default is false. + /// + /// A type descriptor through which additional context can be provided. + /// + /// This method returns true if the System.Drawing.FontConverter.GetPropertiesSupported(System.ComponentModel.ITypeDescriptorContext) + /// method should be called to find the properties of this object; otherwise, false. + /// + public override bool GetPropertiesSupported(ITypeDescriptorContext context) + { + return true; + } + + private static void ParseSizeTokens(string text, char separator, out string size, out string unit) + { + size = null; + unit = null; + text = text.Trim(); + int length = text.Length; + if (length <= 0) + return; + + int index = 0; + while (index < length && !char.IsLetter(text[index])) + index++; + + char[] trimChars = { separator, ' ' }; + if (index > 0) + { + size = text.Substring(0, index); + size = size.Trim(trimChars); + } + + if (index < length) + { + unit = text.Substring(index); + unit = unit.TrimEnd(trimChars); + } + } + + private static GraphicsUnit ParseGraphicsUnits(string units) + { + UnitName unitName = UnitName.Names.FirstOrDefault(un => string.Equals(un.m_name, units, StringComparison.OrdinalIgnoreCase)); + if (unitName == null) + throw new ArgumentException(nameof(units)); + return unitName.m_unit; + } +} diff --git a/src/Common/FontFamily.cs b/src/Common/FontFamily.cs index 4d18dbf..47e7710 100644 --- a/src/Common/FontFamily.cs +++ b/src/Common/FontFamily.cs @@ -7,59 +7,179 @@ namespace GeneXus.Drawing; -public class FontFamily : IDisposable +public abstract class FontFamily : IDisposable, ICloneable { - internal readonly int m_index; - internal readonly SKData m_data; - internal readonly SKTypeface m_typeface; + /// + /// Creates a human-readable string that represents this . + /// + public override string ToString() => $"[{GetType().Name}: Name={Name}]"; + + public virtual void Dispose() { } + + #region Properties - internal FontFamily(SKData skData, int index) - { - m_index = index; - m_data = skData; - m_typeface = SKTypeface.FromData(m_data, m_index); - if (m_typeface == null) throw new ArgumentException("file does not exist or is an invalid font file"); - } + /// + /// Gets the name of this . + /// + public abstract string Name { get; } /// - /// Initializes a new instance of the class from the specified filename name - /// and optional index for font collection. + /// Gets the Face associated with this . /// - public FontFamily(string name, int index = 0) - : this(File.Exists(name) ? SKData.Create(name) : new Font(name).FontFamily.m_data, index) { } + public abstract string Face { get; } /// - /// Initializes a new instance of the class from the specified stream name - /// and optional index for font collection. + /// Gets a value that indicates whether this has the italic style applied. /// - public FontFamily(Stream stream, int index = 0) - : this(SKData.Create(stream), index) { } + public abstract bool IsItalic { get; } /// - /// Initializes a new instance of the class in the specified - /// and with the specified name. + /// Gets a value that indicates whether this is bold. /// - public FontFamily(string name, FontCollection fontCollection) - : this(fontCollection.Families.FirstOrDefault(ff => ff.MatchFamily(name))?.m_data - ?? throw new ArgumentException($"missing family from collection", nameof(name)), 0) - { } + public abstract bool IsBold { get; } + + /// + /// Gets the weight of this . + /// + public abstract int Weight { get; } + + /// + /// Gets the width of this . + /// + public abstract int Width { get; } + + /// + /// Gets the slant of this . + /// + public abstract SlantType Slant { get; } + + #endregion + + #region Methods /// - /// Initializes a new instance of the class from the specified generic font family. + /// Returns the cell ascent of this . /// - public FontFamily(GenericFontFamilies genericFamily) - : this(GetGenericFontFamily(genericFamily).m_data, 0) { } + public abstract int GetCellAscent(); /// - /// Cleans up resources for this . + /// Returns the cell descent of this . /// - ~FontFamily() => Dispose(false); + public abstract int GetCellDescent(); /// - /// Creates a human-readable string that represents this . + /// Gets the height, of the em square of this . + /// + public abstract int GetEmHeight(); + + /// + /// Returns the distance between two consecutive lines of text for this + /// with the specified . + /// + public abstract int GetLineSpacing(); + + /// + /// Returns the name of this in the specified language. + /// + public abstract string GetName(int language); + + /// + /// Indicates whether the specified is available. + /// + public abstract bool IsStyleAvailable(FontStyle style); + + /// + /// Creates an exact copy of this . + /// + public abstract object Clone(); + + #endregion + + #region Generic Font Families + + /// + /// Returns an array that contains all of the objects associated with the current + /// graphics context. + /// + public static FontFamily[] Families => new InstalledFontCollection().Families; + + /// + /// Gets a generic monospace . + /// + public static FontFamily GenericMonospace => new SkiaFontFamily(GenericFontFamilies.Monospace); + + /// + /// Gets a generic SansSerif . + /// + public static FontFamily GenericSansSerif => new SkiaFontFamily(GenericFontFamilies.SansSerif); + + /// + /// Gets a generic Serif . + /// + public static FontFamily GenericSerif => new SkiaFontFamily(GenericFontFamilies.Serif); + + #endregion +} + +public static class FontFamilyFactory +{ + /// + /// Initializes a new instance of the class from the specified filename name + /// and optional index for font collection. + /// + public static FontFamily Create(string fontPath, int index = 0) => new SkiaFontFamily(fontPath, index); + + /// + /// Initializes a new instance of the class from the specified stream name + /// and optional index for font collection. + /// + public static FontFamily Create(Stream stream, int index = 0) => new SkiaFontFamily(stream, index); + + /// + /// Initializes a new instance of the class in the specified + /// and with the specified name. + /// + public static FontFamily Create(string name, FontCollection fontCollection) => new SkiaFontFamily(name, fontCollection); + + /// + /// Initializes a new instance of the class from the specified generic font family. /// - public override string ToString() => $"[{GetType().Name}: Name={m_typeface.FamilyName}]"; + public static FontFamily Create(GenericFontFamilies genericFamily) => new SkiaFontFamily(genericFamily); +} +internal sealed class SkiaFontFamily : FontFamily +{ + internal readonly int m_index; + private readonly SKData m_data; + private readonly SKTypeface m_typeface; + + private SkiaFontFamily(SKData skData, int index) + { + m_index = index; + m_data = skData; + m_typeface = SKTypeface.FromData(m_data, m_index); + if (m_typeface == null) throw new ArgumentException("file does not exist or is an invalid font file"); + } + + public SkiaFontFamily(string fontPath, int index = 0) + : this(File.Exists(fontPath) ? SKData.Create(fontPath) : throw new FileNotFoundException(fontPath), index) { } + + public SkiaFontFamily(Stream stream, int index = 0) + : this(SKData.Create(stream), index) { } + + public SkiaFontFamily(string name, FontCollection fontCollection) + : this((fontCollection.Families.FirstOrDefault(ff => ff is SkiaFontFamily sff && sff.MatchFamily(name)) as SkiaFontFamily)?.m_data + ?? throw new ArgumentException($"missing family from collection", nameof(name)), 0) + { } + + public SkiaFontFamily(GenericFontFamilies genericFamily) + : this(GetGenericFontFamily(genericFamily).m_data, 0) { } + + /// + /// Cleans up resources for this . + /// + ~SkiaFontFamily() => Dispose(false); + #region IEqualitable @@ -67,12 +187,12 @@ public FontFamily(GenericFontFamilies genericFamily) /// Indicates whether the specified object is a and is identical to this . /// public override bool Equals(object obj) - => obj is FontFamily ff + => obj is SkiaFontFamily ff && ff.m_typeface.FamilyName.Equals(m_typeface.FamilyName, StringComparison.OrdinalIgnoreCase) && ff.m_index == m_index; /// - /// Gets a hash code for this . + /// Gets a hash code for this . /// public override int GetHashCode() => Name.GetHashCode(); @@ -82,15 +202,15 @@ public override bool Equals(object obj) #region IDisposable /// - /// Cleans up resources for this . + /// Cleans up resources for this . /// - public void Dispose() + public override void Dispose() { GC.SuppressFinalize(this); Dispose(true); } - protected virtual void Dispose(bool disposing) + private void Dispose(bool disposing) { m_typeface.Dispose(); m_data.Dispose(); @@ -102,9 +222,9 @@ protected virtual void Dispose(bool disposing) #region Operators /// - /// Creates a with the coordinates of the specified . + /// Creates a with the coordinates of the specified . /// - public static explicit operator SKTypeface(FontFamily fontFamily) => fontFamily.m_typeface; + public static explicit operator SKTypeface(SkiaFontFamily fontFamily) => fontFamily.m_typeface; #endregion @@ -114,91 +234,51 @@ protected virtual void Dispose(bool disposing) /// /// Gets the name of this . /// - public string Name => m_typeface.FamilyName; - - /// - /// Returns an array that contains all of the objects associated with the current - /// graphics context. - /// - public static FontFamily[] Families => new InstalledFontCollection().Families; - - /// - /// Gets a generic monospace . - /// - public static FontFamily GenericMonospace => new(GenericFontFamilies.Monospace); - - /// - /// Gets a generic SansSerif . - /// - public static FontFamily GenericSansSerif => new(GenericFontFamilies.SansSerif); - - /// - /// Gets a generic Serif . - /// - public static FontFamily GenericSerif => new(GenericFontFamilies.Serif); + public override string Name => m_typeface.FamilyName; #endregion #region Methods - internal SKFont m_font => GetFont(10); + private SKFont m_font => GetFont(10); internal SKFont GetFont(float size) => m_typeface.ToFont(size); - /// - /// Returns the cell ascent of this . - /// - public int GetCellAscent() => (int)Math.Abs(m_font.Metrics.Ascent * GetEmHeight() / m_font.Size); - - /// - /// Returns the cell descent of this . - /// - public int GetCellDescent() => (int)Math.Abs(m_font.Metrics.Descent * GetEmHeight() / m_font.Size); - - /// - /// Gets the height, of the em square of this . - /// - public int GetEmHeight() => m_typeface.UnitsPerEm; - - /// Returns the distance between two consecutive lines of text for this with the - /// specified . - /// - public int GetLineSpacing() => (int)Math.Abs(m_font.Spacing * GetEmHeight() / m_font.Size); - - /// - /// Returns the name of this in the specified language. - /// - public string GetName(int language) => Name; // NOTE: Language is not suppored in SkiaSharp - - /// - /// Indicates whether the specified is available. - /// - public bool IsStyleAvailable(FontStyle style) => (new Font(this).Style & style) == style; + public override int GetCellAscent() => (int)Math.Abs(m_font.Metrics.Ascent * GetEmHeight() / m_font.Size); + public override int GetCellDescent() => (int)Math.Abs(m_font.Metrics.Descent * GetEmHeight() / m_font.Size); + public override int GetEmHeight() => m_typeface.UnitsPerEm; + public override int GetLineSpacing() => (int)Math.Abs(m_font.Spacing * GetEmHeight() / m_font.Size); + public override string GetName(int language) => Name; // NOTE: Language is not supported in SkiaSharp + public override bool IsStyleAvailable(FontStyle style) => (new Font(this).Style & style) == style; + public override object Clone() => new SkiaFontFamily(SKData.CreateCopy(m_data.ToArray()), m_index); #endregion #region Utilities - internal int Weight => m_typeface.FontWeight; + public override bool IsItalic => m_typeface.IsItalic; + public override bool IsBold => m_typeface.IsBold; + + public override int Weight => m_typeface.FontWeight; private string WeightName => Weight switch { - int n when n < 100 => "Extra Thin", - int n when n < 275 => "Thin", - int n when n < 300 => "Extra Light", - int n when n < 350 => "Light", - int n when n < 400 => "Semi Light", - int n when n < 500 => "Normal", - int n when n < 600 => "Medium", - int n when n < 700 => "Semi Bold", - int n when n < 800 => "Bold", - int n when n < 900 => "Extra Bold", - int n when n < 950 => "Black", - int n when n < 999 => "Extra Black", + < 100 => "Extra Thin", + < 275 => "Thin", + < 300 => "Extra Light", + < 350 => "Light", + < 400 => "Semi Light", + < 500 => "Normal", + < 600 => "Medium", + < 700 => "Semi Bold", + < 800 => "Bold", + < 900 => "Extra Bold", + < 950 => "Black", + < 999 => "Extra Black", _ => string.Empty }; - internal int Width => m_typeface.FontWidth; + public override int Width => m_typeface.FontWidth; private string WidthName => Width switch { 1 => "Ultra Condensed", @@ -213,7 +293,7 @@ protected virtual void Dispose(bool disposing) _ => string.Empty }; - internal SlantType Slant => m_typeface.FontSlant switch + public override SlantType Slant => m_typeface.FontSlant switch { SKFontStyleSlant.Oblique => SlantType.Oblique, SKFontStyleSlant.Italic => SlantType.Italic, @@ -228,7 +308,7 @@ protected virtual void Dispose(bool disposing) _ => string.Empty }; - internal string Face + public override string Face { get { @@ -247,8 +327,8 @@ void AppendStyle(string value) return faceName.Append(faceName.Length > 0 ? string.Empty : "Regular").ToString(); } } - - private static FontFamily GetGenericFontFamily(GenericFontFamilies genericFamily) + + private static SkiaFontFamily GetGenericFontFamily(GenericFontFamilies genericFamily) { var candidates = genericFamily switch // NOTE: Define a set of predefined fonts { @@ -257,15 +337,34 @@ private static FontFamily GetGenericFontFamily(GenericFontFamilies genericFamily GenericFontFamilies.Serif => new[] { "Times New Roman", "Georgia", "Garamond", "Palatino", "Book Antiqua", "Baskerville" }, _ => throw new ArgumentException($"invalid generic font value {genericFamily}", nameof(genericFamily)) }; - foreach (var candidate in candidates) + foreach (string candidate in candidates) if (Font.SystemFonts.FirstOrDefault(f => f.FamilyName.Equals(candidate, StringComparison.OrdinalIgnoreCase)) is Font font) - return font.FontFamily; + return font.FontFamily as SkiaFontFamily; throw new ArgumentException($"invalid generic font family", nameof(genericFamily)); } internal bool MatchFamily(string familyName) // TODO: Improve this code - => new string[] { Name, $"{Name} {Face}", $"{Name}-{Face}" }.Any(candidateName + => new[] { Name, $"{Name} {Face}", $"{Name}-{Face}" }.Any(candidateName => candidateName.Equals(familyName, StringComparison.OrdinalIgnoreCase)); #endregion } + +internal sealed class UnknownFontFamily : FontFamily +{ + public UnknownFontFamily(string name) { Name = name; } + public override string Name { get; } + public override string Face => string.Empty; + public override bool IsItalic => false; + public override bool IsBold => false; + public override int Weight => 0; + public override int Width => 0; + public override SlantType Slant => SlantType.Normal; + public override int GetCellAscent() => 0; + public override int GetCellDescent() => 0; + public override int GetEmHeight() => 0; + public override int GetLineSpacing() => 0; + public override string GetName(int language) => Name; + public override bool IsStyleAvailable(FontStyle style) => false; + public override object Clone() => new UnknownFontFamily(Name); +} diff --git a/src/Common/GraphicsUnit.cs b/src/Common/GraphicsUnit.cs new file mode 100644 index 0000000..f22a2de --- /dev/null +++ b/src/Common/GraphicsUnit.cs @@ -0,0 +1,43 @@ +namespace GeneXus.Drawing; + +/// +/// Specifies the unit of measure for the given data +/// +public enum GraphicsUnit +{ + /// + /// Specifies the world coordinate system unit as the unit of measure. + /// + World, + + /// + /// Specifies the unit of measure of the display device. Typically pixels for video + /// displays, and 1/100 inch for printers. + /// + Display, + + /// + /// Specifies a device pixel as the unit of measure. + /// + Pixel, + + /// + /// Specifies a printer's point (1/72 inch) as the unit of measure. + /// + Point, + + /// + /// Specifies the inch as the unit of measure. + /// + Inch, + + /// + /// Specifies the document unit (1/300 inch) as the unit of measure. + /// + Document, + + /// + /// Specifies the millimeter as the unit of measure. + /// + Millimeter +} diff --git a/src/Common/Point.cs b/src/Common/Point.cs index 9dd4ba9..05bd35c 100644 --- a/src/Common/Point.cs +++ b/src/Common/Point.cs @@ -16,7 +16,7 @@ private Point(SKPoint point) /// /// Initializes a new instance of the class with the specified coordinates. /// - public Point(float x, float y) + public Point(int x, int y) : this(new SKPoint(x, y)) { } /// @@ -113,18 +113,18 @@ public Point(int dw) /// /// Gets the x-coordinate of this . /// - public float X + public int X { - readonly get => m_point.X; + readonly get => (int)m_point.X; set => m_point.X = value; } /// /// Gets the y-coordinate of this . /// - public float Y + public int Y { - readonly get => m_point.Y; + readonly get => (int)m_point.Y; set => m_point.Y = value; } @@ -148,25 +148,10 @@ public float Y /// public static Point Subtract(Point pt, Size sz) => new(pt.m_point - sz.m_size); - /// - /// Converts a by performing a ceiling operation on all the coordinates. - /// - public static Point Ceiling(Point value) => new(unchecked((int)Math.Ceiling(value.X)), unchecked((int)Math.Ceiling(value.Y))); - - /// - /// Converts a by performing a truncate operation on all the coordinates. - /// - public static Point Truncate(Point value) => new(unchecked((int)value.X), unchecked((int)value.Y)); - - /// - /// Converts a by performing a round operation on all the coordinates. - /// - public static Point Round(Point value) => new(unchecked((int)Math.Round(value.X)), unchecked((int)Math.Round(value.Y))); - /// /// Translates this by the specified amount. /// - public void Offset(float dx, float dy) => m_point.Offset(dx, dy); + public void Offset(int dx, int dy) => m_point.Offset(dx, dy); /// /// Translates this by the specified amount. diff --git a/src/Common/PointF.cs b/src/Common/PointF.cs new file mode 100644 index 0000000..b009356 --- /dev/null +++ b/src/Common/PointF.cs @@ -0,0 +1,177 @@ +using System; +using SkiaSharp; + +namespace GeneXus.Drawing; + +[Serializable] +public struct PointF : IEquatable +{ + internal SKPoint m_point; + + private PointF(SKPoint point) + { + m_point = point; + } + + /// + /// Initializes a new instance of the class with the specified coordinates. + /// + public PointF(float x, float y) + : this(new SKPoint(x, y)) { } + + /// + /// Initializes a new instance of the class from a . + /// + public PointF(SizeF sz) + : this(sz.Width, sz.Height) { } + + /// + /// Initializes a new instance of the class using coordinates specified by an integer value. + /// + public PointF(int dw) + : this(unchecked((short)((dw >> 0) & 0xFFFF)), unchecked((short)((dw >> 16) & 0xFFFF))) { } + + /// + /// Creates a human-readable string that represents this . + /// + public override readonly string ToString() => $"{{X={X},Y={Y}}}"; + + + #region Operators + + /// + /// Creates a with the coordinates of the specified . + /// + public static explicit operator SKPoint(PointF point) => point.m_point; + + /// + /// Creates a with the coordinates of the specified . + /// + public static explicit operator SizeF(PointF p) => new(p.X, p.Y); + + /// + /// Compares two objects. The result specifies whether the values of the + /// and properties of the two + /// objects are equal. + /// + public static bool operator ==(PointF left, PointF right) => left.m_point == right.m_point; + + /// + /// Compares two objects. The result specifies whether the values of the + /// or properties of the two + /// objects are unequal. + /// + public static bool operator !=(PointF left, PointF right) => left.m_point != right.m_point; + + /// + /// Translates a by a given . + /// + public static PointF operator +(PointF pt, SizeF sz) => Add(pt, sz); + + /// + /// Translates a by the negative of a given . + /// + public static PointF operator -(PointF pt, SizeF sz) => Subtract(pt, sz); + + #endregion + + + #region IEquatable + + /// + /// Tests whether a has the same coordinates + /// as this Point. + /// + public readonly bool Equals(PointF other) => m_point == other.m_point; + + /// + /// Tests whether is a with the same coordinates + /// as this Point. + /// + public override readonly bool Equals(object obj) => m_point.Equals(obj); + + /// + /// Returns a hash code. + /// + public override readonly int GetHashCode() => m_point.GetHashCode(); + + #endregion + + + #region Fields + + /// + /// Creates a new instance of the class with member data left uninitialized. + /// + public static readonly PointF Empty = default; + + #endregion + + + #region Properties + + /// + /// Gets the x-coordinate of this . + /// + public float X + { + readonly get => m_point.X; + set => m_point.X = value; + } + + /// + /// Gets the y-coordinate of this . + /// + public float Y + { + readonly get => m_point.Y; + set => m_point.Y = value; + } + + /// + /// Gets a value indicating whether this is empty. + /// + public readonly bool IsEmpty => m_point.IsEmpty; + + #endregion + + + #region Methods + + /// + /// Translates a by a given . + /// + public static PointF Add(PointF pt, SizeF sz) => new(pt.m_point + sz.m_size); + + /// + /// Translates a by the negative of a given . + /// + public static PointF Subtract(PointF pt, SizeF sz) => new(pt.m_point - sz.m_size); + + /// + /// Converts a by performing a ceiling operation on all the coordinates. + /// + public static PointF Ceiling(PointF value) => new(unchecked((int)Math.Ceiling(value.X)), unchecked((int)Math.Ceiling(value.Y))); + + /// + /// Converts a by performing a truncate operation on all the coordinates. + /// + public static PointF Truncate(PointF value) => new(unchecked((int)value.X), unchecked((int)value.Y)); + + /// + /// Converts a by performing a round operation on all the coordinates. + /// + public static PointF Round(PointF value) => new(unchecked((int)Math.Round(value.X)), unchecked((int)Math.Round(value.Y))); + + /// + /// Translates this by the specified amount. + /// + public void Offset(float dx, float dy) => m_point.Offset(dx, dy); + + /// + /// Translates this by the specified amount. + /// + public void Offset(PointF p) => Offset(p.X, p.Y); + + #endregion +} diff --git a/src/Common/Rectangle.cs b/src/Common/Rectangle.cs index cc447ab..6ae6bd9 100644 --- a/src/Common/Rectangle.cs +++ b/src/Common/Rectangle.cs @@ -17,7 +17,7 @@ internal Rectangle(SKRect rect) /// Initializes a new instance of the struct with the /// specified location and size. /// - public Rectangle(float x, float y, float width, float height) + public Rectangle(int x, int y, int width, int height) : this(new SKRect(x, y, width + x, height + y)) { } /// @@ -29,13 +29,13 @@ public Rectangle(Point location, Size size) /// /// Initializes a new instance of the struct with the specified location (x, y) and size. /// - public Rectangle(float x, float y, Size size) + public Rectangle(int x, int y, Size size) : this(x, y, size.Width, size.Height) { } /// /// Initializes a new instance of the struct with the specified location and size (width, height). /// - public Rectangle(Point location, float width, float height) + public Rectangle(Point location, int width, int height) : this(location.X, location.Y, width, height) { } /// @@ -99,41 +99,41 @@ public Rectangle(Point location, float width, float height) /// Gets the x-coordinate of the upper-left corner of the rectangular region defined by this /// . /// - public readonly float Left => m_rect.Left; + public readonly int Left => (int)m_rect.Left; /// /// Gets the x-coordinate of the lower-right corner of the rectangular region defined by this /// . /// - public readonly float Right => m_rect.Right; + public readonly int Right => (int)m_rect.Right; /// /// Gets the y-coordinate of the upper-left corner of the rectangular region defined by this /// . /// - public readonly float Top => m_rect.Top; + public readonly int Top => (int)m_rect.Top; /// /// Gets the y-coordinate of the lower-right corner of the rectangular region defined by this /// . /// - public readonly float Bottom => m_rect.Bottom; + public readonly int Bottom => (int)m_rect.Bottom; /// /// Gets or sets the width of the rectangular region defined by this . /// - public float Width + public int Width { - readonly get => m_rect.Width; + readonly get => (int)m_rect.Width; set => m_rect.Right = m_rect.Left + value; } /// /// Gets or sets the width of the rectangular region defined by this . /// - public float Height + public int Height { - readonly get => m_rect.Height; + readonly get => (int)m_rect.Height; set => m_rect.Bottom = m_rect.Top + value; } @@ -141,7 +141,7 @@ public float Height /// Gets or sets the x-coordinate of the upper-left corner of the rectangular region defined by this /// . /// - public float X + public int X { readonly get => Left; set @@ -155,7 +155,7 @@ public float X /// Gets or sets the y-coordinate of the upper-left corner of the rectangular region defined by this /// . /// - public float Y + public int Y { readonly get => Top; set @@ -170,7 +170,7 @@ public float Y /// public Size Size { - get => new Size(Width, Height); + get => new(Width, Height); set => (Width, Height) = (value.Width, value.Height); } @@ -180,7 +180,7 @@ public Size Size /// public Point Location { - get => new Point(X, Y); + get => new(X, Y); set => (X, Y) = (value.X, value.Y); } @@ -198,7 +198,7 @@ public Point Location /// /// Creates a new with the specified location and size. /// - public static Rectangle FromLTRB(float left, float top, float right, float bottom) => new(left, top, unchecked(right - left), unchecked(bottom - top)); + public static Rectangle FromLTRB(int left, int top, int right, int bottom) => new(left, top, unchecked(right - left), unchecked(bottom - top)); #endregion @@ -215,7 +215,7 @@ public Point Location /// Determines if the specified point is contained within the rectangular region defined by this /// . /// - public readonly bool Contains(float x, float y) => m_rect.Contains(x, y); + public readonly bool Contains(int x, int y) => m_rect.Contains(x, y); /// /// Determines if the specified point is contained within the rectangular region defined by this @@ -260,7 +260,7 @@ public static Rectangle Union(Rectangle a, Rectangle b) /// /// Inflates this by the specified amount. /// - public void Inflate(float width, float height) => m_rect.Inflate(width, height); + public void Inflate(int width, int height) => m_rect.Inflate(width, height); /// /// Inflates this by the specified amount. @@ -270,7 +270,7 @@ public static Rectangle Union(Rectangle a, Rectangle b) /// /// Creates a that is inflated by the specified amount. /// - public static Rectangle Inflate(Rectangle rect, float x, float y) + public static Rectangle Inflate(Rectangle rect, int x, int y) { var ret = SKRect.Inflate(rect.m_rect, x, y); return new Rectangle(ret); @@ -279,7 +279,7 @@ public static Rectangle Inflate(Rectangle rect, float x, float y) /// /// Adjusts the location of this by the specified amount. /// - public void Offset(float x, float y) => m_rect.Offset(x, y); + public void Offset(int x, int y) => m_rect.Offset(x, y); /// /// Adjusts the location of this by the specified amount. diff --git a/src/Common/RectangleF.cs b/src/Common/RectangleF.cs new file mode 100644 index 0000000..56c5ff2 --- /dev/null +++ b/src/Common/RectangleF.cs @@ -0,0 +1,290 @@ +using System; +using SkiaSharp; + +namespace GeneXus.Drawing; + +[Serializable] +public struct RectangleF : IEquatable +{ + internal SKRect m_rect; + + internal RectangleF(SKRect rect) + { + m_rect = rect; + } + + /// + /// Initializes a new instance of the struct with the + /// specified location and size. + /// + public RectangleF(float x, float y, float width, float height) + : this(new SKRect(x, y, width + x, height + y)) { } + + /// + /// Initializes a new instance of the struct with the specified location and size. + /// + public RectangleF(PointF location, SizeF size) + : this(location.X, location.Y, size.Width, size.Height) { } + + /// + /// Initializes a new instance of the struct with the specified location (x, y) and size. + /// + public RectangleF(float x, float y, SizeF size) + : this(x, y, size.Width, size.Height) { } + + /// + /// Initializes a new instance of the struct with the specified location and size (width, height). + /// + public RectangleF(PointF location, float width, float height) + : this(location.X, location.Y, width, height) { } + + /// + /// Creates a human-readable string that represents this . + /// + public override readonly string ToString() => $"{{X={X},Y={Y},Width={Width},Height={Height}}}"; + + + #region Operators + + public static explicit operator SKRect(RectangleF rect) => rect.m_rect; + + /// + /// Tests whether two objects have equal location and size. + /// + public static bool operator ==(RectangleF left, RectangleF right) => left.m_rect == right.m_rect; + + /// + /// Tests whether two objects differ in location or size. + /// + public static bool operator !=(RectangleF left, RectangleF right) => left.m_rect != right.m_rect; + + #endregion + + + #region IEquatable + + /// + /// Tests whether a has the same location + /// and size of this Rectangle. + /// + public readonly bool Equals(RectangleF other) => m_rect == other.m_rect; + + /// + /// Tests whether is a with the same location + /// and size as this Rectangle. + /// + public override readonly bool Equals(object obj) => m_rect.Equals(obj); + + /// + /// Returns a hash code. + /// + public override readonly int GetHashCode() => m_rect.GetHashCode(); + + #endregion + + + #region Fields + + /// + /// Initializes a new instance of the struct. + /// + public static readonly RectangleF Empty = default; + + #endregion + + + #region Properties + + /// + /// Gets the x-coordinate of the upper-left corner of the rectangular region defined by this + /// . + /// + public readonly float Left => m_rect.Left; + + /// + /// Gets the x-coordinate of the lower-right corner of the rectangular region defined by this + /// . + /// + public readonly float Right => m_rect.Right; + + /// + /// Gets the y-coordinate of the upper-left corner of the rectangular region defined by this + /// . + /// + public readonly float Top => m_rect.Top; + + /// + /// Gets the y-coordinate of the lower-right corner of the rectangular region defined by this + /// . + /// + public readonly float Bottom => m_rect.Bottom; + + /// + /// Gets or sets the width of the rectangular region defined by this . + /// + public float Width + { + readonly get => m_rect.Width; + set => m_rect.Right = m_rect.Left + value; + } + + /// + /// Gets or sets the width of the rectangular region defined by this . + /// + public float Height + { + readonly get => m_rect.Height; + set => m_rect.Bottom = m_rect.Top + value; + } + + /// + /// Gets or sets the x-coordinate of the upper-left corner of the rectangular region defined by this + /// . + /// + public float X + { + readonly get => Left; + set + { + m_rect.Right += value - m_rect.Left; + m_rect.Left = value; + } + } + + /// + /// Gets or sets the y-coordinate of the upper-left corner of the rectangular region defined by this + /// . + /// + public float Y + { + readonly get => Top; + set + { + m_rect.Bottom += value - m_rect.Top; + m_rect.Top = value; + } + } + + /// + /// Gets or sets the size of this . + /// + public SizeF Size + { + get => new(Width, Height); + set => (Width, Height) = (value.Width, value.Height); + } + + /// + /// Gets or sets the coordinates of the upper-left corner of the rectangular region represented by this + /// . + /// + public PointF Location + { + get => new(X, Y); + set => (X, Y) = (value.X, value.Y); + } + + /// + /// Tests whether this has a + /// or a of 0. + /// + public readonly bool IsEmpty => m_rect.IsEmpty; + + #endregion + + + #region Factory + + /// + /// Creates a new with the specified location and size. + /// + public static RectangleF FromLTRB(float left, float top, float right, float bottom) => new(left, top, unchecked(right - left), unchecked(bottom - top)); + + #endregion + + + #region Methods + + /// + /// Determines if the rectangular region represented by is entirely contained within the + /// rectangular region represented by this . + /// + public readonly bool Contains(RectangleF rect) => m_rect.Contains(rect.m_rect); + + /// + /// Determines if the specified point is contained within the rectangular region defined by this + /// . + /// + public readonly bool Contains(float x, float y) => m_rect.Contains(x, y); + + /// + /// Determines if the specified point is contained within the rectangular region defined by this + /// . + /// + public readonly bool Contains(PointF pt) => Contains(pt.X, pt.Y); + + /// + /// Creates a that represents the intersection between this Rectangle and rect. + /// + public void Intersect(RectangleF rect) => m_rect.Intersect(rect.m_rect); + + /// + /// Creates a that represents the intersection between a and b. If there + /// is no intersection, an empty rectangle is returned. + /// + public static RectangleF Intersect(RectangleF a, RectangleF b) + { + var ret = SKRect.Intersect(a.m_rect, b.m_rect); + return new RectangleF(ret); + } + + /// + /// Determines if this intersects with rect. + /// + public readonly bool IntersectsWith(RectangleF rect) => m_rect.IntersectsWith(rect.m_rect); + + /// + /// Creates a that represents the union between this Rectangle and rect. + /// + public void Union(RectangleF rect) => m_rect.Union(rect.m_rect); + + /// + /// Creates a that represents the union between a and b. + /// + public static RectangleF Union(RectangleF a, RectangleF b) + { + var ret = SKRect.Union(a.m_rect, b.m_rect); + return new RectangleF(ret); + } + + /// + /// Inflates this by the specified amount. + /// + public void Inflate(float width, float height) => m_rect.Inflate(width, height); + + /// + /// Inflates this by the specified amount. + /// + public void Inflate(SizeF size) => Inflate(size.Width, size.Height); + + /// + /// Creates a that is inflated by the specified amount. + /// + public static RectangleF Inflate(RectangleF rect, float x, float y) + { + var ret = SKRect.Inflate(rect.m_rect, x, y); + return new RectangleF(ret); + } + + /// + /// Adjusts the location of this by the specified amount. + /// + public void Offset(float x, float y) => m_rect.Offset(x, y); + + /// + /// Adjusts the location of this by the specified amount. + /// + public void Offset(PointF pos) => Offset(pos.X, pos.Y); + + #endregion +} diff --git a/src/Common/Size.cs b/src/Common/Size.cs index 15f15c0..e80727f 100644 --- a/src/Common/Size.cs +++ b/src/Common/Size.cs @@ -16,7 +16,7 @@ private Size(SKSize size) /// /// Initializes a new instance of the class from the specified dimensions. /// - public Size(float width, float height) + public Size(int width, int height) : this(new SKSize(width, height)) { } /// @@ -65,28 +65,28 @@ public Size(Point pt) public static Size operator -(Size sz1, Size sz2) => Subtract(sz1, sz2); /// - /// Multiplies by an producing . + /// Multiplies by an producing . /// /// Multiplicand of type . - /// Multiplier of type . + /// Multiplier of type . /// Product of type . - public static Size operator *(Size left, float right) => Multiply(left, right); + public static Size operator *(Size left, int right) => Multiply(left, right); /// - /// Multiplies a by an producing . + /// Multiplies a by an producing . /// - /// Multiplier of type . + /// Multiplier of type . /// Multiplicand of type . /// Product of type . - public static Size operator *(float left, Size right) => Multiply(right, left); + public static Size operator *(int left, Size right) => Multiply(right, left); /// - /// Divides by an producing . + /// Divides by an producing . /// /// Dividend of type . - /// Divisor of type . + /// Divisor of type . /// Result of type . - public static Size operator /(Size left, float right) => Divide(left, right); + public static Size operator /(Size left, int right) => Divide(left, right); #endregion @@ -128,18 +128,18 @@ public Size(Point pt) /// /// Represents the horizontal component of this . /// - public float Width + public int Width { - readonly get => m_size.Width; + readonly get => (int)m_size.Width; set => m_size.Width = value; } /// /// Represents the vertical component of this . /// - public float Height + public int Height { - readonly get => m_size.Height; + readonly get => (int)m_size.Height; set => m_size.Height = value; } @@ -159,40 +159,25 @@ public float Height public static Size Add(Size sz1, Size sz2) => new(sz1.m_size + sz2.m_size); /// - /// Performs vector substraction of two objects. + /// Performs vector subtraction of two objects. /// public static Size Subtract(Size sz1, Size sz2) => new(sz1.m_size - sz2.m_size); /// - /// Converts a by performing a ceiling operation on all the coordinates. - /// - public static Size Ceiling(Size value) => new(unchecked((int)Math.Ceiling(value.Width)), unchecked((int)Math.Ceiling(value.Height))); - - /// - /// Converts a by performing a truncate operation on all the coordinates. - /// - public static Size Truncate(Size value) => new(unchecked((int)value.Width), unchecked((int)value.Height)); - - /// - /// Converts a by performing a round operation on all the coordinates. - /// - public static Size Round(Size value) => new(unchecked((int)Math.Round(value.Width)), unchecked((int)Math.Round(value.Height))); - - /// - /// Multiplies by an producing . + /// Multiplies by an producing . /// /// Multiplicand of type . - /// Multiplier of type . + /// Multiplier of type . /// Product of type . - public static Size Multiply(Size size, float multiplier) => new(unchecked(size.Width * multiplier), unchecked(size.Height * multiplier)); + public static Size Multiply(Size size, int multiplier) => new(size.Width * multiplier, size.Height * multiplier); /// - /// Divides by an producing . + /// Divides by an producing . /// - /// Divident of type . - /// Denominator of type . + /// Dividend of type . + /// Divisor of type . /// Quotient of type . - public static Size Divide(Size size, float denominator) => new(unchecked(size.Width / denominator), unchecked(size.Height / denominator)); + public static Size Divide(Size size, int divisor) => new(size.Width / divisor, size.Height / divisor); #endregion } diff --git a/src/Common/SizeF.cs b/src/Common/SizeF.cs new file mode 100644 index 0000000..434f34e --- /dev/null +++ b/src/Common/SizeF.cs @@ -0,0 +1,198 @@ +using System; +using SkiaSharp; + +namespace GeneXus.Drawing; + +[Serializable] +public struct SizeF : IEquatable +{ + internal SKSize m_size; + + private SizeF(SKSize size) + { + m_size = size; + } + + /// + /// Initializes a new instance of the class from the specified dimensions. + /// + public SizeF(float width, float height) + : this(new SKSize(width, height)) { } + + /// + /// Initializes a new instance of the class from the specified + /// . + /// + public SizeF(PointF pt) + : this(pt.X, pt.Y) { } + + /// + /// Creates a human-readable string that represents this . + /// + public override readonly string ToString() => $"{{Width={Width},Height={Height}}}"; + + + #region Operators + + /// + /// Converts the specified to a . + /// + public static explicit operator SKSize(SizeF point) => point.m_size; + + /// + /// Converts the specified to a . + /// + public static explicit operator PointF(SizeF size) => new(size.Width, size.Height); + + /// + /// Tests whether two objects are identical. + /// + public static bool operator ==(SizeF sz1, SizeF sz2) => sz1.m_size == sz2.m_size; + + /// + /// Tests whether two objects are different. + /// + public static bool operator !=(SizeF sz1, SizeF sz2) => sz1.m_size != sz2.m_size; + + /// + /// Performs vector addition of two objects. + /// + public static SizeF operator +(SizeF sz1, SizeF sz2) => Add(sz1, sz2); + + /// + /// Contracts a by another + /// + public static SizeF operator -(SizeF sz1, SizeF sz2) => Subtract(sz1, sz2); + + /// + /// Multiplies by an producing . + /// + /// Multiplicand of type . + /// Multiplier of type . + /// Product of type . + public static SizeF operator *(SizeF left, float right) => Multiply(left, right); + + /// + /// Multiplies a by an producing . + /// + /// Multiplier of type . + /// Multiplicand of type . + /// Product of type . + public static SizeF operator *(float left, SizeF right) => Multiply(right, left); + + /// + /// Divides by an producing . + /// + /// Dividend of type . + /// Divisor of type . + /// Result of type . + public static SizeF operator /(SizeF left, float right) => Divide(left, right); + + #endregion + + + #region IEquatable + + /// + /// Tests whether a has the same dimensions + /// as this Size. + /// + public readonly bool Equals(SizeF other) => m_size == other.m_size; + + /// + /// Tests whether is a with the same dimensions + /// as this Size. + /// + public override readonly bool Equals(object obj) => m_size.Equals(obj); + + /// + /// Returns a hash code. + /// + public override readonly int GetHashCode() => m_size.GetHashCode(); + + #endregion + + + #region Fields + + /// + /// Initializes a new instance of the class. + /// + public static readonly SizeF Empty = default; + + #endregion + + + #region Properties + + /// + /// Represents the horizontal component of this . + /// + public float Width + { + readonly get => m_size.Width; + set => m_size.Width = value; + } + + /// + /// Represents the vertical component of this . + /// + public float Height + { + readonly get => m_size.Height; + set => m_size.Height = value; + } + + /// + /// Tests whether this has zero width and height. + /// + public readonly bool IsEmpty => m_size.IsEmpty; + + #endregion + + + #region Methods + + /// + /// Performs vector addition of two objects. + /// + public static SizeF Add(SizeF sz1, SizeF sz2) => new(sz1.m_size + sz2.m_size); + + /// + /// Performs vector subtraction of two objects. + /// + public static SizeF Subtract(SizeF sz1, SizeF sz2) => new(sz1.m_size - sz2.m_size); + + /// + /// Converts a by performing a ceiling operation on all the coordinates. + /// + public static SizeF Ceiling(SizeF value) => new(unchecked((int)Math.Ceiling(value.Width)), unchecked((int)Math.Ceiling(value.Height))); + + /// + /// Converts a by performing a truncate operation on all the coordinates. + /// + public static SizeF Truncate(SizeF value) => new(unchecked((int)value.Width), unchecked((int)value.Height)); + + /// + /// Converts a by performing a round operation on all the coordinates. + /// + public static SizeF Round(SizeF value) => new(unchecked((int)Math.Round(value.Width)), unchecked((int)Math.Round(value.Height))); + + /// + /// Multiplies by an producing . + /// + /// Multiplicand of type . + /// Multiplier of type . + /// Product of type . + public static SizeF Multiply(SizeF size, float multiplier) => new(unchecked(size.Width * multiplier), unchecked(size.Height * multiplier)); + + /// + /// Divides by an producing . + /// + /// Dividend of type . + /// Divisor of type . + /// Quotient of type . + public static SizeF Divide(SizeF size, float divisor) => new(unchecked(size.Width / divisor), unchecked(size.Height / divisor)); + + #endregion +} diff --git a/src/Common/Text/PrivateFontCollection.cs b/src/Common/Text/PrivateFontCollection.cs index 17e59c2..e6ab86b 100644 --- a/src/Common/Text/PrivateFontCollection.cs +++ b/src/Common/Text/PrivateFontCollection.cs @@ -9,15 +9,15 @@ public class PrivateFontCollection : FontCollection /// /// Initializes a new instance of the class. /// - public PrivateFontCollection() : base() + public PrivateFontCollection() { } /// /// Adds a font from the specified file to this . /// - public void AddFontFile(string filename) + public void AddFontFile(string filePath) { - var fontFamily = new FontFamily(filename); + var fontFamily = FontFamilyFactory.Create(filePath); m_families.Add(fontFamily); } @@ -31,7 +31,7 @@ public void AddMemoryFont(IntPtr memory, int length) using var stream = new MemoryStream(fontData); - var fontFamily = new FontFamily(stream); + var fontFamily = FontFamilyFactory.Create(stream); m_families.Add(fontFamily); } } diff --git a/test/Common/FontConverterTest.cs b/test/Common/FontConverterTest.cs new file mode 100644 index 0000000..9b1194c --- /dev/null +++ b/test/Common/FontConverterTest.cs @@ -0,0 +1,27 @@ +using System; +using System.ComponentModel; + +namespace GeneXus.Drawing.Test; + +internal class FontConverterTest +{ + [Test] + public void Method_ConvertFromString() + { + Font font = TypeDescriptor.GetConverter(typeof(Font)).ConvertFromString("Verdana,12") as Font; + Assert.Multiple(() => + { + Assert.That(font?.Name, Is.EqualTo("Verdana")); + Assert.That(Math.Abs(font.Size - 12f), Is.LessThan(0.01)); + Assert.That(font.Unit, Is.EqualTo(GraphicsUnit.Point)); + }); + } + + [Test] + public void Method_ConvertToString() + { + Font font = new("Verdana"); // default size is 12 + string text = TypeDescriptor.GetConverter(typeof(Font)).ConvertToString(font); + Assert.That(text, Is.EqualTo("Verdana, 12pt")); + } +} diff --git a/test/Common/FontFamilyUnitTest.cs b/test/Common/FontFamilyUnitTest.cs index 0471b66..6cc1a00 100644 --- a/test/Common/FontFamilyUnitTest.cs +++ b/test/Common/FontFamilyUnitTest.cs @@ -32,7 +32,7 @@ public void Setup() public void Constructor_FileName(string fileName, string familyName, int ascent, int descent, int lineSpacing, int emHeight, int fontIndex = 0) { var fontPath = Path.Combine(FONT_PATH, fileName); - using var family = new FontFamily(fontPath, fontIndex); + using var family = FontFamilyFactory.Create(fontPath, fontIndex); Assert.Multiple(() => { Assert.That(family.Name, Is.EqualTo(familyName)); @@ -60,7 +60,7 @@ public void Constructor_Stream(string fileName, string familyName, int ascent, i { var fontPath = Path.Combine(FONT_PATH, fileName); using var fontStream = File.OpenRead(fontPath); - using var family = new FontFamily(fontStream, fontIndex); + using var family = FontFamilyFactory.Create(fontStream, fontIndex); Assert.Multiple(() => { Assert.That(family.Name, Is.EqualTo(familyName)); @@ -81,7 +81,7 @@ public void Constructor_FontCollection() Assert.That(pfc.Families, Has.Length.EqualTo(3)); using var font = new Font(pfc.Families[1]); - using var family = new FontFamily(font.Name, pfc); + using var family = FontFamilyFactory.Create(font.Name, pfc); Assert.Multiple(() => { Assert.That(family.Name, Is.EqualTo("Montserrat")); @@ -94,13 +94,13 @@ public void Constructor_FontCollection() [Test] public void Constructor_GenericFont() { - using var monospace = new FontFamily(GenericFontFamilies.Monospace); + using var monospace = FontFamilyFactory.Create(GenericFontFamilies.Monospace); Assert.That(monospace.Name, Is.AnyOf("Courier New", "Consolas", "Courier", "Menlo", "Monaco", "Lucida Console").IgnoreCase); - using var sanserif = new FontFamily(GenericFontFamilies.SansSerif); + using var sanserif = FontFamilyFactory.Create(GenericFontFamilies.SansSerif); Assert.That(sanserif.Name, Is.AnyOf("Arial", "Helvetica", "Verdana", "Tahoma", "Trebuchet MS", "Gill Sans").IgnoreCase); - using var justserif = new FontFamily(GenericFontFamilies.Serif); + using var justserif = FontFamilyFactory.Create(GenericFontFamilies.Serif); Assert.That(justserif.Name, Is.AnyOf("Times New Roman", "Georgia", "Garamond", "Palatino", "Book Antiqua", "Baskerville").IgnoreCase); } } diff --git a/test/Common/FontUnitTest.cs b/test/Common/FontUnitTest.cs index 4136b00..c17c638 100644 --- a/test/Common/FontUnitTest.cs +++ b/test/Common/FontUnitTest.cs @@ -31,7 +31,7 @@ public void Setup() public void Constructor_Family(string fileName, string familyName, string faceName, int fontWeight, int fontWidth, int fontHeight, SlantType fontSlant, FontStyle fontStyle, int fontIndex = 0) { var fontPath = Path.Combine(FONT_PATH, fileName); - using var family = new FontFamily(fontPath, fontIndex); + using var family = FontFamilyFactory.Create(fontPath, fontIndex); using var font = new Font(family); Assert.Multiple(() => @@ -98,8 +98,8 @@ public void Property_IsSystemFont() [Test] public void Method_Clone() { - var fontpath = Path.Combine(FONT_PATH, "Montserrat-Regular.ttf"); - using var family = new FontFamily(fontpath); + var fontPath = Path.Combine(FONT_PATH, "Montserrat-Regular.ttf"); + using var family = FontFamilyFactory.Create(fontPath); using var font1 = new Font(family); using var font2 = font1.Clone() as Font; @@ -130,5 +130,4 @@ public void Extra_GetFontCount_Stream() var count = Font.GetFontCount(stream); Assert.That(count, Is.EqualTo(12)); } - } diff --git a/test/Common/PointFUnitTest.cs b/test/Common/PointFUnitTest.cs new file mode 100644 index 0000000..e2c0140 --- /dev/null +++ b/test/Common/PointFUnitTest.cs @@ -0,0 +1,188 @@ +namespace GeneXus.Drawing.Test; + +internal class PointFUnitTest +{ + [SetUp] + public void Setup() + { + } + + [Test] + public void Constructor_None() + { + var point = new PointF(); + Assert.Multiple(() => + { + Assert.That(point.X, Is.EqualTo(0f)); + Assert.That(point.Y, Is.EqualTo(0f)); + }); + } + + [Test] + public void Constructor_Properties() + { + float x = 10f, y = 20f; + var point = new PointF { X = x, Y = y }; + Assert.Multiple(() => + { + Assert.That(point.X, Is.EqualTo(x)); + Assert.That(point.Y, Is.EqualTo(y)); + }); + } + + [Test] + public void Constructor_Float() + { + float x = 10f, y = -20f; + var point = new PointF(x, y); + Assert.Multiple(() => + { + Assert.That(point.X, Is.EqualTo(x)); + Assert.That(point.Y, Is.EqualTo(y)); + }); + } + + [Test] + public void Constructor_Size() + { + var size = new SizeF(30f, 40f); + var point = new PointF(size); + Assert.Multiple(() => + { + Assert.That(point.X, Is.EqualTo(size.Width)); + Assert.That(point.Y, Is.EqualTo(size.Height)); + }); + } + + [Test] + public void Constructor_Int() + { + int x = 100, y = 200; + int dw = unchecked(y << 16) | unchecked(x << 0); + var point = new PointF(dw); + Assert.Multiple(() => + { + Assert.That(point.X, Is.EqualTo(x)); + Assert.That(point.Y, Is.EqualTo(y)); + }); + } + + [Test] + public void Operator_Equality() + { + var point1 = new PointF(10f, 20f); + var point2 = new PointF(10f, 20f); + Assert.That(point1 == point2, Is.True); + } + + [Test] + public void Operator_Inequality() + { + var point1 = new PointF(10f, 20f); + var point2 = new PointF(20f, 30f); + Assert.That(point1 != point2, Is.True); + } + + [Test] + public void Operator_Addition() + { + var point = new PointF(10f, 20f); + var size = new SizeF(30f, 40f); + var result = point + size; + Assert.Multiple(() => + { + Assert.That(result.X, Is.EqualTo(40f)); + Assert.That(result.Y, Is.EqualTo(60f)); + }); + } + + [Test] + public void Operator_Subtraction() + { + var point = new PointF(50f, 60f); + var size = new SizeF(20f, 30f); + var result = point - size; + Assert.Multiple(() => + { + Assert.That(result.X, Is.EqualTo(30f)); + Assert.That(result.Y, Is.EqualTo(30f)); + }); + } + + [Test] + public void Method_Equals() + { + var point1 = new PointF(10f, 20f); + var point2 = new PointF(10f, 20f); + Assert.That(point1.Equals(point2), Is.True); + } + + [Test] + public void Method_GetHashCode() + { + var point1 = new PointF(10f, 20f); + var point2 = new PointF(10f, 20f); + Assert.That(point2.GetHashCode(), Is.EqualTo(point1.GetHashCode())); + } + + [Test] + public void Method_Offset() + { + var point = new PointF(10f, 20f); + point.Offset(5f, -5f); + Assert.Multiple(() => + { + Assert.That(point.X, Is.EqualTo(15f)); + Assert.That(point.Y, Is.EqualTo(15f)); + }); + } + + [Test] + public void Method_Offset_Point() + { + var point = new PointF(10f, 20f); + var offsetPoint = new PointF(5f, -5f); + point.Offset(offsetPoint); + Assert.Multiple(() => + { + Assert.That(point.X, Is.EqualTo(15f)); + Assert.That(point.Y, Is.EqualTo(15f)); + }); + } + + [Test] + public void Static_Method_Ceiling() + { + var point = new PointF(10.4f, 20.6f); + var result = PointF.Ceiling(point); + Assert.Multiple(() => + { + Assert.That(result.X, Is.EqualTo(11f)); + Assert.That(result.Y, Is.EqualTo(21f)); + }); + } + + [Test] + public void Static_Method_Truncate() + { + var point = new PointF(10.9f, 20.6f); + var result = PointF.Truncate(point); + Assert.Multiple(() => + { + Assert.That(result.X, Is.EqualTo(10f)); + Assert.That(result.Y, Is.EqualTo(20f)); + }); + } + + [Test] + public void Static_Method_Round() + { + var point = new PointF(10.4f, 20.6f); + var result = PointF.Round(point); + Assert.Multiple(() => + { + Assert.That(result.X, Is.EqualTo(10f)); + Assert.That(result.Y, Is.EqualTo(21f)); + }); + } +} diff --git a/test/Common/PointUnitTest.cs b/test/Common/PointUnitTest.cs index 79271fc..4e0fcec 100644 --- a/test/Common/PointUnitTest.cs +++ b/test/Common/PointUnitTest.cs @@ -21,8 +21,9 @@ public void Constructor_None() [Test] public void Constructor_Properties() { - float x = 10f, y = 20f; - var point = new Point() { X = x, Y = y }; + const int x = 10; + const int y = 20; + Point point = new() { X = x, Y = y }; Assert.Multiple(() => { Assert.That(point.X, Is.EqualTo(x)); @@ -33,8 +34,9 @@ public void Constructor_Properties() [Test] public void Constructor_Float() { - float x = 10f, y = -20f; - var point = new Point(x, y); + const int x = 10; + const int y = -20; + Point point = new(x, y); Assert.Multiple(() => { Assert.That(point.X, Is.EqualTo(x)); @@ -45,8 +47,8 @@ public void Constructor_Float() [Test] public void Constructor_Size() { - var size = new Size(30f, 40f); - var point = new Point(size); + Size size = new(30, 40); + Point point = new(size); Assert.Multiple(() => { Assert.That(point.X, Is.EqualTo(size.Width)); @@ -57,9 +59,10 @@ public void Constructor_Size() [Test] public void Constructor_Int() { - int x = 100, y = 200; - int dw = unchecked(y << 16) | unchecked(x << 0); - var point = new Point(dw); + const int x = 100; + const int y = 200; + const int dw = y << 16 | x << 0; + Point point = new(dw); Assert.Multiple(() => { Assert.That(point.X, Is.EqualTo(x)); @@ -70,38 +73,38 @@ public void Constructor_Int() [Test] public void Operator_Equality() { - var point1 = new Point(10f, 20f); - var point2 = new Point(10f, 20f); + Point point1 = new(10, 20); + Point point2 = new(10, 20); Assert.That(point1 == point2, Is.True); } [Test] public void Operator_Inequality() { - var point1 = new Point(10f, 20f); - var point2 = new Point(20f, 30f); - Assert.That(point1 != point2, Is.True); + Point point1 = new(10, 20); + Point point2 = new(20, 30); + Assert.That(point1, Is.Not.EqualTo(point2)); } [Test] public void Operator_Addition() { - var point = new Point(10f, 20f); - var size = new Size(30f, 40f); - var result = point + size; + Point point = new(10, 20); + Size size = new(30, 40); + Point result = point + size; Assert.Multiple(() => { - Assert.That(result.X, Is.EqualTo(40f)); - Assert.That(result.Y, Is.EqualTo(60f)); + Assert.That(result.X, Is.EqualTo(40)); + Assert.That(result.Y, Is.EqualTo(60)); }); } [Test] public void Operator_Subtraction() { - var point = new Point(50f, 60f); - var size = new Size(20f, 30f); - var result = point - size; + Point point = new(50, 60); + Size size = new(20, 30); + Point result = point - size; Assert.Multiple(() => { Assert.That(result.X, Is.EqualTo(30f)); @@ -112,24 +115,24 @@ public void Operator_Subtraction() [Test] public void Method_Equals() { - var point1 = new Point(10f, 20f); - var point2 = new Point(10f, 20f); + Point point1 = new(10, 20); + Point point2 = new(10, 20); Assert.That(point1.Equals(point2), Is.True); } [Test] public void Method_GetHashCode() { - var point1 = new Point(10f, 20f); - var point2 = new Point(10f, 20f); + Point point1 = new(10, 20); + Point point2 = new(10, 20); Assert.That(point2.GetHashCode(), Is.EqualTo(point1.GetHashCode())); } [Test] public void Method_Offset() { - var point = new Point(10f, 20f); - point.Offset(5f, -5f); + Point point = new(10, 20); + point.Offset(5, -5); Assert.Multiple(() => { Assert.That(point.X, Is.EqualTo(15f)); @@ -140,8 +143,8 @@ public void Method_Offset() [Test] public void Method_Offset_Point() { - var point = new Point(10f, 20f); - var offsetPoint = new Point(5f, -5f); + Point point = new(10, 20); + Point offsetPoint = new(5, -5); point.Offset(offsetPoint); Assert.Multiple(() => { @@ -149,40 +152,4 @@ public void Method_Offset_Point() Assert.That(point.Y, Is.EqualTo(15f)); }); } - - [Test] - public void Static_Method_Ceiling() - { - var point = new Point(10.4f, 20.6f); - var result = Point.Ceiling(point); - Assert.Multiple(() => - { - Assert.That(result.X, Is.EqualTo(11f)); - Assert.That(result.Y, Is.EqualTo(21f)); - }); - } - - [Test] - public void Static_Method_Truncate() - { - var point = new Point(10.9f, 20.6f); - var result = Point.Truncate(point); - Assert.Multiple(() => - { - Assert.That(result.X, Is.EqualTo(10f)); - Assert.That(result.Y, Is.EqualTo(20f)); - }); - } - - [Test] - public void Static_Method_Round() - { - var point = new Point(10.4f, 20.6f); - var result = Point.Round(point); - Assert.Multiple(() => - { - Assert.That(result.X, Is.EqualTo(10f)); - Assert.That(result.Y, Is.EqualTo(21f)); - }); - } } \ No newline at end of file diff --git a/test/Common/RectangleFUnitTest.cs b/test/Common/RectangleFUnitTest.cs new file mode 100644 index 0000000..e3a8c2a --- /dev/null +++ b/test/Common/RectangleFUnitTest.cs @@ -0,0 +1,315 @@ +namespace GeneXus.Drawing.Test; + +internal class RectangleFUnitTest +{ + [SetUp] + public void Setup() + { + } + + [Test] + public void Constructor_Float() + { + float x = 5f, y = 10f, w = 100f, h = 200f; + var rect = new RectangleF(x, y, w, h); + Assert.Multiple(() => + { + Assert.That(rect.X, Is.EqualTo(x)); + Assert.That(rect.Y, Is.EqualTo(y)); + Assert.That(rect.Width, Is.EqualTo(w)); + Assert.That(rect.Height, Is.EqualTo(h)); + }); + } + + [Test] + public void Constructor_PointSize() + { + float x = 5f, y = 10f, w = 100f, h = 200f; + var point = new PointF(x, y); + var size = new SizeF(w, h); + var rect = new RectangleF(point, size); + Assert.Multiple(() => + { + Assert.That(rect.X, Is.EqualTo(x)); + Assert.That(rect.Y, Is.EqualTo(y)); + Assert.That(rect.Width, Is.EqualTo(w)); + Assert.That(rect.Height, Is.EqualTo(h)); + }); + } + + [Test] + public void Constructor_FloatSize() + { + float x = 5f, y = 10f, w = 100f, h = 200f; + var size = new SizeF(w, h); + var rect = new RectangleF(x, y, size); + Assert.Multiple(() => + { + Assert.That(rect.X, Is.EqualTo(x)); + Assert.That(rect.Y, Is.EqualTo(y)); + Assert.That(rect.Width, Is.EqualTo(w)); + Assert.That(rect.Height, Is.EqualTo(h)); + }); + } + + [Test] + public void Constructor_PointFloat() + { + float x = 5f, y = 10f, w = 100f, h = 200f; + var point = new PointF(x, y); + var rect = new RectangleF(point, w, h); + Assert.Multiple(() => + { + Assert.That(rect.X, Is.EqualTo(x)); + Assert.That(rect.Y, Is.EqualTo(y)); + Assert.That(rect.Width, Is.EqualTo(w)); + Assert.That(rect.Height, Is.EqualTo(h)); + }); + } + + [Test] + public void Operator_Equality() + { + var rect1 = new RectangleF(5f, 10f, 100f, 200f); + var rect2 = new RectangleF(5f, 10f, 100f, 200f); + Assert.That(rect1 == rect2, Is.True); + } + + [Test] + public void Operator_Inequality() + { + var rect1 = new RectangleF(5f, 10f, 100f, 200f); + var rect2 = new RectangleF(10f, 5f, 200f, 100f); + Assert.That(rect1 != rect2, Is.True); + } + + [Test] + public void Property_X() + { + float x = 5f, y = 10f, w = 100f, h = 200f; + var rect = new RectangleF(x, y, w, h); + rect.X += 5; + Assert.Multiple(() => + { + Assert.That(rect.X, Is.EqualTo(x + 5)); + Assert.That(rect.Y, Is.EqualTo(y)); + Assert.That(rect.Width, Is.EqualTo(w)); + Assert.That(rect.Height, Is.EqualTo(h)); + }); + } + + [Test] + public void Property_Y() + { + float x = 5f, y = 10f, w = 100f, h = 200f; + var rect = new RectangleF(x, y, w, h); + rect.Y += 5f; + Assert.Multiple(() => + { + Assert.That(rect.X, Is.EqualTo(x)); + Assert.That(rect.Y, Is.EqualTo(y + 5f)); + Assert.That(rect.Width, Is.EqualTo(w)); + Assert.That(rect.Height, Is.EqualTo(h)); + }); + } + + [Test] + public void Property_Width() + { + float x = 5f, y = 10f, w = 100f, h = 200f; + var rect = new RectangleF(x, y, w, h); + rect.Width += 50; + Assert.Multiple(() => + { + Assert.That(rect.X, Is.EqualTo(x)); + Assert.That(rect.Y, Is.EqualTo(y)); + Assert.That(rect.Width, Is.EqualTo(w + 50f)); + Assert.That(rect.Height, Is.EqualTo(h)); + }); + } + + [Test] + public void Property_Height() + { + float x = 5f, y = 10f, w = 100f, h = 200f; + var rect = new RectangleF(x, y, w, h); + rect.Height += 50f; + Assert.Multiple(() => + { + Assert.That(rect.X, Is.EqualTo(x)); + Assert.That(rect.Y, Is.EqualTo(y)); + Assert.That(rect.Width, Is.EqualTo(w)); + Assert.That(rect.Height, Is.EqualTo(h + 50f)); + }); + } + + [Test] + public void Property_Left() + { + float x = 5f, y = 10f, w = 100f, h = 200f; + var rect = new RectangleF(x, y, w, h); + Assert.That(rect.Left, Is.EqualTo(x)); + } + + [Test] + public void Property_Right() + { + float x = 5f, y = 10f, w = 100f, h = 200f; + var rect = new RectangleF(x, y, w, h); + Assert.That(rect.Right, Is.EqualTo(x + w)); + } + + [Test] + public void Property_Top() + { + float x = 5f, y = 10f, w = 100f, h = 200f; + var rect = new RectangleF(x, y, w, h); + Assert.That(rect.Top, Is.EqualTo(y)); + } + + [Test] + public void Property_Bottom() + { + float x = 5f, y = 10f, w = 100f, h = 200f; + var rect = new RectangleF(x, y, w, h); + Assert.That(rect.Bottom, Is.EqualTo(y + h)); + } + + [Test] + public void Property_Location() + { + float x = 5f, y = 10f, w = 100f, h = 200f; + var rect = new RectangleF(x, y, w, h); + Assert.Multiple(() => + { + Assert.That(rect.Location.X, Is.EqualTo(x)); + Assert.That(rect.Location.Y, Is.EqualTo(y)); + }); + } + + [Test] + public void Property_Size() + { + float x = 5f, y = 10f, w = 100f, h = 200f; + var rect = new RectangleF(x, y, w, h); + Assert.Multiple(() => + { + Assert.That(rect.Size.Width, Is.EqualTo(w)); + Assert.That(rect.Size.Height, Is.EqualTo(h)); + }); + } + + [Test] + public void Method_FromLTRB() + { + float l = 5f, t = 10f, r = 105f, b = 210f; + var rect = RectangleF.FromLTRB(l, t, r, b); + Assert.Multiple(() => + { + Assert.That(rect.Left, Is.EqualTo(l)); + Assert.That(rect.Top, Is.EqualTo(t)); + Assert.That(rect.Right, Is.EqualTo(r)); + Assert.That(rect.Bottom, Is.EqualTo(b)); + }); + } + + [Test] + public void Method_Contains() + { + var rect1 = new RectangleF(10f, 20f, 100f, 50f); + var rect2 = new RectangleF(20f, 30f, 50f, 20f); + Assert.That(rect1.Contains(rect2), Is.True); + } + + [Test] + public void Method_Contains_Point() + { + var rect = new RectangleF(10f, 20f, 100f, 50f); + Assert.Multiple(() => + { + Assert.That(rect.Contains(50f, 40f), Is.True); + Assert.That(rect.Contains(5f, 15f), Is.False); + }); + } + + [Test] + public void Method_IntersectWith() + { + var rect1 = new RectangleF(10f, 20f, 100f, 50f); + var rect2 = new RectangleF(50f, 30f, 80f, 70f); + Assert.That(rect1.IntersectsWith(rect2), Is.True); + } + + [Test] + public void Method_Union() + { + var rect1 = new RectangleF(10f, 20f, 100f, 50f); + var rect2 = new RectangleF(50f, 30f, 80f, 70f); + rect1.Union(rect2); + Assert.Multiple(() => + { + Assert.That(rect1.Left, Is.EqualTo(10f)); + Assert.That(rect1.Top, Is.EqualTo(20f)); + Assert.That(rect1.Right, Is.EqualTo(130f)); + Assert.That(rect1.Bottom, Is.EqualTo(100f)); + }); + } + + [Test] + public void Method_Inflate() + { + var rect = new RectangleF(10f, 20f, 100f, 50f); + rect.Inflate(5f, 10f); + Assert.Multiple(() => + { + Assert.That(rect.Left, Is.EqualTo(5f)); + Assert.That(rect.Top, Is.EqualTo(10f)); + Assert.That(rect.Right, Is.EqualTo(115f)); + Assert.That(rect.Bottom, Is.EqualTo(80f)); + }); + } + + [Test] + public void Method_Inflate_Size() + { + var size = new SizeF(5f, 18); + var rect = new RectangleF(10f, 20f, 100f, 50f); + rect.Inflate(size); + Assert.Multiple(() => + { + Assert.That(rect.Left, Is.EqualTo(5f)); + Assert.That(rect.Top, Is.EqualTo(2f)); + Assert.That(rect.Right, Is.EqualTo(115f)); + Assert.That(rect.Bottom, Is.EqualTo(88f)); + }); + } + + [Test] + public void Method_Offset() + { + var rect = new RectangleF(10f, 20f, 100f, 50f); + rect.Offset(5f, 10f); + Assert.Multiple(() => + { + Assert.That(rect.Left, Is.EqualTo(15f)); + Assert.That(rect.Top, Is.EqualTo(30f)); + Assert.That(rect.Right, Is.EqualTo(115f)); + Assert.That(rect.Bottom, Is.EqualTo(80f)); + }); + } + + [Test] + public void Method_Offset_Point() + { + var point = new PointF(5f, 10f); + var rect = new RectangleF(10f, 20f, 100f, 50f); + rect.Offset(point); + Assert.Multiple(() => + { + Assert.That(rect.Left, Is.EqualTo(15f)); + Assert.That(rect.Top, Is.EqualTo(30f)); + Assert.That(rect.Right, Is.EqualTo(115f)); + Assert.That(rect.Bottom, Is.EqualTo(80f)); + }); + } +} diff --git a/test/Common/SizeFUnitTest.cs b/test/Common/SizeFUnitTest.cs new file mode 100644 index 0000000..2442302 --- /dev/null +++ b/test/Common/SizeFUnitTest.cs @@ -0,0 +1,186 @@ +namespace GeneXus.Drawing.Test; + +internal class SizeFUnitTest +{ + [SetUp] + public void Setup() + { + } + + [Test] + public void Constructor_None() + { + var size = new SizeF(); + Assert.Multiple(() => + { + Assert.That(size.Width, Is.EqualTo(0f)); + Assert.That(size.Height, Is.EqualTo(0f)); + }); + } + + [Test] + public void Constructor_Properties() + { + float w = 10f, h = 20f; + var size = new SizeF { Width = w, Height = h }; + Assert.Multiple(() => + { + Assert.That(size.Width, Is.EqualTo(w)); + Assert.That(size.Height, Is.EqualTo(h)); + }); + } + + [Test] + public void Constructor_Float() + { + float w = 10f, h = 20f; + var size = new SizeF(w, h); + Assert.Multiple(() => + { + Assert.That(size.Width, Is.EqualTo(w)); + Assert.That(size.Height, Is.EqualTo(h)); + }); + } + + [Test] + public void Constructor_Point() + { + var point = new PointF(30f, 40f); + var size = new SizeF(point); + Assert.Multiple(() => + { + Assert.That(size.Width, Is.EqualTo(point.X)); + Assert.That(size.Height, Is.EqualTo(point.Y)); + }); + } + + [Test] + public void Operator_Equality() + { + var size1 = new SizeF(10f, 20f); + var size2 = new SizeF(10f, 20f); + Assert.That(size1 == size2, Is.True); + } + + [Test] + public void Operator_Inequality() + { + var size1 = new SizeF(10f, 20f); + var size2 = new SizeF(20f, 30f); + Assert.That(size1 != size2, Is.True); + } + + [Test] + public void Operator_Addition() + { + var size1 = new SizeF(10f, 20f); + var size2 = new SizeF(5f, 10f); + var result = size1 + size2; + Assert.Multiple(() => + { + Assert.That(result.Width, Is.EqualTo(15f)); + Assert.That(result.Height, Is.EqualTo(30f)); + }); + } + + [Test] + public void Operator_Subtraction() + { + var size1 = new SizeF(10f, 20f); + var size2 = new SizeF(5f, 10f); + var result = size1 - size2; + Assert.Multiple(() => + { + Assert.That(result.Width, Is.EqualTo(5f)); + Assert.That(result.Height, Is.EqualTo(10f)); + }); + } + + [Test] + public void Operator_Multiplication_Right() + { + var size = new SizeF(10f, 20f); + var result = size * 2; + Assert.Multiple(() => + { + Assert.That(result.Width, Is.EqualTo(20f)); + Assert.That(result.Height, Is.EqualTo(40f)); + }); + } + + [Test] + public void Operator_Multiplication_Left() + { + var size = new SizeF(10f, 20f); + var result = 2 * size; + Assert.Multiple(() => + { + Assert.That(result.Width, Is.EqualTo(20f)); + Assert.That(result.Height, Is.EqualTo(40f)); + }); + } + + [Test] + public void Operator_Division() + { + var size1 = new SizeF(10f, 20f); + var result = size1 / 2; + Assert.Multiple(() => + { + Assert.That(result.Width, Is.EqualTo(5f)); + Assert.That(result.Height, Is.EqualTo(10f)); + }); + } + + [Test] + public void Operator_Round() + { + var size1 = new SizeF(10.2f, 20.8f); + var result = SizeF.Round(size1); + Assert.Multiple(() => + { + Assert.That(result.Width, Is.EqualTo(10f)); + Assert.That(result.Height, Is.EqualTo(21f)); + }); + } + + [Test] + public void Operator_Truncate() + { + var size1 = new SizeF(10.9f, 20.1f); + var result = SizeF.Truncate(size1); + Assert.Multiple(() => + { + Assert.That(result.Width, Is.EqualTo(10f)); + Assert.That(result.Height, Is.EqualTo(20f)); + }); + } + + [Test] + public void Operator_Ceiling() + { + var size1 = new SizeF(10.2f, 20.6f); + var result = SizeF.Ceiling(size1); + Assert.Multiple(() => + { + Assert.That(result.Width, Is.EqualTo(11f)); + Assert.That(result.Height, Is.EqualTo(21f)); + }); + } + + [Test] + public void Method_Equals() + { + var size1 = new SizeF(10f, 20f); + var size2 = new SizeF(10f, 20f); + Assert.That(size1.Equals(size2), Is.True); + } + + [Test] + public void Method_GetHashCode() + { + var size1 = new SizeF(10f, 20f); + var size2 = new SizeF(10f, 20f); + Assert.That(size2.GetHashCode(), Is.EqualTo(size1.GetHashCode())); + } +} diff --git a/test/Common/SizeUnitTest.cs b/test/Common/SizeUnitTest.cs index 77b3130..c0db5d0 100644 --- a/test/Common/SizeUnitTest.cs +++ b/test/Common/SizeUnitTest.cs @@ -21,8 +21,9 @@ public void Constructor_None() [Test] public void Constructor_Properties() { - float w = 10f, h = 20f; - var size = new Size() { Width = w, Height = h }; + const int w = 10; + const int h = 20; + Size size = new() { Width = w, Height = h }; Assert.Multiple(() => { Assert.That(size.Width, Is.EqualTo(w)); @@ -33,8 +34,9 @@ public void Constructor_Properties() [Test] public void Constructor_Float() { - float w = 10f, h = 20f; - var size = new Size(w, h); + const int w = 10; + const int h = 20; + Size size = new(w, h); Assert.Multiple(() => { Assert.That(size.Width, Is.EqualTo(w)); @@ -45,8 +47,8 @@ public void Constructor_Float() [Test] public void Constructor_Point() { - var point = new Point(30f, 40f); - var size = new Size(point); + Point point = new(30, 40); + Size size = new(point); Assert.Multiple(() => { Assert.That(size.Width, Is.EqualTo(point.X)); @@ -57,130 +59,94 @@ public void Constructor_Point() [Test] public void Operator_Equality() { - var size1 = new Size(10f, 20f); - var size2 = new Size(10f, 20f); + Size size1 = new(10, 20); + Size size2 = new(10, 20); Assert.That(size1 == size2, Is.True); } [Test] public void Operator_Inequality() { - var size1 = new Size(10f, 20f); - var size2 = new Size(20f, 30f); + Size size1 = new(10, 20); + Size size2 = new(20, 30); Assert.That(size1 != size2, Is.True); } [Test] public void Operator_Addition() { - var size1 = new Size(10f, 20f); - var size2 = new Size(5f, 10f); - var result = size1 + size2; + Size size1 = new(10, 20); + Size size2 = new(5, 10); + Size result = size1 + size2; Assert.Multiple(() => { - Assert.That(result.Width, Is.EqualTo(15f)); - Assert.That(result.Height, Is.EqualTo(30f)); + Assert.That(result.Width, Is.EqualTo(15)); + Assert.That(result.Height, Is.EqualTo(30)); }); } [Test] public void Operator_Substraction() { - var size1 = new Size(10f, 20f); - var size2 = new Size(5f, 10f); - var result = size1 - size2; + Size size1 = new(10, 20); + Size size2 = new(5, 10); + Size result = size1 - size2; Assert.Multiple(() => { - Assert.That(result.Width, Is.EqualTo(5f)); - Assert.That(result.Height, Is.EqualTo(10f)); + Assert.That(result.Width, Is.EqualTo(5)); + Assert.That(result.Height, Is.EqualTo(10)); }); } [Test] public void Operator_Multiplication_Right() { - var size = new Size(10f, 20f); - var result = size * 2; + Size size = new(10, 20); + Size result = size * 2; Assert.Multiple(() => { - Assert.That(result.Width, Is.EqualTo(20f)); - Assert.That(result.Height, Is.EqualTo(40f)); + Assert.That(result.Width, Is.EqualTo(20)); + Assert.That(result.Height, Is.EqualTo(40)); }); } [Test] public void Operator_Multiplication_Left() { - var size = new Size(10f, 20f); - var result = 2 * size; + Size size = new(10, 20); + Size result = 2 * size; Assert.Multiple(() => { - Assert.That(result.Width, Is.EqualTo(20f)); - Assert.That(result.Height, Is.EqualTo(40f)); + Assert.That(result.Width, Is.EqualTo(20)); + Assert.That(result.Height, Is.EqualTo(40)); }); } [Test] public void Operator_Division() { - var size1 = new Size(10f, 20f); - var result = size1 / 2; + Size size1 = new(10, 20); + Size result = size1 / 2; Assert.Multiple(() => { - Assert.That(result.Width, Is.EqualTo(5f)); - Assert.That(result.Height, Is.EqualTo(10f)); - }); - } - - [Test] - public void Operator_Round() - { - var size1 = new Size(10.2f, 20.8f); - var result = Size.Round(size1); - Assert.Multiple(() => - { - Assert.That(result.Width, Is.EqualTo(10f)); - Assert.That(result.Height, Is.EqualTo(21f)); - }); - } - - [Test] - public void Operator_Truncate() - { - var size1 = new Size(10.9f, 20.1f); - var result = Size.Truncate(size1); - Assert.Multiple(() => - { - Assert.That(result.Width, Is.EqualTo(10f)); - Assert.That(result.Height, Is.EqualTo(20f)); - }); - } - - [Test] - public void Operator_Ceiling() - { - var size1 = new Size(10.2f, 20.6f); - var result = Size.Ceiling(size1); - Assert.Multiple(() => - { - Assert.That(result.Width, Is.EqualTo(11f)); - Assert.That(result.Height, Is.EqualTo(21f)); + Assert.That(result.Width, Is.EqualTo(5)); + Assert.That(result.Height, Is.EqualTo(10)); }); } [Test] public void Method_Equals() { - var size1 = new Size(10f, 20f); - var size2 = new Size(10f, 20f); - Assert.That(size1.Equals(size2), Is.True); + Size size1 = new(10, 20); + Size size2 = new(10, 20); + Assert.That(size1, Is.EqualTo(size2)); } [Test] public void Method_GetHashCode() { - var size1 = new Size(10f, 20f); - var size2 = new Size(10f, 20f); + Size size1 = new(10, 20); + Size size2 = new(10, 20); Assert.That(size2.GetHashCode(), Is.EqualTo(size1.GetHashCode())); } }