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
+
+
+
+
+
+
+
+
+