Microsoft.Maui.Graphics.Gtk: add Gtk - Plattform (gtk 3.22 / GtkSharp)
This commit is contained in:
Родитель
4618ea016c
Коммит
594305aa55
|
@ -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 system’s 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 system’s 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>
|
Загрузка…
Ссылка в новой задаче