Microsoft.Maui.Graphics.Gtk: add Gtk - Plattform (gtk 3.22 / GtkSharp)

This commit is contained in:
lytico 2021-05-06 23:32:43 +02:00
Родитель 4618ea016c
Коммит 594305aa55
26 изменённых файлов: 2155 добавлений и 0 удалений

Просмотреть файл

@ -6,6 +6,7 @@
<add key="dotnet-public" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public/nuget/v3/index.json" protocolVersion="3" />
<add key="dotnet6" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet6/nuget/v3/index.json" />
<add key="xamarin" value="https://pkgs.dev.azure.com/azure-public/vside/_packaging/xamarin-impl/nuget/v3/index.json" />
<add key="Nuget Official" value="https://api.nuget.org/v3/index.json" />
</packageSources>
<activePackageSource>
<add key="All" value="(Aggregate source)" />

Просмотреть файл

@ -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,
}
}

Просмотреть файл

@ -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;
}
}
}

Просмотреть файл

@ -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);
}
}

Просмотреть файл

@ -0,0 +1,74 @@
using System.Collections.Generic;
using System.Linq;
namespace Microsoft.Maui.Graphics.Native.Gtk {
public static class FontExtensions {
/// <summary>
/// size in points
/// <seealso cref="https://developer.gnome.org/pygtk/stable/class-pangofontdescription.html#method-pangofontdescription--set-size"/>
/// the size of a font description is specified in pango units.
/// There are <see cref="Pango.Scale.PangoScale"/> pango units in one device unit (the device unit is a point for font sizes).
/// </summary>
/// <param name="it"></param>
/// <returns></returns>
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<IFontStyle> 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);
};
}

Просмотреть файл

@ -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);
}
}

Просмотреть файл

@ -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;
/// <summary>
/// writes a pixbuf to stream
/// </summary>
/// <param name="stream"></param>
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();
}
}
}

Просмотреть файл

@ -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));
}
}
}

Просмотреть файл

@ -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;
/// <summary>
/// A <see cref="Gdk.Visual"/> 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 <see cref="Gdk.Screen.Default.SystemVisual"/> is the systems default visual,
/// and the visual returned by <see cref="Gdk.Screen.Default.RgbaVisual"/> should be used for creating windows with an alpha channel.
///
/// Get the systems default visual for screen .
/// This is the visual for the root window of the display.
/// The return value should not be freed.
/// </summary>
public static Gdk.Visual SystemVisual => Gdk.Screen.Default.SystemVisual;
public static double DefaultResolution => DefaultScreen.Resolution;
/// <summary>
/// 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 <see cref="Gdk.Screen"/>s.
/// <see cref="Gdk.Display"/>'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 <see cref="Gdk.Screen"/> objects currently instantiated by the application.
/// It is also used to access the keyboard(s) and mouse pointer(s) of the display.
/// </summary>
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<Gdk.Monitor> GetMonitors() {
for (var i = 0; i < DefaultDisplay.NMonitors; i++) {
yield return DefaultDisplay.GetMonitor(i);
}
}
}
}

Просмотреть файл

@ -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;
}
}
}

Просмотреть файл

@ -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);
}
/// <summary>
/// degree-value * mRadians = radians
/// </summary>
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();
}
}
}

Просмотреть файл

@ -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;
}
}
}
}

Просмотреть файл

@ -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();
}
}
}
}

Просмотреть файл

@ -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);
}
}
}

Просмотреть файл

@ -0,0 +1,236 @@
namespace Microsoft.Maui.Graphics.Native.Gtk {
public partial class NativeCanvas : AbstractCanvas<NativeCanvasState> {
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) { }
}
}

Просмотреть файл

@ -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();
}
}
}

Просмотреть файл

@ -0,0 +1,63 @@
using System;
using System.Linq;
namespace Microsoft.Maui.Graphics.Native.Gtk {
public class NativeFontFamily : IFontFamily, IComparable<IFontFamily>, 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;
}
}
}

Просмотреть файл

@ -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<IFontFamily>();
public IEnumerable<IFontStyle> 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());
}
}
}

Просмотреть файл

@ -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();
}
}
}

Просмотреть файл

@ -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);
}
}
}

Просмотреть файл

@ -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();
}
/// <summary>
/// does not work, pattern isn't shown
/// </summary>
[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;
}
}
}

Просмотреть файл

@ -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 {
/// <summary>
/// Measures and draws text using <see cref="Pango.Layout"/>
/// https://developer.gnome.org/pango/1.46/pango-Layout-Objects.html
/// https://developer.gnome.org/gdk3/stable/gdk3-Pango-Interaction.html
/// </summary>
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<TextLayout> BeforeDrawn { get; set; }
public Action<TextLayout> 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
/// <summary>
/// future use for
/// </summary>
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());
}
/// <summary>
/// Get the distance in pixels between the top of the layout bounds and the first line's baseline
/// </summary>
public double GetBaseline() {
// Just get the first line
using var iter = GetLayout().Iter;
return Pango.Units.ToPixels(iter.Baseline);
}
/// <summary>
/// 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)
/// </summary>
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
}
}

Просмотреть файл

@ -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;
}
}
}

Просмотреть файл

@ -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);
}
}
}

Просмотреть файл

@ -0,0 +1,5 @@
namespace Microsoft.Maui.Graphics.Native.Gtk {
public class GtkMissingImplementationAttribute : System.Attribute { }
}

Просмотреть файл

@ -0,0 +1,18 @@
<Project Sdk="MSBuild.Sdk.Extras/">
<PropertyGroup>
<TargetFrameworks>netstandard2.1;netstandard2.0</TargetFrameworks>
<RootNamespace>Microsoft.Maui.Graphics.Gtk</RootNamespace>
</PropertyGroup>
<PropertyGroup>
<LangVersion>8.0</LangVersion>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Microsoft.Maui.Graphics\Microsoft.Maui.Graphics.csproj"/>
</ItemGroup>
<ItemGroup>
<PackageReference Include="GtkSharp" Version="3.24.24.34"/>
</ItemGroup>
</Project>