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