diff --git a/Avalonia.sln b/Avalonia.sln index a6fdcbd421..0eda7e5fc7 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -155,6 +155,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RenderTest", "samples\Rende EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ControlCatalog.Android", "samples\ControlCatalog.Android\ControlCatalog.Android.csproj", "{29132311-1848-4FD6-AE0C-4FF841151BD3}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Gtk3", "src\Gtk\Avalonia.Gtk3\Avalonia.Gtk3.csproj", "{BB1F7BB5-6AD4-4776-94D9-C09D0A972658}" +EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution src\Skia\Avalonia.Skia\Avalonia.Skia.projitems*{2f59f3d0-748d-4652-b01e-e0d954756308}*SharedItemsImports = 13 @@ -2329,6 +2331,46 @@ Global {29132311-1848-4FD6-AE0C-4FF841151BD3}.Release|x86.ActiveCfg = Release|Any CPU {29132311-1848-4FD6-AE0C-4FF841151BD3}.Release|x86.Build.0 = Release|Any CPU {29132311-1848-4FD6-AE0C-4FF841151BD3}.Release|x86.Deploy.0 = Release|Any CPU + {BB1F7BB5-6AD4-4776-94D9-C09D0A972658}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU + {BB1F7BB5-6AD4-4776-94D9-C09D0A972658}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU + {BB1F7BB5-6AD4-4776-94D9-C09D0A972658}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU + {BB1F7BB5-6AD4-4776-94D9-C09D0A972658}.Ad-Hoc|iPhone.Build.0 = Release|Any CPU + {BB1F7BB5-6AD4-4776-94D9-C09D0A972658}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Release|Any CPU + {BB1F7BB5-6AD4-4776-94D9-C09D0A972658}.Ad-Hoc|iPhoneSimulator.Build.0 = Release|Any CPU + {BB1F7BB5-6AD4-4776-94D9-C09D0A972658}.Ad-Hoc|Mono.ActiveCfg = Release|Any CPU + {BB1F7BB5-6AD4-4776-94D9-C09D0A972658}.Ad-Hoc|Mono.Build.0 = Release|Any CPU + {BB1F7BB5-6AD4-4776-94D9-C09D0A972658}.Ad-Hoc|x86.ActiveCfg = Release|Any CPU + {BB1F7BB5-6AD4-4776-94D9-C09D0A972658}.Ad-Hoc|x86.Build.0 = Release|Any CPU + {BB1F7BB5-6AD4-4776-94D9-C09D0A972658}.AppStore|Any CPU.ActiveCfg = Release|Any CPU + {BB1F7BB5-6AD4-4776-94D9-C09D0A972658}.AppStore|Any CPU.Build.0 = Release|Any CPU + {BB1F7BB5-6AD4-4776-94D9-C09D0A972658}.AppStore|iPhone.ActiveCfg = Release|Any CPU + {BB1F7BB5-6AD4-4776-94D9-C09D0A972658}.AppStore|iPhone.Build.0 = Release|Any CPU + {BB1F7BB5-6AD4-4776-94D9-C09D0A972658}.AppStore|iPhoneSimulator.ActiveCfg = Release|Any CPU + {BB1F7BB5-6AD4-4776-94D9-C09D0A972658}.AppStore|iPhoneSimulator.Build.0 = Release|Any CPU + {BB1F7BB5-6AD4-4776-94D9-C09D0A972658}.AppStore|Mono.ActiveCfg = Release|Any CPU + {BB1F7BB5-6AD4-4776-94D9-C09D0A972658}.AppStore|Mono.Build.0 = Release|Any CPU + {BB1F7BB5-6AD4-4776-94D9-C09D0A972658}.AppStore|x86.ActiveCfg = Release|Any CPU + {BB1F7BB5-6AD4-4776-94D9-C09D0A972658}.AppStore|x86.Build.0 = Release|Any CPU + {BB1F7BB5-6AD4-4776-94D9-C09D0A972658}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BB1F7BB5-6AD4-4776-94D9-C09D0A972658}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BB1F7BB5-6AD4-4776-94D9-C09D0A972658}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {BB1F7BB5-6AD4-4776-94D9-C09D0A972658}.Debug|iPhone.Build.0 = Debug|Any CPU + {BB1F7BB5-6AD4-4776-94D9-C09D0A972658}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {BB1F7BB5-6AD4-4776-94D9-C09D0A972658}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {BB1F7BB5-6AD4-4776-94D9-C09D0A972658}.Debug|Mono.ActiveCfg = Debug|Any CPU + {BB1F7BB5-6AD4-4776-94D9-C09D0A972658}.Debug|Mono.Build.0 = Debug|Any CPU + {BB1F7BB5-6AD4-4776-94D9-C09D0A972658}.Debug|x86.ActiveCfg = Debug|Any CPU + {BB1F7BB5-6AD4-4776-94D9-C09D0A972658}.Debug|x86.Build.0 = Debug|Any CPU + {BB1F7BB5-6AD4-4776-94D9-C09D0A972658}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BB1F7BB5-6AD4-4776-94D9-C09D0A972658}.Release|Any CPU.Build.0 = Release|Any CPU + {BB1F7BB5-6AD4-4776-94D9-C09D0A972658}.Release|iPhone.ActiveCfg = Release|Any CPU + {BB1F7BB5-6AD4-4776-94D9-C09D0A972658}.Release|iPhone.Build.0 = Release|Any CPU + {BB1F7BB5-6AD4-4776-94D9-C09D0A972658}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {BB1F7BB5-6AD4-4776-94D9-C09D0A972658}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {BB1F7BB5-6AD4-4776-94D9-C09D0A972658}.Release|Mono.ActiveCfg = Release|Any CPU + {BB1F7BB5-6AD4-4776-94D9-C09D0A972658}.Release|Mono.Build.0 = Release|Any CPU + {BB1F7BB5-6AD4-4776-94D9-C09D0A972658}.Release|x86.ActiveCfg = Release|Any CPU + {BB1F7BB5-6AD4-4776-94D9-C09D0A972658}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -2382,5 +2424,6 @@ Global {BD7F352C-6DC1-4740-BAF2-2D34A038728C} = {A0CC0258-D18C-4AB3-854F-7101680FC3F9} {F1FDC5B0-4654-416F-AE69-E3E9BBD87801} = {9B9E3891-2366-4253-A952-D08BCEB71098} {29132311-1848-4FD6-AE0C-4FF841151BD3} = {9B9E3891-2366-4253-A952-D08BCEB71098} + {BB1F7BB5-6AD4-4776-94D9-C09D0A972658} = {B9894058-278A-46B5-B6ED-AD613FCC03B3} EndGlobalSection EndGlobal diff --git a/samples/ControlCatalog.Desktop/ControlCatalog.Desktop.csproj b/samples/ControlCatalog.Desktop/ControlCatalog.Desktop.csproj index 53cb277233..6fec660e55 100644 --- a/samples/ControlCatalog.Desktop/ControlCatalog.Desktop.csproj +++ b/samples/ControlCatalog.Desktop/ControlCatalog.Desktop.csproj @@ -72,6 +72,10 @@ {FB05AC90-89BA-4F2F-A924-F37875FB547C} Avalonia.Cairo + + {bb1f7bb5-6ad4-4776-94d9-c09d0a972658} + Avalonia.Gtk3 + {54F237D5-A70A-4752-9656-0C70B1A7B047} Avalonia.Gtk diff --git a/samples/ControlCatalog.Desktop/Program.cs b/samples/ControlCatalog.Desktop/Program.cs index b67c5ea51d..f2634865b9 100644 --- a/samples/ControlCatalog.Desktop/Program.cs +++ b/samples/ControlCatalog.Desktop/Program.cs @@ -2,6 +2,7 @@ using System; using System.Linq; using Avalonia; using Avalonia.Controls; +using Avalonia.Gtk3; using Avalonia.Logging.Serilog; using Avalonia.Platform; using Serilog; @@ -17,7 +18,8 @@ namespace ControlCatalog // TODO: Make this work with GTK/Skia/Cairo depending on command-line args // again. AppBuilder.Configure() - .UsePlatformDetect() + .UseDirect2D1() + .UseGtk3() .Start(); } diff --git a/src/Gtk/Avalonia.Gtk3/.gitignore b/src/Gtk/Avalonia.Gtk3/.gitignore new file mode 100644 index 0000000000..ddb0c6248b --- /dev/null +++ b/src/Gtk/Avalonia.Gtk3/.gitignore @@ -0,0 +1 @@ +project.lock.json \ No newline at end of file diff --git a/src/Gtk/Avalonia.Gtk3/Avalonia.Gtk3.csproj b/src/Gtk/Avalonia.Gtk3/Avalonia.Gtk3.csproj new file mode 100644 index 0000000000..5e645fb542 --- /dev/null +++ b/src/Gtk/Avalonia.Gtk3/Avalonia.Gtk3.csproj @@ -0,0 +1,80 @@ + + + + + 14.0 + Debug + AnyCPU + {BB1F7BB5-6AD4-4776-94D9-C09D0A972658} + Library + Properties + Avalonia.Gtk3 + Avalonia.Gtk3 + en-US + 512 + {786C830F-07A1-408B-BD7F-6EE04809D6DB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + + + v5.0 + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + + + {B09B78D8-9B26-48B0-9149-D64A2F120F3F} + Avalonia.Base + + + {D2221C82-4A25-4583-9B43-D791E3F6820C} + Avalonia.Controls + + + {62024B2D-53EB-4638-B26B-85EEAA54866E} + Avalonia.Input + + + {EB582467-6ABB-43A1-B052-E981BA910E3A} + Avalonia.SceneGraph + + + + + \ No newline at end of file diff --git a/src/Gtk/Avalonia.Gtk3/Gtk3Platform.cs b/src/Gtk/Avalonia.Gtk3/Gtk3Platform.cs new file mode 100644 index 0000000000..164e987802 --- /dev/null +++ b/src/Gtk/Avalonia.Gtk3/Gtk3Platform.cs @@ -0,0 +1,91 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Avalonia.Controls; +using Avalonia.Controls.Platform; +using Avalonia.Gtk3.Interop; +using Avalonia.Input; +using Avalonia.Input.Platform; +using Avalonia.Platform; + +namespace Avalonia.Gtk3 +{ + public class Gtk3Platform : IWindowingPlatform, IPlatformSettings, IPlatformThreadingInterface + { + internal static readonly Gtk3Platform Instance = new Gtk3Platform(); + internal static readonly MouseDevice Mouse = new MouseDevice(); + internal static readonly KeyboardDevice Keyboard = new KeyboardDevice(); + internal static IntPtr App { get; set; } + public static void Initialize() + { + Resolver.Resolve(); + Native.GtkInit(0, IntPtr.Zero); + App = Native.GtkApplicationNew("avalonia.app." + Guid.NewGuid(), 0); + //Mark current thread as UI thread + s_tlsMarker = true; + + AvaloniaLocator.CurrentMutable.Bind().ToConstant(Instance) + .Bind().ToSingleton() + .Bind().ToConstant(new CursorFactoryStub()) + .Bind().ToConstant(Keyboard) + .Bind().ToConstant(Mouse) + .Bind().ToConstant(Instance) + .Bind().ToConstant(Instance) + .Bind().ToSingleton() + .Bind().ToConstant(new PlatformIconLoaderStub()); + + } + + public IWindowImpl CreateWindow() => new WindowImpl(); + + public IEmbeddableWindowImpl CreateEmbeddableWindow() + { + throw new NotImplementedException(); + } + + public IPopupImpl CreatePopup() + { + throw new NotImplementedException(); + } + + + + public Size DoubleClickSize => new Size(4, 4); + + public TimeSpan DoubleClickTime => TimeSpan.FromMilliseconds(100); //STUB + public double RenderScalingFactor { get; } = 1; + public double LayoutScalingFactor { get; } = 1; + + public void RunLoop(CancellationToken cancellationToken) + { + while (!cancellationToken.IsCancellationRequested) + Native.GtkMainIteration(); + } + + public IDisposable StartTimer(TimeSpan interval, Action tick) + { + return null; + } + + public void Signal() + { + } + public event Action Signaled; + + + [ThreadStatic] + private static bool s_tlsMarker; + + public bool CurrentThreadIsLoopThread => s_tlsMarker; + + } + + public static class Gtk3AppBuilderExtensions + { + public static T UseGtk3(this AppBuilderBase builder) where T : AppBuilderBase, new() + => builder.UseWindowingSubsystem(Gtk3Platform.Initialize, "GTK3"); + } +} \ No newline at end of file diff --git a/src/Gtk/Avalonia.Gtk3/Interop/DynLoader.cs b/src/Gtk/Avalonia.Gtk3/Interop/DynLoader.cs new file mode 100644 index 0000000000..93ba7cc195 --- /dev/null +++ b/src/Gtk/Avalonia.Gtk3/Interop/DynLoader.cs @@ -0,0 +1,142 @@ +using System; +using System.Runtime.InteropServices; + +/* + * Source code imported from https://github.com/kekekeks/evhttp-sharp + * Source is provided under MIT license for Avalonia project and derived works + */ + + +namespace Avalonia.Gtk3.Interop +{ + internal interface IDynLoader + { + IntPtr LoadLibrary(string basePath, string dll); + IntPtr GetProcAddress(IntPtr dll, string proc); + + } + + class UnixLoader : IDynLoader + { + // ReSharper disable InconsistentNaming + static class LinuxImports + { + + [DllImport("libdl.so.2")] + private static extern IntPtr dlopen(string path, int flags); + + [DllImport("libdl.so.2")] + private static extern IntPtr dlsym(IntPtr handle, string symbol); + + [DllImport("libdl.so.2")] + private static extern IntPtr dlerror(); + + public static void Init() + { + DlOpen = dlopen; + DlSym = dlsym; + DlError = dlerror; + Postfix = ".so.0"; + } + } + static class OsXImports + { + + + [DllImport("/usr/lib/libSystem.dylib")] + private static extern IntPtr dlopen(string path, int flags); + + [DllImport("/usr/lib/libSystem.dylib")] + private static extern IntPtr dlsym(IntPtr handle, string symbol); + + [DllImport("/usr/lib/libSystem.dylib")] + private static extern IntPtr dlerror(); + + public static void Init() + { + DlOpen = dlopen; + DlSym = dlsym; + DlError = dlerror; + Postfix = ".dylib"; //TODO + } + + } + + + [DllImport("libc")] + static extern int uname(IntPtr buf); + + + + static UnixLoader() + { + var buffer = Marshal.AllocHGlobal(0x1000); + uname(buffer); + var unixName = Marshal.PtrToStringAnsi(buffer); + Marshal.FreeHGlobal(buffer); + if(unixName == "Darwin") + OsXImports.Init(); + else + LinuxImports.Init(); + } + + private static Func DlOpen; + private static Func DlSym; + private static Func DlError; + private static string Postfix; + // ReSharper restore InconsistentNaming + + static string DlErrorString() + { + + return Marshal.PtrToStringAnsi(DlError()); + } + + public IntPtr LoadLibrary(string basePath, string dll) + { + dll += Postfix; + if (basePath != null) + dll = System.IO.Path.Combine(basePath, dll); + var handle = DlOpen(dll, 1); + if (handle == IntPtr.Zero) + throw new NativeException(DlErrorString()); + return handle; + } + + public IntPtr GetProcAddress(IntPtr dll, string proc) + { + var ptr = DlSym(dll, proc); + if (ptr == IntPtr.Zero) + throw new NativeException(DlErrorString()); + return ptr; + } + } + + internal class Win32Loader : IDynLoader + { + [DllImport("kernel32", CharSet = CharSet.Ansi, ExactSpelling = true, SetLastError = true)] + private static extern IntPtr GetProcAddress(IntPtr hModule, string procName); + + [DllImport("kernel32", EntryPoint = "LoadLibraryW", SetLastError = true, CharSet = CharSet.Unicode)] + private static extern IntPtr LoadLibrary(string lpszLib); + + IntPtr IDynLoader.LoadLibrary(string basePath, string dll) + { + dll += "-0.dll"; + if (basePath != null) + dll = System.IO.Path.Combine(basePath, dll); + var handle = LoadLibrary(dll); + if (handle == IntPtr.Zero) + throw new NativeException("Error " + Marshal.GetLastWin32Error()); + return handle; + } + + IntPtr IDynLoader.GetProcAddress(IntPtr dll, string proc) + { + var ptr = GetProcAddress(dll, proc); + if (ptr == IntPtr.Zero) + throw new NativeException("Error " + Marshal.GetLastWin32Error()); + return ptr; + } + } +} diff --git a/src/Gtk/Avalonia.Gtk3/Interop/Native.cs b/src/Gtk/Avalonia.Gtk3/Interop/Native.cs new file mode 100644 index 0000000000..a07d84c710 --- /dev/null +++ b/src/Gtk/Avalonia.Gtk3/Interop/Native.cs @@ -0,0 +1,87 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; + +namespace Avalonia.Gtk3.Interop +{ + static class Native + { + public static class D + { + [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gtk)] + public delegate IntPtr gtk_application_new([MarshalAs(UnmanagedType.AnsiBStr)] string appId, int flags); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gtk)] + public delegate void gtk_main_iteration(); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gtk)] + public delegate IntPtr gtk_window_new(GtkWindowType windowType); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gtk)] + public delegate IntPtr gtk_init(int argc, IntPtr argv); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gtk)] + public delegate void gtk_window_present(IntPtr gtkWindow); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gtk)] + public delegate void gtk_widget_hide(IntPtr gtkWidget); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] //No manual import + public delegate IntPtr gdk_get_native_handle(IntPtr gdkWindow); + + + [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gtk)] + public delegate IntPtr gtk_widget_get_window(IntPtr gtkWidget); + [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gtk)] + public delegate IntPtr gtk_widget_set_double_buffered(IntPtr gtkWidget, bool value); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gtk)] + public delegate void gtk_widget_realize(IntPtr gtkWidget); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gtk)] + public delegate void gtk_window_set_title(IntPtr gtkWindow, Utf8Buffer title); + + + [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gtk)] + public delegate void gtk_window_set_decorated(IntPtr gtkWindow, bool decorated); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gtk)] + public delegate void gtk_window_get_size(IntPtr gtkWindow, out int width, out int height); + [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gobject)] + public delegate ulong g_signal_connect_object(IntPtr instance, [MarshalAs(UnmanagedType.AnsiBStr)]string signal, IntPtr handler, IntPtr userData, int flags); + [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gobject)] + public delegate ulong g_signal_handler_disconnect(IntPtr instance, ulong connectionId); + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate bool signal_widget_draw(IntPtr gtkWidget, IntPtr cairoContext, IntPtr userData); + } + + + public static D.gtk_window_set_decorated GtkWindowSetDecorated; + public static D.gtk_window_set_title GtkWindowSetTitle; + public static D.gtk_application_new GtkApplicationNew; + public static D.gtk_main_iteration GtkMainIteration; + public static D.gtk_window_new GtkWindowNew; + public static D.gtk_init GtkInit; + public static D.gtk_window_present GtkWindowPresent; + public static D.gtk_widget_hide GtkWidgetHide; + public static D.gdk_get_native_handle GetNativeGdkWindowHandle; + public static D.gtk_widget_get_window GtkWidgetGetWindow; + public static D.gtk_widget_realize GtkWidgetRealize; + public static D.gtk_window_get_size GtkWindowGetSize; + public static D.g_signal_connect_object GSignalConnectObject; + public static D.g_signal_handler_disconnect GSignalHandlerDisconnect; + public static D.gtk_widget_set_double_buffered GtkWidgetSetDoubleBuffered; + + + + } + + public enum GtkWindowType + { + TopLevel, + Popup + } +} diff --git a/src/Gtk/Avalonia.Gtk3/Interop/NativeException.cs b/src/Gtk/Avalonia.Gtk3/Interop/NativeException.cs new file mode 100644 index 0000000000..43659f8f83 --- /dev/null +++ b/src/Gtk/Avalonia.Gtk3/Interop/NativeException.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Avalonia.Gtk3.Interop +{ + public class NativeException : Exception + { + public NativeException() + { + } + + public NativeException(string message) : base(message) + { + } + + public NativeException(string message, Exception inner) : base(message, inner) + { + } + + } +} diff --git a/src/Gtk/Avalonia.Gtk3/Interop/Resolver.cs b/src/Gtk/Avalonia.Gtk3/Interop/Resolver.cs new file mode 100644 index 0000000000..2410af7591 --- /dev/null +++ b/src/Gtk/Avalonia.Gtk3/Interop/Resolver.cs @@ -0,0 +1,104 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; + +namespace Avalonia.Gtk3.Interop +{ + internal class GtkImportAttribute : Attribute + { + public GtkDll Dll { get; set; } + public string Name { get; set; } + public bool Optional { get; set; } + + public GtkImportAttribute(GtkDll dll, string name = null, bool optional = false) + { + Dll = dll; + Name = name; + Optional = optional; + } + } + + internal enum GtkDll + { + Gdk, + Gtk, + Glib, + Gio, + Gobject + } + + static class Resolver + { + [DllImport("kernel32.dll")] + static extern int GetVersion(); + + static bool IsWin32() + { + try + { + GetVersion(); + return true; + } + catch + { + return false; + } + } + + + + public static void Resolve(string basePath = null) + { + var loader = IsWin32() ? (IDynLoader)new Win32Loader() : new UnixLoader(); + + + var gdk = loader.LoadLibrary(basePath, "libgdk-3"); + var gtk = loader.LoadLibrary(basePath, "libgtk-3"); + var gio = loader.LoadLibrary(basePath, "libgio-2.0"); + var glib = loader.LoadLibrary(basePath, "libglib-2.0"); + var gobject = loader.LoadLibrary(basePath, "libgobject-2.0"); + foreach (var fieldInfo in typeof(Native).GetTypeInfo().DeclaredFields) + { + var import = fieldInfo.FieldType.GetTypeInfo().GetCustomAttributes(typeof(GtkImportAttribute), true).Cast().FirstOrDefault(); + if(import == null) + continue; + IntPtr lib; + if (import.Dll == GtkDll.Gtk) + lib = gtk; + else if (import.Dll == GtkDll.Gdk) + lib = gdk; + else if (import.Dll == GtkDll.Gio) + lib = gio; + else if (import.Dll == GtkDll.Glib) + lib = glib; + else if (import.Dll == GtkDll.Gobject) + lib = gobject; + else + throw new ArgumentException("Invalid GtkImportAttribute for " + fieldInfo.FieldType); + + var funcPtr = loader.GetProcAddress(lib, import.Name ?? fieldInfo.FieldType.Name); + fieldInfo.SetValue(null, Marshal.GetDelegateForFunctionPointer(funcPtr, fieldInfo.FieldType)); + } + + var nativeHandleNames = new[] {"gdk_x11_window_get_xid", "gdk_win32_window_get_handle"}; + foreach (var name in nativeHandleNames) + { + try + { + Native.GetNativeGdkWindowHandle = (Native.D.gdk_get_native_handle)Marshal + .GetDelegateForFunctionPointer(loader.GetProcAddress(gdk, name), typeof(Native.D.gdk_get_native_handle)); + } + catch { } + } + if (Native.GetNativeGdkWindowHandle == null) + throw new Exception($"Unable to locate any of [{string.Join(", ", nativeHandleNames)}] in libgdk"); + + } + + + } +} diff --git a/src/Gtk/Avalonia.Gtk3/Interop/Signal.cs b/src/Gtk/Avalonia.Gtk3/Interop/Signal.cs new file mode 100644 index 0000000000..5896c547f2 --- /dev/null +++ b/src/Gtk/Avalonia.Gtk3/Interop/Signal.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; + +namespace Avalonia.Gtk3.Interop +{ + public class Signal + { + class ConnectedSignal : IDisposable + { + private readonly IntPtr _instance; + private GCHandle _handle; + private readonly ulong _id; + + public ConnectedSignal(IntPtr instance, GCHandle handle, ulong id) + { + _instance = instance; + _handle = handle; + _id = id; + } + + public void Dispose() + { + if (_handle.IsAllocated) + { + Native.GSignalHandlerDisconnect(_instance, _id); + _handle.Free(); + } + } + } + + public static IDisposable Connect(IntPtr obj, string name, T handler) + { + var handle = GCHandle.Alloc(handler); + var ptr = Marshal.GetFunctionPointerForDelegate((Delegate)(object)handler); + var id = Native.GSignalConnectObject(obj, name, ptr, IntPtr.Zero, 0); + if (id == 0) + throw new ArgumentException("Unable to connect to signal " + name); + return new ConnectedSignal(obj, handle, id); + } + } +} diff --git a/src/Gtk/Avalonia.Gtk3/Interop/Utf8Buffer.cs b/src/Gtk/Avalonia.Gtk3/Interop/Utf8Buffer.cs new file mode 100644 index 0000000000..7a9868857f --- /dev/null +++ b/src/Gtk/Avalonia.Gtk3/Interop/Utf8Buffer.cs @@ -0,0 +1,32 @@ +using System; +using System.Runtime.InteropServices; +using System.Text; + +namespace Avalonia.Gtk3.Interop +{ + class Utf8Buffer : SafeHandle + { + private GCHandle _gchandle; + private byte[] _data; + + public Utf8Buffer(string s) : base(IntPtr.Zero, true) + { + _data = Encoding.UTF8.GetBytes(s); + _gchandle = GCHandle.Alloc(_data, GCHandleType.Pinned); + handle = _gchandle.AddrOfPinnedObject(); + } + + protected override bool ReleaseHandle() + { + if (handle != IntPtr.Zero) + { + handle = IntPtr.Zero; + _data = null; + _gchandle.Free(); + } + return true; + } + + public override bool IsInvalid => handle == IntPtr.Zero; + } +} diff --git a/src/Gtk/Avalonia.Gtk3/Properties/AssemblyInfo.cs b/src/Gtk/Avalonia.Gtk3/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..72e6388531 --- /dev/null +++ b/src/Gtk/Avalonia.Gtk3/Properties/AssemblyInfo.cs @@ -0,0 +1,30 @@ +using System.Resources; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Avalonia.Gtk3")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Avalonia.Gtk3")] +[assembly: AssemblyCopyright("Copyright © 2016")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] +[assembly: NeutralResourcesLanguage("en")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/src/Gtk/Avalonia.Gtk3/README.md b/src/Gtk/Avalonia.Gtk3/README.md new file mode 100644 index 0000000000..7f117d24b3 --- /dev/null +++ b/src/Gtk/Avalonia.Gtk3/README.md @@ -0,0 +1,9 @@ +P/Invoke based GTK3 backend +=========================== + +Code is EXPERIMENTAL at this point. It also needs Direct2D/Skia for rendering. + +Windows GTK3 binaries aren't included in the repo, you need to download them from http://www.tarnyko.net/repo/gtk3_build_system/gtk+-bundle_3.4.2-20130513_win32.zip +Then you need to extract them somewhere and add `bin` directory to PATH. Support for specifying exact path to binaries will be implemented later. + +On Linux it should work out of the box with system-provided GTK3. On OSX you should be able to wire GTK3 using DYLD_LIBRARY_PATH environment variable. \ No newline at end of file diff --git a/src/Gtk/Avalonia.Gtk3/Stubs.cs b/src/Gtk/Avalonia.Gtk3/Stubs.cs new file mode 100644 index 0000000000..76333aba60 --- /dev/null +++ b/src/Gtk/Avalonia.Gtk3/Stubs.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Avalonia.Controls; +using Avalonia.Controls.Platform; +using Avalonia.Input; +using Avalonia.Input.Platform; +using Avalonia.Platform; + +//TODO: This file should be empty once everything is implemented + +namespace Avalonia.Gtk3 +{ + class ClipboardStub : IClipboard + { + public Task GetTextAsync() => Task.FromResult(""); + + public Task SetTextAsync(string text) => Task.FromResult(0); + + public Task ClearAsync() => Task.FromResult(0); + } + + class CursorFactoryStub : IStandardCursorFactory + { + public IPlatformHandle GetCursor(StandardCursorType cursorType) => new PlatformHandle(IntPtr.Zero, "STUB"); + } + + class SystemDialogStub : ISystemDialogImpl + { + public Task ShowFileDialogAsync(FileDialog dialog, IWindowImpl parent) => Task.FromResult(new string[0]); + + public Task ShowFolderDialogAsync(OpenFolderDialog dialog, IWindowImpl parent) + => Task.FromResult((string) null); + } + + class PlatformIconLoaderStub : IPlatformIconLoader + { + public IWindowIconImpl LoadIcon(string fileName) => null; + + public IWindowIconImpl LoadIcon(Stream stream) => null; + + public IWindowIconImpl LoadIcon(IBitmapImpl bitmap) => null; + } +} diff --git a/src/Gtk/Avalonia.Gtk3/TopLevelImpl.cs b/src/Gtk/Avalonia.Gtk3/TopLevelImpl.cs new file mode 100644 index 0000000000..2f6181c893 --- /dev/null +++ b/src/Gtk/Avalonia.Gtk3/TopLevelImpl.cs @@ -0,0 +1,95 @@ +using System; +using Avalonia.Controls; +using Avalonia.Gtk3.Interop; +using Avalonia.Input; +using Avalonia.Input.Raw; +using Avalonia.Platform; + +namespace Avalonia.Gtk3 +{ + abstract class TopLevelImpl : ITopLevelImpl, IPlatformHandle + { + protected readonly IntPtr _gtkWidget; + private IInputRoot _inputRoot; + + public TopLevelImpl(IntPtr gtkWidget) + { + _gtkWidget = gtkWidget; + Native.GtkWidgetRealize(gtkWidget); + Native.GtkWidgetSetDoubleBuffered(gtkWidget, false); + Signal.Connect(_gtkWidget, "draw", OnDraw); + } + + private bool OnDraw(IntPtr gtkwidget, IntPtr cairocontext, IntPtr userdata) + { + Paint?.Invoke(new Rect(ClientSize)); + return true; + } + + public void Dispose() + { + //STUB + } + + public abstract Size ClientSize { get; set; } + + public Size MaxClientSize => new Size(1024, 768); //STUB + public double Scaling => 1; //TODO: Implement scaling + public IPlatformHandle Handle => this; + + string IPlatformHandle.HandleDescriptor => "HWND"; + + public Action Activated { get; set; } + public Action Closed { get; set; } + public Action Deactivated { get; set; } + public Action Input { get; set; } + public Action Paint { get; set; } + public Action Resized { get; set; } + public Action ScalingChanged { get; set; } + public Action PositionChanged { get; set; } + public void Activate() + { + throw new NotImplementedException(); + } + + public void Invalidate(Rect rect) + { + throw new NotImplementedException(); + } + + public void SetInputRoot(IInputRoot inputRoot) => _inputRoot = inputRoot; + + public Point PointToClient(Point point) + { + throw new NotImplementedException(); + } + + public Point PointToScreen(Point point) + { + throw new NotImplementedException(); + } + + public void SetCursor(IPlatformHandle cursor) + { + //STUB + } + + public void Show() => Native.GtkWindowPresent(_gtkWidget); + + public void Hide() => Native.GtkWidgetHide(_gtkWidget); + + public void BeginMoveDrag() + { + //STUB + } + + public void BeginResizeDrag(WindowEdge edge) + { + //STUB + } + + public Point Position { get; set; } + + IntPtr IPlatformHandle.Handle => Native.GetNativeGdkWindowHandle(Native.GtkWidgetGetWindow(_gtkWidget)); + } +} diff --git a/src/Gtk/Avalonia.Gtk3/WindowImpl.cs b/src/Gtk/Avalonia.Gtk3/WindowImpl.cs new file mode 100644 index 0000000000..e3fd52f971 --- /dev/null +++ b/src/Gtk/Avalonia.Gtk3/WindowImpl.cs @@ -0,0 +1,48 @@ +using System; +using Avalonia.Controls; +using Avalonia.Gtk3.Interop; +using Avalonia.Platform; + +namespace Avalonia.Gtk3 +{ + class WindowImpl : TopLevelImpl, IWindowImpl + { + public WindowState WindowState { get; set; } //STUB + public void SetTitle(string title) + { + using (var t = new Utf8Buffer(title)) + Native.GtkWindowSetTitle(_gtkWidget, t); + } + + public IDisposable ShowDialog() + { + return null; + //STUB + } + + public void SetSystemDecorations(bool enabled) => Native.GtkWindowSetDecorated(_gtkWidget, enabled); + + public void SetIcon(IWindowIconImpl icon) + { + //STUB + } + + public WindowImpl() : base(Native.GtkWindowNew(GtkWindowType.TopLevel)) + { + } + + public override Size ClientSize + { + get + { + int w, h; + Native.GtkWindowGetSize(_gtkWidget, out w, out h); + return new Size(w, h); + } + set + { + //STUB + } + } + } +} diff --git a/src/Gtk/Avalonia.Gtk3/project.json b/src/Gtk/Avalonia.Gtk3/project.json new file mode 100644 index 0000000000..7af946da1a --- /dev/null +++ b/src/Gtk/Avalonia.Gtk3/project.json @@ -0,0 +1,10 @@ +{ + "supports": {}, + "dependencies": { + "Microsoft.NETCore.Portable.Compatibility": "1.0.1", + "NETStandard.Library": "1.6.0" + }, + "frameworks": { + "netstandard1.1": {} + } +} \ No newline at end of file