diff --git a/NuGet.config b/NuGet.config index 6b25b1d..4afe8f6 100644 --- a/NuGet.config +++ b/NuGet.config @@ -6,6 +6,7 @@ + diff --git a/src/Microsoft.Maui.Graphics.Gtk/Extras/LineBreakMode.cs b/src/Microsoft.Maui.Graphics.Gtk/Extras/LineBreakMode.cs new file mode 100644 index 0000000..82ab3a3 --- /dev/null +++ b/src/Microsoft.Maui.Graphics.Gtk/Extras/LineBreakMode.cs @@ -0,0 +1,43 @@ +using System; + +// copied from: Microsoft.Maui/Core/src/Primitives/LineBreakMode.cs +// merged with: Xwt.Drawing/TextLayout.cs TextTrimming + +namespace Microsoft.Maui.Graphics.Extras { + + [Flags] + public enum LineBreakMode { + + None = 0, + + Wrap = 0x1 << 0, + Truncation = 0x1 << 1, + Elipsis = 0x1 << 2, + + Character = 0x1 << 3, + Word = 0x1 << 4, + + Start = 0x1 << 5, + Center = 0x1 << 6, + End = 0x1 << 7, + + NoWrap = None, + + WordWrap = Wrap | Word | End, + CharacterWrap = Wrap | Character | End, + WordCharacterWrap = Wrap | Word | Character | End, + + StartTruncation = Truncation | Character | Start, + EndTruncation = Truncation | Character | End, + CenterTruncation = Truncation | Character | Center, + + HeadTruncation = StartTruncation, + TailTruncation = EndTruncation, + MiddleTruncation = CenterTruncation, + + WordElipsis = Elipsis | Word | End, + CharacterElipsis = Elipsis | Character | End, + + } + +} diff --git a/src/Microsoft.Maui.Graphics.Gtk/Gtk/CanvasExtensions.cs b/src/Microsoft.Maui.Graphics.Gtk/Gtk/CanvasExtensions.cs new file mode 100644 index 0000000..801773f --- /dev/null +++ b/src/Microsoft.Maui.Graphics.Gtk/Gtk/CanvasExtensions.cs @@ -0,0 +1,45 @@ +namespace Microsoft.Maui.Graphics.Native.Gtk { + + public static class CanvasExtensions { + + public static Cairo.LineJoin ToLineJoin(this LineJoin lineJoin) => + lineJoin switch { + LineJoin.Bevel => Cairo.LineJoin.Bevel, + LineJoin.Round => Cairo.LineJoin.Round, + _ => Cairo.LineJoin.Miter + }; + + public static Cairo.FillRule ToFillRule(this WindingMode windingMode) => + windingMode switch { + WindingMode.EvenOdd => Cairo.FillRule.EvenOdd, + _ => Cairo.FillRule.Winding + }; + + public static Cairo.LineCap ToLineCap(this LineCap lineCap) => + lineCap switch { + LineCap.Butt => Cairo.LineCap.Butt, + LineCap.Round => Cairo.LineCap.Round, + _ => Cairo.LineCap.Square + }; + + public static Cairo.Antialias ToAntialias(bool antialias) => antialias ? Cairo.Antialias.Default : Cairo.Antialias.None; + + public static Size? GetSize(this Cairo.Surface it) { + if (it is Cairo.ImageSurface i) + return new Size(i.Width, i.Height); + + if (it is Cairo.XlibSurface x) + return new Size(x.Width, x.Height); + + if (it is Cairo.XcbSurface c) + return null; + + if (it is Cairo.SvgSurface s) + return null; + + return null; + } + + } + +} diff --git a/src/Microsoft.Maui.Graphics.Gtk/Gtk/ColorExtensions.cs b/src/Microsoft.Maui.Graphics.Gtk/Gtk/ColorExtensions.cs new file mode 100644 index 0000000..685a0fc --- /dev/null +++ b/src/Microsoft.Maui.Graphics.Gtk/Gtk/ColorExtensions.cs @@ -0,0 +1,33 @@ +using Gdk; + +namespace Microsoft.Maui.Graphics.Native.Gtk { + + public static class ColorExtensions { + + public static Gdk.RGBA ToGdkRgba(this Color color) + => color == default ? default : new RGBA {Red = color.Red, Green = color.Green, Blue = color.Blue, Alpha = color.Alpha}; + + public static Gdk.Color ToGdkColor(this Gdk.RGBA color) + => new Gdk.Color((byte) (color.Red * 255), (byte) (color.Green * 255), (byte) (color.Blue * 255)); + + public static Color ToColor(this Gdk.Color color, float opacity = 255) + => new Color(color.Red, color.Green, color.Blue, opacity); + + public static Color ToColor(this Gdk.RGBA color) + => new Color((byte) (color.Red * 255), (byte) (color.Green * 255), (byte) (color.Blue * 255), (byte) (color.Alpha * 255)); + + public static Cairo.Color ToCairoColor(this Color color) + => color == default ? default : new Cairo.Color(color.Red, color.Green, color.Blue, color.Alpha); + + public static Cairo.Color ToCairoColor(this Gdk.RGBA color) + => new Cairo.Color(color.Red, color.Green, color.Blue, color.Alpha); + + public static Gdk.Color ToGdkColor(this Color color) + => color == default ? default : new Gdk.Color((byte) (color.Red * 255), (byte) (color.Green * 255), (byte) (color.Blue * 255)); + + public static Color ToColor(this Gdk.Color color) + => new Color(color.Red / (float) ushort.MaxValue, color.Green / (float) ushort.MaxValue, color.Blue / (float) ushort.MaxValue); + + } + +} diff --git a/src/Microsoft.Maui.Graphics.Gtk/Gtk/FontExtensions.cs b/src/Microsoft.Maui.Graphics.Gtk/Gtk/FontExtensions.cs new file mode 100644 index 0000000..79ffecf --- /dev/null +++ b/src/Microsoft.Maui.Graphics.Gtk/Gtk/FontExtensions.cs @@ -0,0 +1,74 @@ +using System.Collections.Generic; +using System.Linq; + +namespace Microsoft.Maui.Graphics.Native.Gtk { + + public static class FontExtensions { + + /// + /// size in points + /// + /// the size of a font description is specified in pango units. + /// There are pango units in one device unit (the device unit is a point for font sizes). + /// + /// + /// + public static double GetSize(this Pango.FontDescription it) + => it.Size.ScaledFromPango(); + + public static Pango.Style ToPangoStyle(this FontStyleType it) => it switch { + FontStyleType.Oblique => Pango.Style.Oblique, + FontStyleType.Italic => Pango.Style.Italic, + _ => Pango.Style.Normal + }; + + public static FontStyleType ToFontStyleType(this Pango.Style it) => it switch { + Pango.Style.Oblique => FontStyleType.Oblique, + Pango.Style.Italic => FontStyleType.Italic, + _ => FontStyleType.Normal + }; + + // enum Pango.Weight { Thin = 100, Ultralight = 200, Light = 300, Semilight = 350, Book = 380, Normal = 400, Medium = 500, Semibold = 600, Bold = 700, Ultrabold = 800, Heavy = 900, Ultraheavy = 1000,} + + public static Pango.Weight ToFontWeigth(int it) { + static Pango.Weight Div(double v) => (Pango.Weight) ((int) v / 100 * 100); + + if (it < 100) + return Pango.Weight.Thin; + else if (it > 1000) + return Pango.Weight.Ultraheavy; + else if (it > 390 || it < 325) + return Div(it); + else if (it > 375) + return Pango.Weight.Book; + else if (it > 325) + return Pango.Weight.Semilight; + else + return 0; + + } + + public static double ToFontWeigth(this Pango.Weight it) + => (int) it; + + public static Pango.FontDescription ToFontDescription(this IFontStyle it) => + new Pango.FontDescription { + Style = it.StyleType.ToPangoStyle(), + Weight = ToFontWeigth(it.Weight), + Family = it.FontFamily?.Name, + + }; + + public static IEnumerable GetAvailableFontStyles(this Pango.FontFamily _native) { + return _native.Faces.Select(f => f.Describe().ToFontStyle()); + } + + public static IFontStyle ToFontStyle(this Pango.FontDescription it) => + new NativeFontStyle(it.Family, it.GetSize(), (int) it.Weight.ToFontWeigth(), it.Stretch, it.Style.ToFontStyleType()); + + public static IFontFamily ToFontFamily(this Pango.FontFamily it) => + new NativeFontFamily(it.Name); + + }; + +} diff --git a/src/Microsoft.Maui.Graphics.Gtk/Gtk/GraphicsExtensions.cs b/src/Microsoft.Maui.Graphics.Gtk/Gtk/GraphicsExtensions.cs new file mode 100644 index 0000000..9991aa0 --- /dev/null +++ b/src/Microsoft.Maui.Graphics.Gtk/Gtk/GraphicsExtensions.cs @@ -0,0 +1,63 @@ +using System; + +namespace Microsoft.Maui.Graphics.Native.Gtk { + + public static class GraphicsExtensions { + + public static Rectangle ToRectangle(this Gdk.Rectangle it) + => new Rectangle(it.X, it.Y, it.Width, it.Height); + + public static RectangleF ToRectangleF(this Gdk.Rectangle it) + => new RectangleF(it.X, it.Y, it.Width, it.Height); + + public static Gdk.Rectangle ToNative(this Rectangle it) + => new Gdk.Rectangle((int) it.X, (int) it.Y, (int) it.Width, (int) it.Height); + + public static Gdk.Rectangle ToNative(this RectangleF it) + => new Gdk.Rectangle((int) it.X, (int) it.Y, (int) it.Width, (int) it.Height); + + public static Point ToPoint(this Gdk.Point it) + => new Point(it.X, it.Y); + + public static PointF ToPointF(this Gdk.Point it) + => new PointF(it.X, it.Y); + + public static PointF ToPointF(this Cairo.PointD it) + => new PointF((float) it.X, (float) it.Y); + + public static Gdk.Point ToNative(this Point it) + => new Gdk.Point((int) it.X, (int) it.Y); + + public static Gdk.Point ToNative(this PointF it) + => new Gdk.Point((int) it.X, (int) it.Y); + + public static Size ToSize(this Gdk.Size it) + => new Size(it.Width, it.Height); + + public static SizeF ToSizeF(this Gdk.Size it) + => new SizeF(it.Width, it.Height); + + public static Gdk.Size ToNative(this Size it) + => new Gdk.Size((int) it.Width, (int) it.Height); + + public static Gdk.Size ToNative(this SizeF it) + => new Gdk.Size((int) it.Width, (int) it.Height); + + public static double ScaledFromPango(this int it) + => Math.Ceiling(it / Pango.Scale.PangoScale); + + public static float ScaledFromPangoF(this int it) + => (float) Math.Ceiling(it / Pango.Scale.PangoScale); + + public static int ScaledToPango(this double it) + => (int) Math.Ceiling(it * Pango.Scale.PangoScale); + + public static int ScaledToPango(this float it) + => (int) Math.Ceiling(it * Pango.Scale.PangoScale); + + public static int ScaledToPango(this int it) + => (int) Math.Ceiling(it * Pango.Scale.PangoScale); + + } + +} diff --git a/src/Microsoft.Maui.Graphics.Gtk/Gtk/GtkBitmapExportContext.cs b/src/Microsoft.Maui.Graphics.Gtk/Gtk/GtkBitmapExportContext.cs new file mode 100644 index 0000000..98e5d62 --- /dev/null +++ b/src/Microsoft.Maui.Graphics.Gtk/Gtk/GtkBitmapExportContext.cs @@ -0,0 +1,65 @@ +using System.IO; +using System.Threading; + +namespace Microsoft.Maui.Graphics.Native.Gtk { + + public class GtkBitmapExportContext : BitmapExportContext { + + private NativeCanvas _canvas; + private Cairo.ImageSurface _surface; + private Cairo.Context _context; + private Gdk.Pixbuf? _pixbuf; + + public GtkBitmapExportContext(int width, int height, float dpi) : base(width, height, dpi) { + _surface = new Cairo.ImageSurface(Cairo.Format.Argb32, width, height); + _context = new Cairo.Context(_surface); + + _canvas = new NativeCanvas() { + Context = _context + }; + } + + public ImageFormat Format => ImageFormat.Png; + + public override ICanvas Canvas => _canvas; + + /// + /// writes a pixbuf to stream + /// + /// + public override void WriteToStream(Stream stream) { + if (_pixbuf != null) { + _pixbuf.SaveToStream(stream, Format); + } else { + _pixbuf = _surface.SaveToStream(stream, Format); + } + } + + private GtkImage? _image; + + public override IImage? Image { + get { + _pixbuf ??= _surface.CreatePixbuf(); + + if (_pixbuf != null) return _image ??= new GtkImage(_pixbuf); + + return _image; + } + } + + public override void Dispose() { + _canvas?.Dispose(); + _context?.Dispose(); + _surface?.Dispose(); + + if (_pixbuf != null) { + var previousValue = Interlocked.Exchange(ref _pixbuf, null); + previousValue?.Dispose(); + } + + base.Dispose(); + } + + } + +} diff --git a/src/Microsoft.Maui.Graphics.Gtk/Gtk/GtkImage.cs b/src/Microsoft.Maui.Graphics.Gtk/Gtk/GtkImage.cs new file mode 100644 index 0000000..0832ec4 --- /dev/null +++ b/src/Microsoft.Maui.Graphics.Gtk/Gtk/GtkImage.cs @@ -0,0 +1,57 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Maui.Graphics.Native.Gtk { + + public class GtkImage : IImage { + + public GtkImage(Gdk.Pixbuf pix) { + _pixbuf = pix; + } + + private Gdk.Pixbuf? _pixbuf; + + // https://developer.gnome.org/gdk-pixbuf/stable/gdk-pixbuf-The-GdkPixbuf-Structure.html + public Gdk.Pixbuf? NativeImage => _pixbuf; + + public void Draw(ICanvas canvas, RectangleF dirtyRect) { + canvas.DrawImage(this, dirtyRect.Left, dirtyRect.Top, (float) Math.Round(dirtyRect.Width), (float) Math.Round(dirtyRect.Height)); + } + + public void Dispose() { + var previousValue = Interlocked.Exchange(ref _pixbuf, null); + previousValue?.Dispose(); + } + + public float Width => NativeImage?.Width ?? 0; + + public float Height => NativeImage?.Width ?? 0; + + [GtkMissingImplementation] + public IImage Downsize(float maxWidthOrHeight, bool disposeOriginal = false) { + return this; + } + + [GtkMissingImplementation] + public IImage Downsize(float maxWidth, float maxHeight, bool disposeOriginal = false) { + return this; + } + + [GtkMissingImplementation] + public IImage Resize(float width, float height, ResizeMode resizeMode = ResizeMode.Fit, bool disposeOriginal = false) { + return this; + } + + public void Save(Stream stream, ImageFormat format = ImageFormat.Png, float quality = 1) { + NativeImage.SaveToStream(stream, format, quality); + } + + public async Task SaveAsync(Stream stream, ImageFormat format = ImageFormat.Png, float quality = 1) { + await Task.Run(() => NativeImage.SaveToStream(stream, format, quality)); + } + + } + +} diff --git a/src/Microsoft.Maui.Graphics.Gtk/Gtk/HardwareInformations.cs b/src/Microsoft.Maui.Graphics.Gtk/Gtk/HardwareInformations.cs new file mode 100644 index 0000000..e99efb3 --- /dev/null +++ b/src/Microsoft.Maui.Graphics.Gtk/Gtk/HardwareInformations.cs @@ -0,0 +1,55 @@ +using System.Collections.Generic; +using System.Linq; + +namespace Microsoft.Maui.Graphics.Native.Gtk { + + public static class HardwareInformations { + + public static Gdk.Screen DefaultScreen => Gdk.Screen.Default; + + /// + /// A describes a particular video hardware display format. + /// It includes information about the number of bits used for each color, the way the bits are translated into an RGB value for display, + /// and the way the bits are stored in memory. For example, a piece of display hardware might support 24-bit color, 16-bit color, or 8-bit color; + /// meaning 24/16/8-bit pixel sizes. For a given pixel size, pixels can be in different formats; + /// for example the “red” element of an RGB pixel may be in the top 8 bits of the pixel, or may be in the lower 4 bits. + /// There are several standard visuals. + /// The visual returned by is the system’s default visual, + /// and the visual returned by should be used for creating windows with an alpha channel. + /// + /// Get the system’s default visual for screen . + /// This is the visual for the root window of the display. + /// The return value should not be freed. + /// + public static Gdk.Visual SystemVisual => Gdk.Screen.Default.SystemVisual; + + public static double DefaultResolution => DefaultScreen.Resolution; + + /// + /// GdkDisplay objects purpose are two fold: + /// to manage and provide information about input devices (pointers and keyboards) + /// to manage and provide information about the available s. + /// 's are the GDK representation of an X Display, which can be described as a workstation consisting of a keyboard, + /// a pointing device (such as a mouse) and one or more screens. + /// It is used to open and keep track of various objects currently instantiated by the application. + /// It is also used to access the keyboard(s) and mouse pointer(s) of the display. + /// + public static Gdk.Display DefaultDisplay => Gdk.Display.Default; + + public static Gdk.Monitor CurrentMonitor => DefaultDisplay.GetMonitorAtPoint(0, 0); // TODO: find out aktual mouse position + + public static Gdk.Monitor PrimaryMonitor => GetMonitors().Single(m => m.IsPrimary); + + public static int CurrentScaleFaktor = CurrentMonitor.ScaleFactor; + + public static IEnumerable GetMonitors() { + + for (var i = 0; i < DefaultDisplay.NMonitors; i++) { + yield return DefaultDisplay.GetMonitor(i); + } + + } + + } + +} diff --git a/src/Microsoft.Maui.Graphics.Gtk/Gtk/ImageExtensions.cs b/src/Microsoft.Maui.Graphics.Gtk/Gtk/ImageExtensions.cs new file mode 100644 index 0000000..703e8b2 --- /dev/null +++ b/src/Microsoft.Maui.Graphics.Gtk/Gtk/ImageExtensions.cs @@ -0,0 +1,135 @@ +using System; +using System.IO; + +namespace Microsoft.Maui.Graphics.Native.Gtk { + + public static class ImageExtensions { + + public static string ToImageExtension(this ImageFormat imageFormat) => + imageFormat switch { + ImageFormat.Bmp => "bmp", + ImageFormat.Png => "png", + ImageFormat.Jpeg => "jpeg", + ImageFormat.Gif => "gif", + ImageFormat.Tiff => "tiff", + _ => throw new ArgumentOutOfRangeException(nameof(imageFormat), imageFormat, null) + }; + + public static Gdk.Pixbuf? SaveToStream(this Cairo.ImageSurface? surface, Stream stream, ImageFormat format = ImageFormat.Png, float quality = 1) { + if (surface == null) + return null; + try { + var px = surface.CreatePixbuf(); + SaveToStream(px, stream, format, quality); + + return px; + } catch (Exception ex) { + Logger.Error(ex); + + return default; + } + } + + public static bool SaveToStream(this Gdk.Pixbuf? pixbuf, Stream stream, ImageFormat format = ImageFormat.Png, float quality = 1) { + if (pixbuf == null) + return false; + + try { + var puf = pixbuf.SaveToBuffer(format.ToImageExtension()); + stream.Write(puf, 0, puf.Length); + puf = null; + } catch (Exception ex) { + Logger.Error(ex); + + return false; + } + + return true; + } + + public static Gdk.Pixbuf? CreatePixbuf(this Cairo.ImageSurface? surface) { + if (surface == null) + return null; + + var surfaceData = surface.Data; + var nbytes = surface.Format == Cairo.Format.Argb32 ? 4 : 3; + var pixData = new byte[surfaceData.Length / 4 * nbytes]; + + var i = 0; + var n = 0; + var stride = surface.Stride; + var ncols = surface.Width; + + if (BitConverter.IsLittleEndian) { + var row = surface.Height; + + while (row-- > 0) { + var prevPos = n; + var col = ncols; + + while (col-- > 0) { + var alphaFactor = nbytes == 4 ? 255d / surfaceData[n + 3] : 1; + pixData[i] = (byte) (surfaceData[n + 2] * alphaFactor + 0.5); + pixData[i + 1] = (byte) (surfaceData[n + 1] * alphaFactor + 0.5); + pixData[i + 2] = (byte) (surfaceData[n + 0] * alphaFactor + 0.5); + + if (nbytes == 4) + pixData[i + 3] = surfaceData[n + 3]; + + n += 4; + i += nbytes; + } + + n = prevPos + stride; + } + } else { + var row = surface.Height; + + while (row-- > 0) { + var prevPos = n; + var col = ncols; + + while (col-- > 0) { + var alphaFactor = nbytes == 4 ? 255d / surfaceData[n + 3] : 1; + pixData[i] = (byte) (surfaceData[n + 1] * alphaFactor + 0.5); + pixData[i + 1] = (byte) (surfaceData[n + 2] * alphaFactor + 0.5); + pixData[i + 2] = (byte) (surfaceData[n + 3] * alphaFactor + 0.5); + + if (nbytes == 4) + pixData[i + 3] = surfaceData[n + 0]; + + n += 4; + i += nbytes; + } + + n = prevPos + stride; + } + } + + return new Gdk.Pixbuf(pixData, Gdk.Colorspace.Rgb, nbytes == 4, 8, surface.Width, surface.Height, surface.Width * nbytes, null); + } + + public static Cairo.Pattern? CreatePattern(this Gdk.Pixbuf? pixbuf, double scaleFactor) { + if (pixbuf == null) + return null; + + using var surface = new Cairo.ImageSurface(Cairo.Format.Argb32, (int) (pixbuf.Width * scaleFactor), (int) (pixbuf.Height * scaleFactor)); + using var context = new Cairo.Context(surface); + context.Scale(surface.Width / (double) pixbuf.Width, surface.Height / (double) pixbuf.Height); + Gdk.CairoHelper.SetSourcePixbuf(context, pixbuf, 0, 0); + context.Paint(); + surface.Flush(); + + var pattern = new Cairo.SurfacePattern(surface); + + var matrix = new Cairo.Matrix(); + matrix.Scale(scaleFactor, scaleFactor); + pattern.Matrix = matrix; + + return pattern; + + } + + } + +} diff --git a/src/Microsoft.Maui.Graphics.Gtk/Gtk/NativeCanvas.Context.cs b/src/Microsoft.Maui.Graphics.Gtk/Gtk/NativeCanvas.Context.cs new file mode 100644 index 0000000..8db8834 --- /dev/null +++ b/src/Microsoft.Maui.Graphics.Gtk/Gtk/NativeCanvas.Context.cs @@ -0,0 +1,181 @@ +using System; + +namespace Microsoft.Maui.Graphics.Native.Gtk { + + public partial class NativeCanvas { + + private Cairo.Surface CreateSurface(Cairo.Context context, bool imageSurface = false) { + var surface = context.GetTarget(); + + var extents = context.PathExtents(); + var pathSize = new Size(extents.X + extents.Width, extents.Height + extents.Y); + + var s = surface.GetSize(); + + var shadowSurface = s.HasValue && !imageSurface ? + surface.CreateSimilar(surface.Content, (int) pathSize.Width, (int) pathSize.Height) : + new Cairo.ImageSurface(Cairo.Format.ARGB32, (int) pathSize.Width, (int) pathSize.Height); + + return shadowSurface; + + } + + private void AddLine(Cairo.Context context, float x1, float y1, float x2, float y2) { + context.MoveTo(x1, y1); + context.LineTo(x2, y2); + } + + private void AddArc(Cairo.Context context, float x, float y, float width, float height, float startAngle, float endAngle, bool clockwise, bool closed) { + + AddArc(context, x, y, width, height, startAngle, endAngle, clockwise); + + if (closed) + context.ClosePath(); + } + + private void AddRectangle(Cairo.Context context, float x, float y, float width, float height) { + context.Rectangle(x, y, width, height); + } + + /// + /// degree-value * mRadians = radians + /// + private const double mRadians = System.Math.PI / 180d; + + private void AddRoundedRectangle(Cairo.Context context, float left, float top, float width, float height, float radius) { + + context.NewPath(); + // top left + context.Arc(left + radius, top + radius, radius, 180 * mRadians, 270 * mRadians); + // // top right + context.Arc(left + width - radius, top + radius, radius, 270 * mRadians, 0); + // // bottom right + context.Arc(left + width - radius, top + height - radius, radius, 0, 90 * mRadians); + // // bottom left + context.Arc(left + radius, top + height - radius, radius, 90 * mRadians, 180 * mRadians); + context.ClosePath(); + } + + public void AddEllipse(Cairo.Context context, float x, float y, float width, float height) { + context.Save(); + context.NewPath(); + + context.Translate(x + width / 2, y + height / 2); + context.Scale(width / 2f, height / 2f); + context.Arc(0, 0, 1, 0, 2 * Math.PI); + context.Restore(); + } + + private void AddArc(Cairo.Context context, float x, float y, float width, float height, float startAngle, float endAngle, bool clockwise) { + + // https://developer.gnome.org/cairo/stable/cairo-Paths.html#cairo-arc + // Angles are measured in radians + + var startAngleInRadians = startAngle * -mRadians; + var endAngleInRadians = endAngle * -mRadians; + + var cx = x + width / 2f; + var cy = y + height / 2f; + + var r = 1; + + context.Save(); + + context.Translate(cx, cy); + context.Scale(width / 2f, height / 2f); + + if (clockwise) + context.Arc(0, 0, r, startAngleInRadians, endAngleInRadians); + else { + context.ArcNegative(0, 0, r, startAngleInRadians, endAngleInRadians); + } + + context.Restore(); + + } + + private void AddPath(Cairo.Context context, PathF target) { + var pointIndex = 0; + var arcAngleIndex = 0; + var arcClockwiseIndex = 0; + + foreach (var type in target.SegmentTypes) { + if (type == PathOperation.Move) { + var point = target[pointIndex++]; + context.MoveTo(point.X, point.Y); + } else if (type == PathOperation.Line) { + var endPoint = target[pointIndex++]; + context.LineTo(endPoint.X, endPoint.Y); + + } else if (type == PathOperation.Quad) { + var p1 = pointIndex > 0 ? target[pointIndex - 1] : context.CurrentPoint.ToPointF(); + var c = target[pointIndex++]; + var p2 = target[pointIndex++]; + + // quad bezier to cubic bezier: + // C1 = 2/3•C + 1/3•P1 + // C2 = 2/3•C + 1/3•P2 + + var c1 = new PointF(c.X * 2 / 3 + p1.X / 3, c.Y * 2 / 3 + p1.Y / 3); + var c2 = new PointF(c.X * 2 / 3 + p2.X / 3, c.Y * 2 / 3 + p2.Y / 3); + + // Adds a cubic Bézier spline to the path + context.CurveTo( + c1.X, c1.Y, + c2.X, c2.Y, + p2.X, p2.Y); + + } else if (type == PathOperation.Cubic) { + var controlPoint1 = target[pointIndex++]; + var controlPoint2 = target[pointIndex++]; + var endPoint = target[pointIndex++]; + + // https://developer.gnome.org/cairo/stable/cairo-Paths.html#cairo-curve-to + // Adds a cubic Bézier spline to the path from the current point to position (x3, y3) in user-space coordinates, + // using (x1, y1) and (x2, y2) as the control points. After this call the current point will be (x3, y3). + // If there is no current point before the call to cairo_curve_to() this function will behave as if preceded by a call to cairo_move_to(cr, x1, y1). + context.CurveTo( + controlPoint1.X, controlPoint1.Y, + controlPoint2.X, controlPoint2.Y, + endPoint.X, endPoint.Y); + + } else if (type == PathOperation.Arc) { + var topLeft = target[pointIndex++]; + var bottomRight = target[pointIndex++]; + var startAngle = target.GetArcAngle(arcAngleIndex++); + var endAngle = target.GetArcAngle(arcAngleIndex++); + var clockwise = target.GetArcClockwise(arcClockwiseIndex++); + + AddArc(context, topLeft.X, topLeft.Y, bottomRight.X - topLeft.X, bottomRight.Y - topLeft.Y, startAngle, endAngle, clockwise); + + } else if (type == PathOperation.Close) { + context.ClosePath(); + } + } + } + + public void DrawPixbuf(Cairo.Context context, Gdk.Pixbuf pixbuf, double x, double y, double width, double height) { + context.Save(); + context.Translate(x, y); + + context.Scale(width / pixbuf.Width, height / pixbuf.Height); + Gdk.CairoHelper.SetSourcePixbuf(context, pixbuf, 0, 0); + + using (var p = context.GetSource()) { + if (p is Cairo.SurfacePattern pattern) { + if (width > pixbuf.Width || height > pixbuf.Height) { + // Fixes blur issue when rendering on an image surface + pattern.Filter = Cairo.Filter.Fast; + } else + pattern.Filter = Cairo.Filter.Good; + } + } + + context.Paint(); + + context.Restore(); + } + + } + +} diff --git a/src/Microsoft.Maui.Graphics.Gtk/Gtk/NativeCanvas.Patterns.cs b/src/Microsoft.Maui.Graphics.Gtk/Gtk/NativeCanvas.Patterns.cs new file mode 100644 index 0000000..36f44f9 --- /dev/null +++ b/src/Microsoft.Maui.Graphics.Gtk/Gtk/NativeCanvas.Patterns.cs @@ -0,0 +1,122 @@ +using System; + +namespace Microsoft.Maui.Graphics.Native.Gtk { + + public partial class NativeCanvas { + + public void DrawFillPaint(Cairo.Context? context, Paint? paint, RectangleF rectangle) { + if (paint == null || context == null) + return; + + switch (paint) { + + case SolidPaint solidPaint: { + FillColor = solidPaint.Color; + + break; + } + + case LinearGradientPaint linearGradientPaint: { + try { + if (linearGradientPaint.GetCairoPattern(rectangle, DisplayScale) is { } pattern) { + context.SetSource(pattern); + pattern.Dispose(); + } else { + FillColor = paint.BackgroundColor; + } + } catch (Exception exc) { + Logger.Debug(exc); + FillColor = linearGradientPaint.BlendStartAndEndColors(); + } + + break; + } + + case RadialGradientPaint radialGradientPaint: { + + try { + if (radialGradientPaint.GetCairoPattern(rectangle, DisplayScale) is { } pattern) { + context.SetSource(pattern); + pattern.Dispose(); + } else { + FillColor = paint.BackgroundColor; + } + } catch (Exception exc) { + Logger.Debug(exc); + FillColor = radialGradientPaint.BlendStartAndEndColors(); + } + + break; + } + + case PatternPaint patternPaint: { + try { + +#if UseSurfacePattern + // would be nice to have: draw pattern without creating a pixpuf: + + using var paintSurface = CreateSurface(context, true); + + if (patternPaint.GetCairoPattern(paintSurface, DisplayScale) is { } pattern) { + pattern.Extend = Cairo.Extend.Repeat; + context.SetSource(pattern); + pattern.Dispose(); + + + } else { + FillColor = paint.BackgroundColor; + } +#else + using var pixbuf = patternPaint.GetPatternBitmap(DisplayScale); + + if (pixbuf?.CreatePattern(DisplayScale) is { } pattern) { + pattern.Extend = Cairo.Extend.Repeat; + context.SetSource(pattern); + pattern.Dispose(); + } + +#endif + + } catch (Exception exc) { + Logger.Debug(exc); + FillColor = paint.BackgroundColor; + } + + break; + } + + case ImagePaint {Image: GtkImage image} imagePaint: { + var pixbuf = image.NativeImage; + + if (pixbuf?.CreatePattern(DisplayScale) is { } pattern) { + try { + + context.SetSource(pattern); + pattern.Dispose(); + + } catch (Exception exc) { + Logger.Debug(exc); + FillColor = paint.BackgroundColor; + } + } else { + FillColor = paint.BackgroundColor ?? Colors.White; + } + + break; + } + + case ImagePaint imagePaint: + FillColor = paint.BackgroundColor ?? Colors.White; + + break; + + default: + FillColor = paint.BackgroundColor ?? Colors.White; + + break; + } + } + + } + +} diff --git a/src/Microsoft.Maui.Graphics.Gtk/Gtk/NativeCanvas.Shadow.cs b/src/Microsoft.Maui.Graphics.Gtk/Gtk/NativeCanvas.Shadow.cs new file mode 100644 index 0000000..902bb6b --- /dev/null +++ b/src/Microsoft.Maui.Graphics.Gtk/Gtk/NativeCanvas.Shadow.cs @@ -0,0 +1,49 @@ +namespace Microsoft.Maui.Graphics.Native.Gtk { + + public partial class NativeCanvas { + + public void DrawShadow(bool fill) { + + if (CurrentState.Shadow != default) { + + using var path = Context.CopyPath(); + + Context.Save(); + + var shadowSurface = CreateSurface(Context); + + var shadowCtx = new Cairo.Context(shadowSurface); + + var shadow = CurrentState.Shadow; + + shadowCtx.AppendPath(path); + + if (fill) + shadowCtx.ClosePath(); + + var color = shadow.color.ToCairoColor(); + shadowCtx.SetSourceRGBA(color.R, color.G, color.B, color.A); + shadowCtx.Clip(); + + if (true) + shadowCtx.PaintWithAlpha(0.3); + else { + shadowCtx.LineWidth = 10; + shadowCtx.Stroke(); + } + + // shadowCtx.PopGroupToSource(); + Context.SetSource(shadowSurface, shadow.offset.Width, shadow.offset.Height); + Context.Paint(); + + shadowCtx.Dispose(); + + shadowSurface.Dispose(); + + Context.Restore(); + } + } + + } + +} diff --git a/src/Microsoft.Maui.Graphics.Gtk/Gtk/NativeCanvas.Text.cs b/src/Microsoft.Maui.Graphics.Gtk/Gtk/NativeCanvas.Text.cs new file mode 100644 index 0000000..3000945 --- /dev/null +++ b/src/Microsoft.Maui.Graphics.Gtk/Gtk/NativeCanvas.Text.cs @@ -0,0 +1,54 @@ +using Microsoft.Maui.Graphics.Text; + +namespace Microsoft.Maui.Graphics.Native.Gtk { + + public partial class NativeCanvas { + + public TextLayout CreateTextLayout() { + var layout = new TextLayout(Context) + .WithCanvasState(CurrentState); + + layout.BeforeDrawn = LayoutBeforeDrawn; + layout.AfterDrawn = LayoutAfterDrawn; + + return layout; + } + + private void LayoutBeforeDrawn(TextLayout layout) { + DrawFillPaint(Context, CurrentState.FillPaint.paint, CurrentState.FillPaint.rectangle); + } + + private void LayoutAfterDrawn(TextLayout layout) { } + + public override void DrawString(string value, float x, float y, HorizontalAlignment horizontalAlignment) { + + using var layout = CreateTextLayout(); + layout.HorizontalAlignment = horizontalAlignment; + + layout.DrawString(value, x, y); + + } + + public override void DrawString(string value, float x, float y, float width, float height, HorizontalAlignment horizontalAlignment, VerticalAlignment verticalAlignment, TextFlow textFlow = TextFlow.ClipBounds, float lineSpacingAdjustment = 0) { + + using var layout = CreateTextLayout(); + layout.HorizontalAlignment = horizontalAlignment; + layout.VerticalAlignment = verticalAlignment; + layout.TextFlow = textFlow; + layout.LineSpacingAdjustment = lineSpacingAdjustment; + + layout.DrawString(value, x, y, width, height); + + } + + [GtkMissingImplementation] + public override void DrawText(IAttributedText value, float x, float y, float width, float height) { + using var layout = CreateTextLayout(); + + layout.DrawAttributedText(value, x, y, width, height); + + } + + } + +} diff --git a/src/Microsoft.Maui.Graphics.Gtk/Gtk/NativeCanvas.cs b/src/Microsoft.Maui.Graphics.Gtk/Gtk/NativeCanvas.cs new file mode 100644 index 0000000..a0a0440 --- /dev/null +++ b/src/Microsoft.Maui.Graphics.Gtk/Gtk/NativeCanvas.cs @@ -0,0 +1,236 @@ +namespace Microsoft.Maui.Graphics.Native.Gtk { + + public partial class NativeCanvas : AbstractCanvas { + + public NativeCanvas() : base(CreateNewState, CreateStateCopy) { } + + private Cairo.Context _context; + + public Cairo.Context Context { + get => _context; + set { + _context = default; + ResetState(); + _context = value; + } + } + + private static NativeCanvasState CreateNewState(object context) { + return new NativeCanvasState { }; + } + + private static NativeCanvasState CreateStateCopy(NativeCanvasState prototype) { + return new NativeCanvasState(prototype); + } + + public override void SaveState() { + Context?.Save(); + base.SaveState(); + } + + public override bool RestoreState() { + Context?.Restore(); + + return base.RestoreState(); + } + + public override bool Antialias { + set => CurrentState.Antialias = CanvasExtensions.ToAntialias(value); + } + + public override float MiterLimit { + set => CurrentState.MiterLimit = value; + } + + public override Color StrokeColor { + set => CurrentState.StrokeColor = value.ToCairoColor(); + } + + public override LineCap StrokeLineCap { + set => CurrentState.LineCap = value.ToLineCap(); + } + + public override LineJoin StrokeLineJoin { + set => CurrentState.LineJoin = value.ToLineJoin(); + } + + protected override float NativeStrokeSize { + set => CurrentState.StrokeSize = value; + } + + public override Color FillColor { + set => CurrentState.FillColor = value.ToCairoColor(); + } + + public override Color FontColor { + set => CurrentState.FontColor = value.ToCairoColor(); + } + + public override string FontName { + set => CurrentState.FontName = value; + } + + public override float FontSize { + set => CurrentState.FontSize = value; + } + + public override float Alpha { + set => CurrentState.Alpha = value; + } + + public override BlendMode BlendMode { + set => CurrentState.BlendMode = value; + } + + protected override void NativeSetStrokeDashPattern(float[] pattern, float strokeSize) { + CurrentState.StrokeDashPattern = pattern; + } + + private void Draw(bool preserve = false) { + Context.SetSourceRGBA(CurrentState.StrokeColor.R, CurrentState.StrokeColor.G, CurrentState.StrokeColor.B, CurrentState.StrokeColor.A * CurrentState.Alpha); + Context.LineWidth = CurrentState.StrokeSize; + Context.MiterLimit = CurrentState.MiterLimit; + Context.LineCap = CurrentState.LineCap; + Context.LineJoin = CurrentState.LineJoin; + + Context.SetDash(CurrentState.NativeDash, 0); + DrawShadow(false); + + if (preserve) + Context.StrokePreserve(); + else { + Context.Stroke(); + } + } + + public void Fill(bool preserve = false) { + + Context.SetSourceRGBA(CurrentState.FillColor.R, CurrentState.FillColor.G, CurrentState.FillColor.B, CurrentState.FillColor.A * CurrentState.Alpha); + + DrawShadow(true); + + DrawFillPaint(Context, CurrentState.FillPaint.paint, CurrentState.FillPaint.rectangle); + + if (preserve) { + Context.FillPreserve(); + } else { + Context.Fill(); + CurrentState.FillPaint = default; + } + + } + + protected override void NativeDrawLine(float x1, float y1, float x2, float y2) { + AddLine(Context, x1, y1, x2, y2); + Draw(); + } + + protected override void NativeDrawArc(float x, float y, float width, float height, float startAngle, float endAngle, bool clockwise, bool closed) { + AddArc(Context, x, y, width, height, startAngle, endAngle, clockwise, closed); + Draw(); + + } + + protected override void NativeDrawRectangle(float x, float y, float width, float height) { + AddRectangle(Context, x, y, width, height); + Draw(); + } + + protected override void NativeDrawRoundedRectangle(float x, float y, float width, float height, float radius) { + AddRoundedRectangle(Context, x, y, width, height, radius); + Draw(); + } + + protected override void NativeDrawEllipse(float x, float y, float width, float height) { + AddEllipse(Context, x, y, width, height); + Draw(); + } + + protected override void NativeDrawPath(PathF path) { + AddPath(Context, path); + Draw(); + } + + protected override void NativeRotate(float degrees, float radians, float x, float y) { + Context.Translate(x, y); + Context.Rotate(radians); + Context.Translate(-x, -y); + } + + protected override void NativeRotate(float degrees, float radians) { + Context.Rotate(radians); + } + + protected override void NativeScale(float fx, float fy) { + Context.Scale(fx, fy); + } + + protected override void NativeTranslate(float tx, float ty) { + Context.Translate(tx, ty); + } + + [GtkMissingImplementation] + protected override void NativeConcatenateTransform(AffineTransform transform) { } + + public override void SetShadow(SizeF offset, float blur, Color color) { + CurrentState.Shadow = (offset, blur, color); + } + + public override void SetFillPaint(Paint paint, RectangleF rectangle) { + CurrentState.FillPaint = (paint, rectangle); + } + + public override void FillArc(float x, float y, float width, float height, float startAngle, float endAngle, bool clockwise) { + AddArc(Context, x, y, width, height, startAngle, endAngle, clockwise, true); + Fill(); + } + + public override void FillRectangle(float x, float y, float width, float height) { + AddRectangle(Context, x, y, width, height); + Fill(); + } + + public override void FillRoundedRectangle(float x, float y, float width, float height, float cornerRadius) { + AddRoundedRectangle(Context, x, y, width, height, cornerRadius); + Fill(); + } + + public override void FillEllipse(float x, float y, float width, float height) { + AddEllipse(Context, x, y, width, height); + Fill(); + } + + public override void FillPath(PathF path, WindingMode windingMode) { + Context.Save(); + Context.FillRule = windingMode.ToFillRule(); + AddPath(Context, path); + Fill(); + Context.Restore(); + } + + public override void DrawImage(IImage image, float x, float y, float width, float height) { + if (image is GtkImage {NativeImage: { } pixbuf}) { + DrawPixbuf(Context, pixbuf, x, y, width, height); + } + } + + public override void SetToSystemFont() { + CurrentState.FontName = NativeFontService.Instance.SystemFontName; + } + + public override void SetToBoldSystemFont() { + CurrentState.FontName = NativeFontService.Instance.BoldSystemFontName; + } + + [GtkMissingImplementation] + public override void SubtractFromClip(float x, float y, float width, float height) { } + + [GtkMissingImplementation] + public override void ClipPath(PathF path, WindingMode windingMode = WindingMode.NonZero) { } + + [GtkMissingImplementation] + public override void ClipRectangle(float x, float y, float width, float height) { } + + } + +} diff --git a/src/Microsoft.Maui.Graphics.Gtk/Gtk/NativeCanvasState.cs b/src/Microsoft.Maui.Graphics.Gtk/Gtk/NativeCanvasState.cs new file mode 100644 index 0000000..e389d5a --- /dev/null +++ b/src/Microsoft.Maui.Graphics.Gtk/Gtk/NativeCanvasState.cs @@ -0,0 +1,86 @@ +using System; + +namespace Microsoft.Maui.Graphics.Native.Gtk { + + public class NativeCanvasState : CanvasState { + + public NativeCanvasState() { + Alpha = 1; + + StrokeColor = Colors.Black.ToCairoColor(); + FontColor = StrokeColor; + FillColor = Colors.White.ToCairoColor(); + + MiterLimit = 10; + LineJoin = Cairo.LineJoin.Miter; + LineCap = Cairo.LineCap.Butt; + } + + public NativeCanvasState(NativeCanvasState prototype) { + + StrokeDashPattern = prototype.StrokeDashPattern; + StrokeSize = prototype.StrokeSize; + Scale = prototype.Scale; + Transform = prototype.Transform; + + Antialias = prototype.Antialias; + MiterLimit = prototype.MiterLimit; + StrokeColor = prototype.StrokeColor; + LineCap = prototype.LineCap; + LineJoin = prototype.LineJoin; + FillColor = prototype.FillColor; + + FontName = prototype.FontName; + FontSize = prototype.FontSize; + FontColor = prototype.FontColor; + + BlendMode = prototype.BlendMode; + Alpha = prototype.Alpha; + + Shadow = prototype.Shadow; + FillPaint = prototype.FillPaint; + + } + + public Cairo.Antialias Antialias { get; set; } + + public double MiterLimit { get; set; } + + public Cairo.Color StrokeColor { get; set; } + + public Cairo.LineCap LineCap { get; set; } + + public Cairo.LineJoin LineJoin { get; set; } + + public Cairo.Color FillColor { get; set; } + + public Cairo.Color FontColor { get; set; } + + public string FontName { get; set; } + + public float FontSize { get; set; } + + public BlendMode BlendMode { get; set; } + + public float Alpha { get; set; } + + private readonly double[] zerodash = new double[0]; + + public double[] NativeDash => StrokeDashPattern != null ? Array.ConvertAll(StrokeDashPattern, f => (double) f) : zerodash; + + public (SizeF offset, float blur, Color color) Shadow { get; set; } + + public (Paint paint, RectangleF rectangle) FillPaint { get; set; } + + public override void Dispose() { + + FillPaint = default; + Shadow = default; + StrokeDashPattern = default; + + base.Dispose(); + } + + } + +} diff --git a/src/Microsoft.Maui.Graphics.Gtk/Gtk/NativeFontFamily.cs b/src/Microsoft.Maui.Graphics.Gtk/Gtk/NativeFontFamily.cs new file mode 100644 index 0000000..e35bd98 --- /dev/null +++ b/src/Microsoft.Maui.Graphics.Gtk/Gtk/NativeFontFamily.cs @@ -0,0 +1,63 @@ +using System; +using System.Linq; + +namespace Microsoft.Maui.Graphics.Native.Gtk { + + public class NativeFontFamily : IFontFamily, IComparable, IComparable { + + private readonly string _name; + private IFontStyle[]? _fontStyles; + + public NativeFontFamily(string name) { + _name = name; + + } + + public string Name => _name; + + public IFontStyle[] GetFontStyles() { + return _fontStyles ??= NativeFontService.Instance.GetFontStylesFor(this).ToArray(); + } + + private IFontStyle[] GetAvailableFontStyles() { + return GetFontStyles(); + + } + + public override bool Equals(object obj) { + if (obj == null) + return false; + + if (ReferenceEquals(this, obj)) + return true; + + if (obj.GetType() != typeof(NativeFontFamily)) + return false; + + var other = (NativeFontFamily) obj; + + return _name == other._name; + } + + public override int GetHashCode() { + return _name != null ? _name.GetHashCode() : 0; + } + + public override string ToString() { + return Name; + } + + public int CompareTo(IFontFamily other) { + return string.Compare(_name, other.Name, StringComparison.Ordinal); + } + + public int CompareTo(object obj) { + if (obj is IFontFamily other) + return CompareTo(other); + + return -1; + } + + } + +} diff --git a/src/Microsoft.Maui.Graphics.Gtk/Gtk/NativeFontService.cs b/src/Microsoft.Maui.Graphics.Gtk/Gtk/NativeFontService.cs new file mode 100644 index 0000000..8773450 --- /dev/null +++ b/src/Microsoft.Maui.Graphics.Gtk/Gtk/NativeFontService.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Microsoft.Maui.Graphics.Native.Gtk { + + public class NativeFontService : AbstractFontService { + + public static NativeFontService Instance = new NativeFontService(); + + private static Pango.Context? _systemContext; + + public Pango.Context SystemContext => _systemContext ??= Gdk.PangoHelper.ContextGet(); + + private Pango.FontDescription? _systemFontDescription; + + public Pango.FontDescription SystemFontDescription => _systemFontDescription ??= SystemContext.FontDescription; + + private string? _systemFontName; + + public string SystemFontName => _systemFontName ??= $"{SystemFontDescription.Family} {SystemFontDescription.GetSize()}"; + + private string? _boldSystemFontName; + + public string BoldSystemFontName => _boldSystemFontName ??= $"{SystemFontDescription.Family} {SystemFontDescription.GetSize()} bold"; + + private IFontFamily[]? _fontFamilies; + + public override IFontFamily[] GetFontFamilies() + => _fontFamilies ??= SystemContext.FontMap?.Families?.Select(fam => fam.ToFontFamily()).OrderBy(f => f.Name).ToArray() ?? Array.Empty(); + + public IEnumerable GetFontStylesFor(IFontFamily family) { + var fam = SystemContext.FontMap?.Families?.FirstOrDefault(f => f.Name == family.Name); + + if (fam == null) yield break; + + foreach (var s in fam.GetAvailableFontStyles()) { + yield return s; + } + + } + + private IEnumerable<(Pango.FontFamily family, Pango.FontDescription description)> GetAvailableFamilyFaces(Pango.FontFamily? family) { + + if (family == default) yield break; + + foreach (var face in family.Faces) + yield return (family, face.Describe()); + + } + + } + +} diff --git a/src/Microsoft.Maui.Graphics.Gtk/Gtk/NativeFontStyle.cs b/src/Microsoft.Maui.Graphics.Gtk/Gtk/NativeFontStyle.cs new file mode 100644 index 0000000..170777d --- /dev/null +++ b/src/Microsoft.Maui.Graphics.Gtk/Gtk/NativeFontStyle.cs @@ -0,0 +1,76 @@ +using System; +using Pango; + +namespace Microsoft.Maui.Graphics.Native.Gtk { + + public class NativeFontStyle : IFontStyle { + + private NativeFontFamily? _family; + + public NativeFontStyle(string family, double size, int weight, Pango.Stretch stretch, FontStyleType styleType) { + FamilyName = family; + Size = size; + Weight = weight; + StyleType = styleType; + Stretch = stretch; + } + + protected Stretch Stretch { get; } + + protected string FamilyName { get; } + + public IFontFamily FontFamily => _family ??= new NativeFontFamily(FamilyName); + + public string Id => $"{FamilyName} {Size:N0} {StyleType.ToPangoStyle()}"; + + public double Size { get; } + + public string Name => Id; + + public string FullName => Id; + + public int Weight { get; } + + public FontStyleType StyleType { get; } + + public override bool Equals(object obj) { + if (obj == null) + return false; + + if (ReferenceEquals(this, obj)) + return true; + + if (obj.GetType() != typeof(NativeFontStyle)) + return false; + + var other = (NativeFontStyle) obj; + + return Id == other.Id; + } + + public override int GetHashCode() { + return Id != null ? Id.GetHashCode() : 0; + } + + public override string ToString() { + return Name; + } + + public int CompareTo(IFontStyle other) { + if (Name.Equals("Regular") || Name.Equals("Plain") || Name.Equals("Normal")) { + return -1; + } else if (other.Name.Equals("Regular") || other.Name.Equals("Plain") || other.Name.Equals("Normal")) { + return 1; + } + + return string.Compare(Name, other.Name, StringComparison.Ordinal); + } + + [GtkMissingImplementation] + public System.IO.Stream OpenStream() { + throw new NotImplementedException(); + } + + } + +} diff --git a/src/Microsoft.Maui.Graphics.Gtk/Gtk/NativeGraphicsService.cs b/src/Microsoft.Maui.Graphics.Gtk/Gtk/NativeGraphicsService.cs new file mode 100644 index 0000000..ba7a3d1 --- /dev/null +++ b/src/Microsoft.Maui.Graphics.Gtk/Gtk/NativeGraphicsService.cs @@ -0,0 +1,70 @@ +using System.IO; + +namespace Microsoft.Maui.Graphics.Native.Gtk { + + public class NativeGraphicsService : IGraphicsService { + + public static NativeGraphicsService Instance = new NativeGraphicsService(); + + private static Cairo.Context? _sharedContext; + + public Cairo.Context SharedContext { + get { + if (_sharedContext == null) { + using var sf = new Cairo.ImageSurface(Cairo.Format.ARGB32, 1, 1); + _sharedContext = new Cairo.Context(sf); + } + + return _sharedContext; + } + } + + public string SystemFontName => NativeFontService.Instance.SystemFontName; + + public string BoldSystemFontName => NativeFontService.Instance.BoldSystemFontName; + + private static TextLayout? _textLayout; + + public TextLayout SharedTextLayout => _textLayout ??= new TextLayout(SharedContext) { + HeightForWidth = true + }; + + public SizeF GetStringSize(string value, string fontName, float textWidth) { + if (string.IsNullOrEmpty(value)) + return new SizeF(); + + lock (SharedTextLayout) { + SharedTextLayout.FontFamily = fontName; + + return SharedTextLayout.GetSize(value, textWidth); + } + + } + + public SizeF GetStringSize(string value, string fontName, float textWidth, HorizontalAlignment horizontalAlignment, VerticalAlignment verticalAlignment) { + if (string.IsNullOrEmpty(value)) + return new SizeF(); + + lock (SharedTextLayout) { + SharedTextLayout.FontFamily = fontName; + SharedTextLayout.HorizontalAlignment = horizontalAlignment; + SharedTextLayout.VerticalAlignment = verticalAlignment; + + return SharedTextLayout.GetSize(value, textWidth); + } + } + + public IImage LoadImageFromStream(Stream stream, ImageFormat format = ImageFormat.Png) { + var px = new Gdk.Pixbuf(stream); + var img = new GtkImage(px); + + return img; + } + + public BitmapExportContext CreateBitmapExportContext(int width, int height, float displayScale = 1) { + return new GtkBitmapExportContext(width, height, displayScale); + } + + } + +} diff --git a/src/Microsoft.Maui.Graphics.Gtk/Gtk/PaintExtensions.cs b/src/Microsoft.Maui.Graphics.Gtk/Gtk/PaintExtensions.cs new file mode 100644 index 0000000..6346e47 --- /dev/null +++ b/src/Microsoft.Maui.Graphics.Gtk/Gtk/PaintExtensions.cs @@ -0,0 +1,114 @@ +using System; + +namespace Microsoft.Maui.Graphics.Native.Gtk { + + public static class PaintExtensions { + + public static Cairo.Context? PaintToSurface(this PatternPaint? it, Cairo.Surface? surface, float scale) { + if (surface == null || it == null) + return null; + + var context = new Cairo.Context(surface); + context.Scale(scale, scale); + + using var canv = new NativeCanvas { + Context = context, + }; + + it.Pattern.Draw(canv); + + return context; + + } + + /* + Cairo.Extend is used to describe how pattern color/alpha will be determined for areas "outside" the pattern's natural area, + (for example, outside the surface bounds or outside the gradient geometry). + The default extend mode is CAIRO_EXTEND_NONE for surface patterns and CAIRO_EXTEND_PAD for gradient patterns. + + NONE pixels outside of the source pattern are fully transparent + REPEAT the pattern is tiled by repeating + REFLECT the pattern is tiled by reflecting at the edges (Implemented for surface patterns since 1.6) + PAD pixels outside of the pattern copy the closest pixel from the source (only implemented for surface patterns since 1.6) + + */ + + public static void SetCairoExtend(Cairo.Extend it) { } + + public static Gdk.Pixbuf? GetPatternBitmap(this PatternPaint? it, float scale) { + if (it == null) + return null; + + using var surface = new Cairo.ImageSurface(Cairo.Format.Argb32, (int) it.Pattern.Width, (int) it.Pattern.Height); + using var context = it.PaintToSurface(surface, scale); + surface.Flush(); + + return surface.CreatePixbuf(); + + } + + /// + /// does not work, pattern isn't shown + /// + [GtkMissingImplementation] + public static Cairo.Pattern? GetCairoPattern(this PatternPaint? it, Cairo.Surface? surface, float scale) { + if (surface == null || it == null) + return null; + + using var context = it.PaintToSurface(surface, scale); + surface.Flush(); + + var pattern = new Cairo.SurfacePattern(surface); + + return pattern; + } + + public static Cairo.Pattern? GetCairoPattern(this LinearGradientPaint? it, RectangleF rectangle, float scaleFactor) { + if (it == null) + return null; + + var x1 = it.StartPoint.X * rectangle.Width + rectangle.X; + var y1 = it.StartPoint.Y * rectangle.Height + rectangle.Y; + + var x2 = it.EndPoint.X * rectangle.Width + rectangle.X; + var y2 = it.EndPoint.Y * rectangle.Height + rectangle.Y; + + // https://developer.gnome.org/cairo/stable/cairo-cairo-pattern-t.html#cairo-pattern-create-linear + var pattern = new Cairo.LinearGradient(x1, y1, x2, y2); + + foreach (var s in it.GetSortedStops()) { + pattern.AddColorStop(s.Offset, s.Color.ToCairoColor()); + } + + return pattern; + } + + public static Cairo.Pattern? GetCairoPattern(this RadialGradientPaint? it, RectangleF rectangle, float scaleFactor) { + if (it == null) + return null; + + var centerX = it.Center.X * rectangle.Width; + var centerY = it.Center.Y * rectangle.Height; + + var x1 = centerX + rectangle.X; + var y1 = centerY + rectangle.Y; + + var x2 = rectangle.Right - centerX; + var y2 = rectangle.Bottom - centerY; + + var radius1 = it.Radius * 1; + var radius2 = it.Radius * Math.Max(rectangle.Width, rectangle.Height); + + // https://developer.gnome.org/cairo/stable/cairo-cairo-pattern-t.html#cairo-pattern-create-radial + var pattern = new Cairo.RadialGradient(x1, y1, radius1, x2, y2, radius2); + + foreach (var s in it.GetSortedStops()) { + pattern.AddColorStop(s.Offset, s.Color.ToCairoColor()); + } + + return pattern; + } + + } + +} diff --git a/src/Microsoft.Maui.Graphics.Gtk/Gtk/TextLayout.cs b/src/Microsoft.Maui.Graphics.Gtk/Gtk/TextLayout.cs new file mode 100644 index 0000000..30fe37e --- /dev/null +++ b/src/Microsoft.Maui.Graphics.Gtk/Gtk/TextLayout.cs @@ -0,0 +1,328 @@ +using System; +using Microsoft.Maui.Graphics.Extras; +using Microsoft.Maui.Graphics.Text; +using Context = Cairo.Context; + +namespace Microsoft.Maui.Graphics.Native.Gtk { + + /// + /// Measures and draws text using + /// https://developer.gnome.org/pango/1.46/pango-Layout-Objects.html + /// https://developer.gnome.org/gdk3/stable/gdk3-Pango-Interaction.html + /// + public class TextLayout : IDisposable { + + private Context _context; + + public TextLayout(Context context) { + _context = context; + } + + public Context Context => _context; + + public string FontFamily { get; set; } + + public Pango.Weight Weight { get; set; } = Pango.Weight.Normal; + + public Pango.Style Style { get; set; } = Pango.Style.Normal; + + public int PangoFontSize { get; set; } = -1; + + private Pango.Layout? _layout; + private bool _layoutOwned = false; + + public TextFlow TextFlow { get; set; } = TextFlow.OverflowBounds; + + public HorizontalAlignment HorizontalAlignment { get; set; } + + public VerticalAlignment VerticalAlignment { get; set; } + + public LineBreakMode LineBreakMode { get; set; } = LineBreakMode.EndTruncation; + + public Cairo.Color TextColor { get; set; } + + public float LineSpacingAdjustment { get; set; } + + public bool HeightForWidth { get; set; } = true; + + public Action BeforeDrawn { get; set; } + + public Action AfterDrawn { get; set; } + + public void SetLayout(Pango.Layout value) { + _layout = value; + _layoutOwned = false; + } + + private Pango.FontDescription? _fontDescription; + private bool _fontDescriptionOwned = false; + + public Pango.FontDescription FontDescription { + get { + if (PangoFontSize == -1) { + PangoFontSize = NativeFontService.Instance.SystemFontDescription.Size; + } + + if (string.IsNullOrEmpty(FontFamily)) { + FontFamily = NativeFontService.Instance.SystemFontDescription.Family; + } + + if (_fontDescription == null) { + _fontDescription = new Pango.FontDescription { + Family = FontFamily, + Weight = Weight, + Style = Style, + Size = PangoFontSize + }; + + _fontDescriptionOwned = true; + + } + + return _fontDescription; + } + set { + _fontDescription = value; + _fontDescriptionOwned = false; + } + } + + public Pango.Layout GetLayout() { + if (_layout == null) { + _layout = Pango.CairoHelper.CreateLayout(Context); + _layoutOwned = true; + } + + if (_layout.FontDescription != FontDescription) { + _layout.FontDescription = FontDescription; + } + + // allign & justify per Size + _layout.Alignment = HorizontalAlignment.ToPango(); + _layout.Justify = HorizontalAlignment.HasFlag(HorizontalAlignment.Justified); + _layout.Wrap = LineBreakMode.ToPangoWrap(); + _layout.Ellipsize = LineBreakMode.ToPangoEllipsize(); + + // _layout.SingleParagraphMode = true; + return _layout; + } + + public void Dispose() { + if (_fontDescriptionOwned) { + _fontDescription?.Dispose(); + } + + if (_layoutOwned) { + _layout?.Dispose(); + } + } + + public (int width, int height) GetPixelSize(string text, double desiredSize = -1d) { + + var layout = GetLayout(); + + if (desiredSize > 0) { + if (HeightForWidth) { + layout.Width = desiredSize.ScaledToPango(); + } else { + layout.Height = desiredSize.ScaledToPango(); + } + } + + layout.SetText(text); + layout.GetPixelSize(out var textWidth, out var textHeight); + + return (textWidth, textHeight); + } + + private void Draw() { + if (_layout == null) + return; + + Context.SetSourceRGBA(TextColor.R, TextColor.G, TextColor.B, TextColor.A); + + BeforeDrawn?.Invoke(this); + + // https://developer.gnome.org/pango/1.46/pango-Cairo-Rendering.html#pango-cairo-show-layout + // Draws a PangoLayout in the specified cairo context. + // The top-left corner of the PangoLayout will be drawn at the current point of the cairo context. + Pango.CairoHelper.ShowLayout(Context, _layout); + } + + private float GetX(float x, int width) => HorizontalAlignment switch { + HorizontalAlignment.Left => x, + HorizontalAlignment.Right => x - width, + HorizontalAlignment.Center => x - width / 2f, + _ => x + }; + + private float GetY(float y, int height) => VerticalAlignment switch { + VerticalAlignment.Top => y, + VerticalAlignment.Center => y - height, + VerticalAlignment.Bottom => y - height / 2f, + _ => y + }; + + private float GetDx(int width) => HorizontalAlignment switch { + HorizontalAlignment.Left => 0, + HorizontalAlignment.Center => width / 2f, + HorizontalAlignment.Right => width, + _ => 0 + }; + + private float GetDy(int height) => VerticalAlignment switch { + VerticalAlignment.Top => 0, + VerticalAlignment.Center => height / 2f, + VerticalAlignment.Bottom => height, + _ => 0 + }; + + public void DrawString(string value, float x, float y) { + + Context.Save(); + + var layout = GetLayout(); + layout.SetText(value); + layout.GetPixelSize(out var textWidth, out var textHeight); + + if (layout.IsWrapped || layout.IsEllipsized) { + if (HeightForWidth) + layout.Width = textWidth.ScaledToPango(); + else + layout.Height = textWidth.ScaledToPango(); + } + + var mX = GetX(x, textWidth); + var mY = GetY(y, textHeight); + Context.MoveTo(mX, mY); + Draw(); + Context.Restore(); + + } + + public void DrawString(string value, float x, float y, float width, float height) { + + Context.Save(); + Context.Translate(x, y); + + var layout = GetLayout(); + layout.SetText(value); + + if (HeightForWidth) { + layout.Width = width.ScaledToPango(); + + if (TextFlow == TextFlow.ClipBounds) { + layout.Height = height.ScaledToPango(); + } + } else { + layout.Height = height.ScaledToPango(); + + if (TextFlow == TextFlow.ClipBounds) { + layout.Width = width.ScaledToPango(); + } + } + + if (TextFlow == TextFlow.ClipBounds && !layout.IsEllipsized) { + layout.Ellipsize = Pango.EllipsizeMode.End; + } + + if (!layout.IsWrapped || !layout.IsEllipsized) { + layout.Wrap = Pango.WrapMode.Char; + } + + layout.GetPixelExtents(out var inkRect, out var logicalRect); + + var mX = HeightForWidth ? + 0 : + TextFlow == TextFlow.ClipBounds ? + Math.Max(0, GetDx((int) width - logicalRect.Width - logicalRect.X)) : + GetDx((int) width - logicalRect.Width - inkRect.X); + + var mY = !HeightForWidth ? + 0 : + TextFlow == TextFlow.ClipBounds ? + Math.Max(0, GetDy((int) height - inkRect.Height - inkRect.Y)) : + GetDy((int) height - inkRect.Height - inkRect.Y); + + if (mY + inkRect.Height > height && TextFlow == TextFlow.ClipBounds && !HeightForWidth) { + mY = 0; + } + + Context.MoveTo(mX, mY); + Draw(); + Context.Restore(); + } + + [GtkMissingImplementation] + public void DrawAttributedText(IAttributedText value, float f, float f1, float width, float height) { } + + #region future use for better TextFlow.ClipBounds - algo without Elipsize + + /// + /// future use for + /// + private void ClampToContext() { + if (_layout == null || _context == null) + return; + + var ctxSize = Context.ClipExtents(); + + _layout.GetExtents(out var inkRect, out var logicalRect); + var maxW = ctxSize.Width.ScaledToPango(); + var maxH = ctxSize.Height.ScaledToPango(); + + while (logicalRect.Width > maxW) { + if (!_layout.IsWrapped) { + _layout.Wrap = Pango.WrapMode.Char; + } + + _layout.Width = maxW; + _layout.GetExtents(out inkRect, out logicalRect); + maxW -= 1.ScaledToPango(); + } + + while (logicalRect.Height > maxH) { + if (!_layout.IsWrapped) { + _layout.Wrap = Pango.WrapMode.Char; + } + + _layout.Height = maxH; + _layout.GetExtents(out inkRect, out logicalRect); + maxH -= 1.ScaledToPango(); + } + + var resLr = new Size(logicalRect.Width.ScaledFromPango(), logicalRect.Height.ScaledFromPango()); + + } + + /// + /// Get the distance in pixels between the top of the layout bounds and the first line's baseline + /// + public double GetBaseline() { + // Just get the first line + using var iter = GetLayout().Iter; + + return Pango.Units.ToPixels(iter.Baseline); + } + + /// + /// Get the distance in pixels between the top of the layout bounds and the first line's meanline (usually equivalent to the baseline minus half of the x-height) + /// + public double GetMeanline() { + var baseline = 0; + + var layout = GetLayout(); + using var iter = layout.Iter; + + baseline = iter.Baseline; + + var font = layout.Context.LoadFont(layout.FontDescription); + + return Pango.Units.ToPixels(baseline - font.GetMetrics(Pango.Language.Default).StrikethroughPosition); + } + + #endregion + + } + +} diff --git a/src/Microsoft.Maui.Graphics.Gtk/Gtk/TextLayoutExtensions.cs b/src/Microsoft.Maui.Graphics.Gtk/Gtk/TextLayoutExtensions.cs new file mode 100644 index 0000000..f9f4899 --- /dev/null +++ b/src/Microsoft.Maui.Graphics.Gtk/Gtk/TextLayoutExtensions.cs @@ -0,0 +1,67 @@ +namespace Microsoft.Maui.Graphics.Native.Gtk { + + public static class TextLayoutExtensions { + + public static void SetFontStyle(this TextLayout it, IFontStyle fs) { + it.FontFamily = fs.FontFamily.Name; + it.Weight = FontExtensions.ToFontWeigth(fs.Weight); + it.Style = fs.StyleType.ToPangoStyle(); + + if (fs is NativeFontStyle nfs) { + it.PangoFontSize = nfs.Size.ScaledToPango(); + } + } + + public static void SetCanvasState(this TextLayout it, NativeCanvasState state) { + it.FontFamily = state.FontName; + it.PangoFontSize = state.FontSize.ScaledToPango(); + it.TextColor = state.FontColor; + } + + public static TextLayout WithCanvasState(this TextLayout it, NativeCanvasState state) { + it.SetCanvasState(state); + + return it; + } + + public static Size GetSize(this TextLayout it, string text, float textHeigth) { + var (width, height) = it.GetPixelSize(text, (int) textHeigth); + + return new Size(width, height); + } + + public static Pango.Alignment ToPango(this HorizontalAlignment it) => it switch { + HorizontalAlignment.Center => Pango.Alignment.Center, + HorizontalAlignment.Right => Pango.Alignment.Right, + _ => Pango.Alignment.Left + }; + + public static Pango.WrapMode ToPangoWrap(this Extras.LineBreakMode it) { + if (it.HasFlag(Extras.LineBreakMode.CharacterWrap)) + return Pango.WrapMode.Char; + else if (it.HasFlag(Extras.LineBreakMode.WordCharacterWrap)) + return Pango.WrapMode.WordChar; + else + return Pango.WrapMode.Word; + } + + public static Pango.EllipsizeMode ToPangoEllipsize(this Extras.LineBreakMode it) { + + if (it.HasFlag(Extras.LineBreakMode.Elipsis | Extras.LineBreakMode.End)) + return Pango.EllipsizeMode.End; + + if (it.HasFlag(Extras.LineBreakMode.Elipsis | Extras.LineBreakMode.Center)) + return Pango.EllipsizeMode.Middle; + + if (it.HasFlag(Extras.LineBreakMode.Elipsis | Extras.LineBreakMode.Start)) + return Pango.EllipsizeMode.Start; + + if (it.HasFlag(Extras.LineBreakMode.Elipsis)) + return Pango.EllipsizeMode.End; + + return Pango.EllipsizeMode.None; + } + + } + +} diff --git a/src/Microsoft.Maui.Graphics.Gtk/Gtk/Views/GtkGraphicsView.cs b/src/Microsoft.Maui.Graphics.Gtk/Gtk/Views/GtkGraphicsView.cs new file mode 100644 index 0000000..6720bbe --- /dev/null +++ b/src/Microsoft.Maui.Graphics.Gtk/Gtk/Views/GtkGraphicsView.cs @@ -0,0 +1,61 @@ +namespace Microsoft.Maui.Graphics.Native.Gtk { + + public class GtkGraphicsView : global::Gtk.EventBox { + + private IDrawable? _drawable; + private RectangleF _dirtyRect; + private Color? _backgroundColor; + + public GtkGraphicsView() { + AppPaintable = true; + VisibleWindow = false; + } + + protected override bool OnDrawn(Cairo.Context context) { + if (_drawable == null) { + return base.OnDrawn(context); + } + + // ensure cr does not get disposed before it is passed back to Gtk + var canvas = new NativeCanvas {Context = context}; + + canvas.SaveState(); + + if (_backgroundColor != null) { + canvas.FillColor = _backgroundColor; + canvas.FillRectangle(_dirtyRect); + } else { + canvas.ClipRectangle(_dirtyRect); + } + + canvas.RestoreState(); + Drawable?.Draw(canvas, _dirtyRect); + + return base.OnDrawn(context); + } + + public Color? BackgroundColor { + get => _backgroundColor; + set { + _backgroundColor = value; + QueueDraw(); + } + } + + public IDrawable? Drawable { + get => _drawable; + set { + _drawable = value; + QueueDraw(); + } + } + + protected override void OnSizeAllocated(Gdk.Rectangle allocation) { + _dirtyRect.Width = allocation.Width; + _dirtyRect.Height = allocation.Height; + base.OnSizeAllocated(allocation); + } + + } + +} diff --git a/src/Microsoft.Maui.Graphics.Gtk/GtkMissingImplementationAttribute.cs b/src/Microsoft.Maui.Graphics.Gtk/GtkMissingImplementationAttribute.cs new file mode 100644 index 0000000..471640b --- /dev/null +++ b/src/Microsoft.Maui.Graphics.Gtk/GtkMissingImplementationAttribute.cs @@ -0,0 +1,5 @@ +namespace Microsoft.Maui.Graphics.Native.Gtk { + + public class GtkMissingImplementationAttribute : System.Attribute { } + +} diff --git a/src/Microsoft.Maui.Graphics.Gtk/Microsoft.Maui.Graphics.Gtk.csproj b/src/Microsoft.Maui.Graphics.Gtk/Microsoft.Maui.Graphics.Gtk.csproj new file mode 100644 index 0000000..23cff34 --- /dev/null +++ b/src/Microsoft.Maui.Graphics.Gtk/Microsoft.Maui.Graphics.Gtk.csproj @@ -0,0 +1,18 @@ + + + + netstandard2.1;netstandard2.0 + Microsoft.Maui.Graphics.Gtk + + + 8.0 + enable + + + + + + + + +