csharpc-sharpdotnetxamlavaloniauicross-platformcross-platform-xamlavaloniaguimulti-platformuser-interfacedotnetcore
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
503 lines
18 KiB
503 lines
18 KiB
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.Linq;
|
|
using System.Runtime.InteropServices;
|
|
using System.Text;
|
|
using System.Threading;
|
|
using Avalonia.Controls;
|
|
using Avalonia.Gtk3.Interop;
|
|
using Avalonia.Input;
|
|
using Avalonia.Input.Raw;
|
|
using Avalonia.Platform;
|
|
using Avalonia.Rendering;
|
|
using Avalonia.Threading;
|
|
|
|
namespace Avalonia.Gtk3
|
|
{
|
|
abstract class WindowBaseImpl : IWindowBaseImpl, IPlatformHandle
|
|
{
|
|
public readonly GtkWindow GtkWidget;
|
|
private IInputRoot _inputRoot;
|
|
private readonly GtkImContext _imContext;
|
|
private readonly FramebufferManager _framebuffer;
|
|
protected readonly List<IDisposable> Disposables = new List<IDisposable>();
|
|
private Size _lastSize;
|
|
private Point _lastPosition;
|
|
private double _lastScaling;
|
|
private uint _lastKbdEvent;
|
|
private uint _lastSmoothScrollEvent;
|
|
private GCHandle _gcHandle;
|
|
private object _lock = new object();
|
|
private IDeferredRenderOperation _nextRenderOperation;
|
|
private readonly AutoResetEvent _canSetNextOperation = new AutoResetEvent(true);
|
|
internal IntPtr? GdkWindowHandle;
|
|
private bool _overrideRedirect;
|
|
private uint? _tickCallback;
|
|
public WindowBaseImpl(GtkWindow gtkWidget)
|
|
{
|
|
|
|
GtkWidget = gtkWidget;
|
|
_framebuffer = new FramebufferManager(this);
|
|
_imContext = Native.GtkImMulticontextNew();
|
|
Disposables.Add(_imContext);
|
|
Native.GtkWidgetSetEvents(gtkWidget, 0xFFFFFE);
|
|
Disposables.Add(Signal.Connect<Native.D.signal_commit>(_imContext, "commit", OnCommit));
|
|
Connect<Native.D.signal_widget_draw>("draw", OnDraw);
|
|
Connect<Native.D.signal_generic>("realize", OnRealized);
|
|
ConnectEvent("configure-event", OnConfigured);
|
|
ConnectEvent("button-press-event", OnButton);
|
|
ConnectEvent("button-release-event", OnButton);
|
|
ConnectEvent("motion-notify-event", OnMotion);
|
|
ConnectEvent("scroll-event", OnScroll);
|
|
ConnectEvent("window-state-event", OnStateChanged);
|
|
ConnectEvent("key-press-event", OnKeyEvent);
|
|
ConnectEvent("key-release-event", OnKeyEvent);
|
|
ConnectEvent("leave-notify-event", OnLeaveNotifyEvent);
|
|
ConnectEvent("delete-event", OnClosingEvent);
|
|
Connect<Native.D.signal_generic>("destroy", OnDestroy);
|
|
Native.GtkWidgetRealize(gtkWidget);
|
|
GdkWindowHandle = this.Handle.Handle;
|
|
_lastSize = ClientSize;
|
|
if (Gtk3Platform.UseDeferredRendering)
|
|
{
|
|
Native.GtkWidgetSetDoubleBuffered(gtkWidget, false);
|
|
_gcHandle = GCHandle.Alloc(this);
|
|
_tickCallback = Native.GtkWidgetAddTickCallback(GtkWidget, PinnedStaticCallback, GCHandle.ToIntPtr(_gcHandle), IntPtr.Zero);
|
|
|
|
}
|
|
}
|
|
|
|
private bool OnConfigured(IntPtr gtkwidget, IntPtr ev, IntPtr userdata)
|
|
{
|
|
int w, h;
|
|
if (!OverrideRedirect)
|
|
{
|
|
Native.GtkWindowGetSize(GtkWidget, out w, out h);
|
|
var size = ClientSize = new Size(w, h);
|
|
if (_lastSize != size)
|
|
{
|
|
Resized?.Invoke(size);
|
|
_lastSize = size;
|
|
}
|
|
}
|
|
var pos = Position;
|
|
if (_lastPosition != pos)
|
|
{
|
|
PositionChanged?.Invoke(pos);
|
|
_lastPosition = pos;
|
|
}
|
|
var scaling = Scaling;
|
|
if (_lastScaling != scaling)
|
|
{
|
|
ScalingChanged?.Invoke(scaling);
|
|
_lastScaling = scaling;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private bool OnRealized(IntPtr gtkwidget, IntPtr userdata)
|
|
{
|
|
Native.GtkImContextSetClientWindow(_imContext, Native.GtkWidgetGetWindow(GtkWidget));
|
|
return false;
|
|
}
|
|
|
|
private bool OnDestroy(IntPtr gtkwidget, IntPtr userdata)
|
|
{
|
|
DoDispose(true);
|
|
return false;
|
|
}
|
|
|
|
private static InputModifiers GetModifierKeys(GdkModifierType state)
|
|
{
|
|
var rv = InputModifiers.None;
|
|
if (state.HasFlag(GdkModifierType.ControlMask))
|
|
rv |= InputModifiers.Control;
|
|
if (state.HasFlag(GdkModifierType.ShiftMask))
|
|
rv |= InputModifiers.Shift;
|
|
if (state.HasFlag(GdkModifierType.Mod1Mask))
|
|
rv |= InputModifiers.Control;
|
|
if (state.HasFlag(GdkModifierType.Button1Mask))
|
|
rv |= InputModifiers.LeftMouseButton;
|
|
if (state.HasFlag(GdkModifierType.Button2Mask))
|
|
rv |= InputModifiers.RightMouseButton;
|
|
if (state.HasFlag(GdkModifierType.Button3Mask))
|
|
rv |= InputModifiers.MiddleMouseButton;
|
|
return rv;
|
|
}
|
|
|
|
private unsafe bool OnClosingEvent(IntPtr w, IntPtr ev, IntPtr userdata)
|
|
{
|
|
bool? preventClosing = Closing?.Invoke();
|
|
return preventClosing ?? false;
|
|
}
|
|
|
|
private unsafe bool OnButton(IntPtr w, IntPtr ev, IntPtr userdata)
|
|
{
|
|
var evnt = (GdkEventButton*)ev;
|
|
var e = new RawMouseEventArgs(
|
|
Gtk3Platform.Mouse,
|
|
evnt->time,
|
|
_inputRoot,
|
|
evnt->type == GdkEventType.ButtonRelease
|
|
? evnt->button == 1
|
|
? RawMouseEventType.LeftButtonUp
|
|
: evnt->button == 3 ? RawMouseEventType.RightButtonUp : RawMouseEventType.MiddleButtonUp
|
|
: evnt->button == 1
|
|
? RawMouseEventType.LeftButtonDown
|
|
: evnt->button == 3 ? RawMouseEventType.RightButtonDown : RawMouseEventType.MiddleButtonDown,
|
|
new Point(evnt->x, evnt->y), GetModifierKeys(evnt->state));
|
|
OnInput(e);
|
|
return true;
|
|
}
|
|
|
|
protected virtual unsafe bool OnStateChanged(IntPtr w, IntPtr pev, IntPtr userData)
|
|
{
|
|
var ev = (GdkEventWindowState*) pev;
|
|
if (ev->changed_mask.HasFlag(GdkWindowState.Focused))
|
|
{
|
|
if(ev->new_window_state.HasFlag(GdkWindowState.Focused))
|
|
Activated?.Invoke();
|
|
else
|
|
Deactivated?.Invoke();
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private unsafe bool OnMotion(IntPtr w, IntPtr ev, IntPtr userdata)
|
|
{
|
|
var evnt = (GdkEventMotion*)ev;
|
|
var position = new Point(evnt->x, evnt->y);
|
|
Native.GdkEventRequestMotions(ev);
|
|
var e = new RawMouseEventArgs(
|
|
Gtk3Platform.Mouse,
|
|
evnt->time,
|
|
_inputRoot,
|
|
RawMouseEventType.Move,
|
|
position, GetModifierKeys(evnt->state));
|
|
OnInput(e);
|
|
|
|
return true;
|
|
}
|
|
private unsafe bool OnScroll(IntPtr w, IntPtr ev, IntPtr userdata)
|
|
{
|
|
var evnt = (GdkEventScroll*)ev;
|
|
|
|
//Ignore duplicates
|
|
if (evnt->time - _lastSmoothScrollEvent < 10 && evnt->direction != GdkScrollDirection.Smooth)
|
|
return true;
|
|
|
|
var delta = new Vector();
|
|
const double step = (double) 1;
|
|
if (evnt->direction == GdkScrollDirection.Down)
|
|
delta = new Vector(0, -step);
|
|
else if (evnt->direction == GdkScrollDirection.Up)
|
|
delta = new Vector(0, step);
|
|
else if (evnt->direction == GdkScrollDirection.Right)
|
|
delta = new Vector(-step, 0);
|
|
else if (evnt->direction == GdkScrollDirection.Left)
|
|
delta = new Vector(step, 0);
|
|
else if (evnt->direction == GdkScrollDirection.Smooth)
|
|
{
|
|
delta = new Vector(-evnt->delta_x, -evnt->delta_y);
|
|
_lastSmoothScrollEvent = evnt->time;
|
|
}
|
|
var e = new RawMouseWheelEventArgs(Gtk3Platform.Mouse, evnt->time, _inputRoot,
|
|
new Point(evnt->x, evnt->y), delta, GetModifierKeys(evnt->state));
|
|
OnInput(e);
|
|
return true;
|
|
}
|
|
|
|
private unsafe bool OnKeyEvent(IntPtr w, IntPtr pev, IntPtr userData)
|
|
{
|
|
var evnt = (GdkEventKey*) pev;
|
|
_lastKbdEvent = evnt->time;
|
|
if (Native.GtkImContextFilterKeypress(_imContext, pev))
|
|
return true;
|
|
var e = new RawKeyEventArgs(
|
|
Gtk3Platform.Keyboard,
|
|
evnt->time,
|
|
evnt->type == GdkEventType.KeyPress ? RawKeyEventType.KeyDown : RawKeyEventType.KeyUp,
|
|
Avalonia.Gtk.Common.KeyTransform.ConvertKey((GdkKey)evnt->keyval), GetModifierKeys((GdkModifierType)evnt->state));
|
|
OnInput(e);
|
|
return true;
|
|
}
|
|
|
|
private unsafe bool OnLeaveNotifyEvent(IntPtr w, IntPtr pev, IntPtr userData)
|
|
{
|
|
var evnt = (GdkEventCrossing*) pev;
|
|
var position = new Point(evnt->x, evnt->y);
|
|
OnInput(new RawMouseEventArgs(Gtk3Platform.Mouse,
|
|
evnt->time,
|
|
_inputRoot,
|
|
RawMouseEventType.Move,
|
|
position, GetModifierKeys(evnt->state)));
|
|
return true;
|
|
}
|
|
|
|
private unsafe bool OnCommit(IntPtr gtkwidget, IntPtr utf8string, IntPtr userdata)
|
|
{
|
|
OnInput(new RawTextInputEventArgs(Gtk3Platform.Keyboard, _lastKbdEvent, Utf8Buffer.StringFromPtr(utf8string)));
|
|
return true;
|
|
}
|
|
|
|
protected void ConnectEvent(string name, Native.D.signal_onevent handler)
|
|
=> Disposables.Add(Signal.Connect<Native.D.signal_onevent>(GtkWidget, name, handler));
|
|
void Connect<T>(string name, T handler) => Disposables.Add(Signal.Connect(GtkWidget, name, handler));
|
|
|
|
internal IntPtr CurrentCairoContext { get; private set; }
|
|
|
|
private bool OnDraw(IntPtr gtkwidget, IntPtr cairocontext, IntPtr userdata)
|
|
{
|
|
if (!Gtk3Platform.UseDeferredRendering)
|
|
{
|
|
CurrentCairoContext = cairocontext;
|
|
Paint?.Invoke(new Rect(ClientSize));
|
|
CurrentCairoContext = IntPtr.Zero;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private static Native.D.TickCallback PinnedStaticCallback = StaticTickCallback;
|
|
|
|
static bool StaticTickCallback(IntPtr widget, IntPtr clock, IntPtr userData)
|
|
{
|
|
var impl = (WindowBaseImpl) GCHandle.FromIntPtr(userData).Target;
|
|
impl.OnRenderTick();
|
|
return true;
|
|
}
|
|
|
|
public void SetNextRenderOperation(IDeferredRenderOperation op)
|
|
{
|
|
while (true)
|
|
{
|
|
lock (_lock)
|
|
{
|
|
if (_nextRenderOperation == null)
|
|
{
|
|
_nextRenderOperation = op;
|
|
return;
|
|
}
|
|
}
|
|
_canSetNextOperation.WaitOne();
|
|
}
|
|
|
|
}
|
|
|
|
private void OnRenderTick()
|
|
{
|
|
IDeferredRenderOperation op = null;
|
|
lock (_lock)
|
|
{
|
|
if (_nextRenderOperation != null)
|
|
{
|
|
op = _nextRenderOperation;
|
|
_nextRenderOperation = null;
|
|
}
|
|
_canSetNextOperation.Set();
|
|
}
|
|
if (op != null)
|
|
{
|
|
op?.RenderNow(null);
|
|
op?.Dispose();
|
|
}
|
|
}
|
|
|
|
|
|
public void Dispose() => DoDispose(false);
|
|
|
|
void DoDispose(bool fromDestroy)
|
|
{
|
|
if (_tickCallback.HasValue)
|
|
{
|
|
if (!GtkWidget.IsClosed)
|
|
Native.GtkWidgetRemoveTickCallback(GtkWidget, _tickCallback.Value);
|
|
_tickCallback = null;
|
|
}
|
|
|
|
//We are calling it here, since signal handler will be detached
|
|
if (!GtkWidget.IsClosed)
|
|
Closed?.Invoke();
|
|
foreach(var d in Disposables.AsEnumerable().Reverse())
|
|
d.Dispose();
|
|
Disposables.Clear();
|
|
|
|
if (!fromDestroy && !GtkWidget.IsClosed)
|
|
Native.GtkWindowClose(GtkWidget);
|
|
GtkWidget.Dispose();
|
|
|
|
if (_gcHandle.IsAllocated)
|
|
{
|
|
_gcHandle.Free();
|
|
}
|
|
}
|
|
|
|
public Size MaxClientSize
|
|
{
|
|
get
|
|
{
|
|
var s = Native.GtkWidgetGetScreen(GtkWidget);
|
|
return new Size(Native.GdkScreenGetWidth(s), Native.GdkScreenGetHeight(s));
|
|
}
|
|
}
|
|
|
|
public void SetMinMaxSize(Size minSize, Size maxSize)
|
|
{
|
|
if (GtkWidget.IsClosed)
|
|
return;
|
|
|
|
GdkGeometry geometry = new GdkGeometry();
|
|
geometry.min_width = minSize.Width > 0 ? (int)minSize.Width : -1;
|
|
geometry.min_height = minSize.Height > 0 ? (int)minSize.Height : -1;
|
|
geometry.max_width = !Double.IsInfinity(maxSize.Width) && maxSize.Width > 0 ? (int)maxSize.Width : 999999;
|
|
geometry.max_height = !Double.IsInfinity(maxSize.Height) && maxSize.Height > 0 ? (int)maxSize.Height : 999999;
|
|
|
|
Native.GtkWindowSetGeometryHints(GtkWidget, IntPtr.Zero, ref geometry, GdkWindowHints.GDK_HINT_MIN_SIZE | GdkWindowHints.GDK_HINT_MAX_SIZE);
|
|
}
|
|
|
|
public IMouseDevice MouseDevice => Gtk3Platform.Mouse;
|
|
|
|
public double Scaling => LastKnownScaleFactor = (int) (Native.GtkWidgetGetScaleFactor?.Invoke(GtkWidget) ?? 1);
|
|
|
|
public IPlatformHandle Handle => this;
|
|
|
|
string IPlatformHandle.HandleDescriptor => "HWND";
|
|
|
|
public Action Activated { get; set; }
|
|
public Func<bool> Closing { get; set; }
|
|
public Action Closed { get; set; }
|
|
public Action Deactivated { get; set; }
|
|
public Action<RawInputEventArgs> Input { get; set; }
|
|
public Action<Rect> Paint { get; set; }
|
|
public Action<Size> Resized { get; set; }
|
|
public Action<double> ScalingChanged { get; set; } //TODO
|
|
public Action<Point> PositionChanged { get; set; }
|
|
|
|
public void Activate() => Native.GtkWidgetActivate(GtkWidget);
|
|
|
|
public void Invalidate(Rect rect)
|
|
{
|
|
if(GtkWidget.IsClosed)
|
|
return;
|
|
var s = ClientSize;
|
|
Native.GtkWidgetQueueDrawArea(GtkWidget, 0, 0, (int) s.Width, (int) s.Height);
|
|
}
|
|
|
|
public void SetInputRoot(IInputRoot inputRoot) => _inputRoot = inputRoot;
|
|
|
|
void OnInput(RawInputEventArgs args)
|
|
{
|
|
Dispatcher.UIThread.Post(() => Input?.Invoke(args), DispatcherPriority.Input);
|
|
}
|
|
|
|
public Point PointToClient(Point point)
|
|
{
|
|
int x, y;
|
|
Native.GdkWindowGetOrigin(Native.GtkWidgetGetWindow(GtkWidget), out x, out y);
|
|
|
|
return new Point(point.X - x, point.Y - y);
|
|
}
|
|
|
|
public Point PointToScreen(Point point)
|
|
{
|
|
int x, y;
|
|
Native.GdkWindowGetOrigin(Native.GtkWidgetGetWindow(GtkWidget), out x, out y);
|
|
return new Point(point.X + x, point.Y + y);
|
|
}
|
|
|
|
public void SetCursor(IPlatformHandle cursor)
|
|
{
|
|
if (GtkWidget.IsClosed)
|
|
return;
|
|
Native.GdkWindowSetCursor(Native.GtkWidgetGetWindow(GtkWidget), cursor?.Handle ?? IntPtr.Zero);
|
|
}
|
|
|
|
public void Show() => Native.GtkWindowPresent(GtkWidget);
|
|
|
|
public void Hide() => Native.GtkWidgetHide(GtkWidget);
|
|
|
|
public void SetTopmost(bool value) => Native.GtkWindowSetKeepAbove(GtkWidget, value);
|
|
|
|
void GetGlobalPointer(out int x, out int y)
|
|
{
|
|
int mask;
|
|
Native.GdkWindowGetPointer(Native.GdkScreenGetRootWindow(Native.GtkWidgetGetScreen(GtkWidget)),
|
|
out x, out y, out mask);
|
|
}
|
|
|
|
public void BeginMoveDrag()
|
|
{
|
|
int x, y;
|
|
GetGlobalPointer(out x, out y);
|
|
Native.GdkWindowBeginMoveDrag(Native.GtkWidgetGetWindow(GtkWidget), 1, x, y, 0);
|
|
}
|
|
|
|
public void BeginResizeDrag(WindowEdge edge)
|
|
{
|
|
int x, y;
|
|
GetGlobalPointer(out x, out y);
|
|
Native.GdkWindowBeginResizeDrag(Native.GtkWidgetGetWindow(GtkWidget), edge, 1, x, y, 0);
|
|
}
|
|
|
|
|
|
public Size ClientSize { get; private set; }
|
|
public int LastKnownScaleFactor { get; private set; }
|
|
|
|
public void Resize(Size value)
|
|
{
|
|
if (GtkWidget.IsClosed)
|
|
return;
|
|
|
|
Native.GtkWindowResize(GtkWidget, (int)value.Width, (int)value.Height);
|
|
if (OverrideRedirect)
|
|
{
|
|
var size = ClientSize = value;
|
|
if (_lastSize != size)
|
|
{
|
|
Resized?.Invoke(size);
|
|
_lastSize = size;
|
|
}
|
|
}
|
|
}
|
|
|
|
public bool OverrideRedirect
|
|
{
|
|
get => _overrideRedirect;
|
|
set
|
|
{
|
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
|
{
|
|
Native.GdkWindowSetOverrideRedirect(Native.GtkWidgetGetWindow(GtkWidget), value);
|
|
_overrideRedirect = value;
|
|
}
|
|
}
|
|
}
|
|
|
|
public IScreenImpl Screen
|
|
{
|
|
get;
|
|
} = new ScreenImpl();
|
|
|
|
public Point Position
|
|
{
|
|
get
|
|
{
|
|
int x, y;
|
|
Native.GtkWindowGetPosition(GtkWidget, out x, out y);
|
|
return new Point(x, y);
|
|
}
|
|
set { Native.GtkWindowMove(GtkWidget, (int)value.X, (int)value.Y); }
|
|
}
|
|
|
|
IntPtr IPlatformHandle.Handle => Native.GetNativeGdkWindowHandle(Native.GtkWidgetGetWindow(GtkWidget));
|
|
public IEnumerable<object> Surfaces => new object[] {Handle, _framebuffer};
|
|
|
|
public IRenderer CreateRenderer(IRenderRoot root)
|
|
{
|
|
var loop = AvaloniaLocator.Current.GetService<IRenderLoop>();
|
|
return Gtk3Platform.UseDeferredRendering
|
|
? (IRenderer) new DeferredRenderer(root, loop)
|
|
: new ImmediateRenderer(root);
|
|
}
|
|
}
|
|
}
|
|
|