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.
942 lines
36 KiB
942 lines
36 KiB
using System;
|
|
using System.Collections.Generic;
|
|
using System.ComponentModel.DataAnnotations;
|
|
using System.Diagnostics;
|
|
using System.Linq;
|
|
using System.Reactive.Disposables;
|
|
using System.Text;
|
|
using Avalonia.Controls;
|
|
using Avalonia.Input;
|
|
using Avalonia.Input.Raw;
|
|
using Avalonia.OpenGL;
|
|
using Avalonia.Platform;
|
|
using Avalonia.Rendering;
|
|
using Avalonia.Threading;
|
|
using Avalonia.X11.Glx;
|
|
using static Avalonia.X11.XLib;
|
|
// ReSharper disable IdentifierTypo
|
|
// ReSharper disable StringLiteralTypo
|
|
namespace Avalonia.X11
|
|
{
|
|
unsafe class X11Window : IWindowImpl, IPopupImpl, IXI2Client
|
|
{
|
|
private readonly AvaloniaX11Platform _platform;
|
|
private readonly bool _popup;
|
|
private readonly X11Info _x11;
|
|
private bool _invalidated;
|
|
private XConfigureEvent? _configure;
|
|
private PixelPoint? _configurePoint;
|
|
private bool _triggeredExpose;
|
|
private IInputRoot _inputRoot;
|
|
private readonly IMouseDevice _mouse;
|
|
private readonly IKeyboardDevice _keyboard;
|
|
private PixelPoint? _position;
|
|
private PixelSize _realSize;
|
|
private IntPtr _handle;
|
|
private IntPtr _xic;
|
|
private IntPtr _renderHandle;
|
|
private bool _mapped;
|
|
private HashSet<X11Window> _transientChildren = new HashSet<X11Window>();
|
|
private X11Window _transientParent;
|
|
public object SyncRoot { get; } = new object();
|
|
|
|
class InputEventContainer
|
|
{
|
|
public RawInputEventArgs Event;
|
|
}
|
|
private readonly Queue<InputEventContainer> _inputQueue = new Queue<InputEventContainer>();
|
|
private InputEventContainer _lastEvent;
|
|
private bool _useRenderWindow = false;
|
|
public X11Window(AvaloniaX11Platform platform, bool popup)
|
|
{
|
|
_platform = platform;
|
|
_popup = popup;
|
|
_x11 = platform.Info;
|
|
_mouse = platform.MouseDevice;
|
|
_keyboard = platform.KeyboardDevice;
|
|
|
|
var glfeature = AvaloniaLocator.Current.GetService<IWindowingPlatformGlFeature>();
|
|
XSetWindowAttributes attr = new XSetWindowAttributes();
|
|
var valueMask = default(SetWindowValuemask);
|
|
|
|
attr.backing_store = 1;
|
|
attr.bit_gravity = Gravity.NorthWestGravity;
|
|
attr.win_gravity = Gravity.NorthWestGravity;
|
|
valueMask |= SetWindowValuemask.BackPixel | SetWindowValuemask.BorderPixel
|
|
| SetWindowValuemask.BackPixmap | SetWindowValuemask.BackingStore
|
|
| SetWindowValuemask.BitGravity | SetWindowValuemask.WinGravity;
|
|
|
|
if (popup)
|
|
{
|
|
attr.override_redirect = true;
|
|
valueMask |= SetWindowValuemask.OverrideRedirect;
|
|
}
|
|
|
|
XVisualInfo? visualInfo = null;
|
|
|
|
// OpenGL seems to be do weird things to it's current window which breaks resize sometimes
|
|
_useRenderWindow = glfeature != null;
|
|
|
|
var glx = glfeature as GlxGlPlatformFeature;
|
|
if (glx != null)
|
|
visualInfo = *glx.Display.VisualInfo;
|
|
else if (glfeature == null)
|
|
visualInfo = _x11.TransparentVisualInfo;
|
|
|
|
var egl = glfeature as EglGlPlatformFeature;
|
|
|
|
var visual = IntPtr.Zero;
|
|
var depth = 24;
|
|
if (visualInfo != null)
|
|
{
|
|
visual = visualInfo.Value.visual;
|
|
depth = (int)visualInfo.Value.depth;
|
|
attr.colormap = XCreateColormap(_x11.Display, _x11.RootWindow, visualInfo.Value.visual, 0);
|
|
valueMask |= SetWindowValuemask.ColorMap;
|
|
}
|
|
|
|
_handle = XCreateWindow(_x11.Display, _x11.RootWindow, 10, 10, 300, 200, 0,
|
|
depth,
|
|
(int)CreateWindowArgs.InputOutput,
|
|
visual,
|
|
new UIntPtr((uint)valueMask), ref attr);
|
|
|
|
if (_useRenderWindow)
|
|
_renderHandle = XCreateWindow(_x11.Display, _handle, 0, 0, 300, 200, 0, depth,
|
|
(int)CreateWindowArgs.InputOutput,
|
|
visual,
|
|
new UIntPtr((uint)(SetWindowValuemask.BorderPixel | SetWindowValuemask.BitGravity |
|
|
SetWindowValuemask.WinGravity | SetWindowValuemask.BackingStore)), ref attr);
|
|
else
|
|
_renderHandle = _handle;
|
|
|
|
Handle = new PlatformHandle(_handle, "XID");
|
|
_realSize = new PixelSize(300, 200);
|
|
platform.Windows[_handle] = OnEvent;
|
|
XEventMask ignoredMask = XEventMask.SubstructureRedirectMask
|
|
| XEventMask.ResizeRedirectMask
|
|
| XEventMask.PointerMotionHintMask;
|
|
if (platform.XI2 != null)
|
|
ignoredMask |= platform.XI2.AddWindow(_handle, this);
|
|
var mask = new IntPtr(0xffffff ^ (int)ignoredMask);
|
|
XSelectInput(_x11.Display, _handle, mask);
|
|
var protocols = new[]
|
|
{
|
|
_x11.Atoms.WM_DELETE_WINDOW
|
|
};
|
|
XSetWMProtocols(_x11.Display, _handle, protocols, protocols.Length);
|
|
XChangeProperty(_x11.Display, _handle, _x11.Atoms._NET_WM_WINDOW_TYPE, _x11.Atoms.XA_ATOM,
|
|
32, PropertyMode.Replace, new[] {_x11.Atoms._NET_WM_WINDOW_TYPE_NORMAL}, 1);
|
|
|
|
if (platform.Options.WmClass != null)
|
|
SetWmClass(platform.Options.WmClass);
|
|
|
|
var surfaces = new List<object>
|
|
{
|
|
new X11FramebufferSurface(_x11.DeferredDisplay, _renderHandle,
|
|
depth, () => Scaling)
|
|
};
|
|
|
|
if (egl != null)
|
|
surfaces.Insert(0,
|
|
new EglGlPlatformSurface((EglDisplay)egl.Display, egl.DeferredContext,
|
|
new SurfaceInfo(this, _x11.DeferredDisplay, _handle, _renderHandle)));
|
|
if (glx != null)
|
|
surfaces.Insert(0, new GlxGlPlatformSurface(glx.Display, glx.DeferredContext,
|
|
new SurfaceInfo(this, _x11.Display, _handle, _renderHandle)));
|
|
|
|
Surfaces = surfaces.ToArray();
|
|
UpdateMotifHints();
|
|
_xic = XCreateIC(_x11.Xim, XNames.XNInputStyle, XIMProperties.XIMPreeditNothing | XIMProperties.XIMStatusNothing,
|
|
XNames.XNClientWindow, _handle, IntPtr.Zero);
|
|
XFlush(_x11.Display);
|
|
}
|
|
|
|
class SurfaceInfo : EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo
|
|
{
|
|
private readonly X11Window _window;
|
|
private readonly IntPtr _display;
|
|
private readonly IntPtr _parent;
|
|
|
|
public SurfaceInfo(X11Window window, IntPtr display, IntPtr parent, IntPtr xid)
|
|
{
|
|
_window = window;
|
|
_display = display;
|
|
_parent = parent;
|
|
Handle = xid;
|
|
}
|
|
public IntPtr Handle { get; }
|
|
|
|
public PixelSize Size
|
|
{
|
|
get
|
|
{
|
|
XLockDisplay(_display);
|
|
XGetGeometry(_display, _parent, out var geo);
|
|
XResizeWindow(_display, Handle, geo.width, geo.height);
|
|
XUnlockDisplay(_display);
|
|
return new PixelSize(geo.width, geo.height);
|
|
}
|
|
}
|
|
|
|
public double Scaling => _window.Scaling;
|
|
}
|
|
|
|
void UpdateMotifHints()
|
|
{
|
|
var functions = MotifFunctions.Move | MotifFunctions.Close | MotifFunctions.Resize |
|
|
MotifFunctions.Minimize | MotifFunctions.Maximize;
|
|
var decorations = MotifDecorations.Menu | MotifDecorations.Title | MotifDecorations.Border |
|
|
MotifDecorations.Maximize | MotifDecorations.Minimize | MotifDecorations.ResizeH;
|
|
|
|
if (_popup || !_systemDecorations)
|
|
{
|
|
decorations = 0;
|
|
}
|
|
|
|
if (!_canResize)
|
|
{
|
|
functions &= ~(MotifFunctions.Resize | MotifFunctions.Maximize);
|
|
decorations &= ~(MotifDecorations.Maximize | MotifDecorations.ResizeH);
|
|
}
|
|
|
|
var hints = new MotifWmHints
|
|
{
|
|
flags = new IntPtr((int)(MotifFlags.Decorations | MotifFlags.Functions)),
|
|
decorations = new IntPtr((int)decorations),
|
|
functions = new IntPtr((int)functions)
|
|
};
|
|
|
|
XChangeProperty(_x11.Display, _handle,
|
|
_x11.Atoms._MOTIF_WM_HINTS, _x11.Atoms._MOTIF_WM_HINTS, 32,
|
|
PropertyMode.Replace, ref hints, 5);
|
|
}
|
|
|
|
void UpdateSizeHints(PixelSize? preResize)
|
|
{
|
|
var min = _minMaxSize.minSize;
|
|
var max = _minMaxSize.maxSize;
|
|
|
|
if (!_canResize)
|
|
max = min = _realSize;
|
|
|
|
if (preResize.HasValue)
|
|
{
|
|
var desired = preResize.Value;
|
|
max = new PixelSize(Math.Max(desired.Width, max.Width), Math.Max(desired.Height, max.Height));
|
|
min = new PixelSize(Math.Min(desired.Width, min.Width), Math.Min(desired.Height, min.Height));
|
|
}
|
|
|
|
var hints = new XSizeHints
|
|
{
|
|
min_width = min.Width,
|
|
min_height = min.Height
|
|
};
|
|
hints.height_inc = hints.width_inc = 1;
|
|
var flags = XSizeHintsFlags.PMinSize | XSizeHintsFlags.PResizeInc;
|
|
// People might be passing double.MaxValue
|
|
if (max.Width < 100000 && max.Height < 100000)
|
|
{
|
|
hints.max_width = max.Width;
|
|
hints.max_height = max.Height;
|
|
flags |= XSizeHintsFlags.PMaxSize;
|
|
}
|
|
|
|
hints.flags = (IntPtr)flags;
|
|
|
|
XSetWMNormalHints(_x11.Display, _handle, ref hints);
|
|
}
|
|
|
|
public Size ClientSize => new Size(_realSize.Width / Scaling, _realSize.Height / Scaling);
|
|
|
|
public double Scaling
|
|
{
|
|
get
|
|
{
|
|
lock (SyncRoot)
|
|
return _scaling;
|
|
|
|
}
|
|
private set => _scaling = value;
|
|
}
|
|
|
|
public IEnumerable<object> Surfaces { get; }
|
|
public Action<RawInputEventArgs> Input { get; set; }
|
|
public Action<Rect> Paint { get; set; }
|
|
public Action<Size> Resized { get; set; }
|
|
//TODO
|
|
public Action<double> ScalingChanged { get; set; }
|
|
public Action Deactivated { get; set; }
|
|
public Action Activated { get; set; }
|
|
public Func<bool> Closing { get; set; }
|
|
public Action<WindowState> WindowStateChanged { get; set; }
|
|
public Action Closed { get; set; }
|
|
public Action<PixelPoint> PositionChanged { get; set; }
|
|
|
|
public IRenderer CreateRenderer(IRenderRoot root) =>
|
|
new DeferredRenderer(root, AvaloniaLocator.Current.GetService<IRenderLoop>());
|
|
|
|
void OnEvent(XEvent ev)
|
|
{
|
|
lock (SyncRoot)
|
|
OnEventSync(ev);
|
|
}
|
|
void OnEventSync(XEvent ev)
|
|
{
|
|
if(XFilterEvent(ref ev, _handle))
|
|
return;
|
|
if (ev.type == XEventName.MapNotify)
|
|
{
|
|
_mapped = true;
|
|
if (_useRenderWindow)
|
|
XMapWindow(_x11.Display, _renderHandle);
|
|
}
|
|
else if (ev.type == XEventName.UnmapNotify)
|
|
_mapped = false;
|
|
else if (ev.type == XEventName.Expose)
|
|
{
|
|
if (!_triggeredExpose)
|
|
{
|
|
_triggeredExpose = true;
|
|
Dispatcher.UIThread.Post(() =>
|
|
{
|
|
_triggeredExpose = false;
|
|
DoPaint();
|
|
}, DispatcherPriority.Render);
|
|
}
|
|
}
|
|
else if (ev.type == XEventName.FocusIn)
|
|
{
|
|
if (ActivateTransientChildIfNeeded())
|
|
return;
|
|
Activated?.Invoke();
|
|
}
|
|
else if (ev.type == XEventName.FocusOut)
|
|
Deactivated?.Invoke();
|
|
else if (ev.type == XEventName.MotionNotify)
|
|
MouseEvent(RawMouseEventType.Move, ref ev, ev.MotionEvent.state);
|
|
else if (ev.type == XEventName.LeaveNotify)
|
|
MouseEvent(RawMouseEventType.LeaveWindow, ref ev, ev.CrossingEvent.state);
|
|
else if (ev.type == XEventName.PropertyNotify)
|
|
{
|
|
OnPropertyChange(ev.PropertyEvent.atom, ev.PropertyEvent.state == 0);
|
|
}
|
|
else if (ev.type == XEventName.ButtonPress)
|
|
{
|
|
if (ActivateTransientChildIfNeeded())
|
|
return;
|
|
if (ev.ButtonEvent.button < 4)
|
|
MouseEvent(ev.ButtonEvent.button == 1 ? RawMouseEventType.LeftButtonDown
|
|
: ev.ButtonEvent.button == 2 ? RawMouseEventType.MiddleButtonDown
|
|
: RawMouseEventType.RightButtonDown, ref ev, ev.ButtonEvent.state);
|
|
else
|
|
{
|
|
var delta = ev.ButtonEvent.button == 4
|
|
? new Vector(0, 1)
|
|
: ev.ButtonEvent.button == 5
|
|
? new Vector(0, -1)
|
|
: ev.ButtonEvent.button == 6
|
|
? new Vector(1, 0)
|
|
: new Vector(-1, 0);
|
|
ScheduleInput(new RawMouseWheelEventArgs(_mouse, (ulong)ev.ButtonEvent.time.ToInt64(),
|
|
_inputRoot, new Point(ev.ButtonEvent.x, ev.ButtonEvent.y), delta,
|
|
TranslateModifiers(ev.ButtonEvent.state)), ref ev);
|
|
}
|
|
|
|
}
|
|
else if (ev.type == XEventName.ButtonRelease)
|
|
{
|
|
if (ev.ButtonEvent.button < 4)
|
|
MouseEvent(ev.ButtonEvent.button == 1 ? RawMouseEventType.LeftButtonUp
|
|
: ev.ButtonEvent.button == 2 ? RawMouseEventType.MiddleButtonUp
|
|
: RawMouseEventType.RightButtonUp, ref ev, ev.ButtonEvent.state);
|
|
}
|
|
else if (ev.type == XEventName.ConfigureNotify)
|
|
{
|
|
if (ev.ConfigureEvent.window != _handle)
|
|
return;
|
|
var needEnqueue = (_configure == null);
|
|
_configure = ev.ConfigureEvent;
|
|
if (ev.ConfigureEvent.override_redirect || ev.ConfigureEvent.send_event)
|
|
_configurePoint = new PixelPoint(ev.ConfigureEvent.x, ev.ConfigureEvent.y);
|
|
else
|
|
{
|
|
XTranslateCoordinates(_x11.Display, _handle, _x11.RootWindow,
|
|
0, 0,
|
|
out var tx, out var ty, out _);
|
|
_configurePoint = new PixelPoint(tx, ty);
|
|
}
|
|
if (needEnqueue)
|
|
Dispatcher.UIThread.Post(() =>
|
|
{
|
|
if (_configure == null)
|
|
return;
|
|
var cev = _configure.Value;
|
|
var npos = _configurePoint.Value;
|
|
_configure = null;
|
|
_configurePoint = null;
|
|
|
|
var nsize = new PixelSize(cev.width, cev.height);
|
|
var changedSize = _realSize != nsize;
|
|
var changedPos = _position == null || npos != _position;
|
|
_realSize = nsize;
|
|
_position = npos;
|
|
bool updatedSizeViaScaling = false;
|
|
if (changedPos)
|
|
{
|
|
PositionChanged?.Invoke(npos);
|
|
updatedSizeViaScaling = UpdateScaling();
|
|
}
|
|
|
|
if (changedSize && !updatedSizeViaScaling)
|
|
Resized?.Invoke(ClientSize);
|
|
|
|
Dispatcher.UIThread.RunJobs(DispatcherPriority.Layout);
|
|
}, DispatcherPriority.Layout);
|
|
if (_useRenderWindow)
|
|
XConfigureResizeWindow(_x11.Display, _renderHandle, ev.ConfigureEvent.width,
|
|
ev.ConfigureEvent.height);
|
|
}
|
|
else if (ev.type == XEventName.DestroyNotify && ev.AnyEvent.window == _handle)
|
|
{
|
|
Cleanup();
|
|
}
|
|
else if (ev.type == XEventName.ClientMessage)
|
|
{
|
|
if (ev.ClientMessageEvent.message_type == _x11.Atoms.WM_PROTOCOLS)
|
|
{
|
|
if (ev.ClientMessageEvent.ptr1 == _x11.Atoms.WM_DELETE_WINDOW)
|
|
{
|
|
if (Closing?.Invoke() != true)
|
|
Dispose();
|
|
}
|
|
|
|
}
|
|
}
|
|
else if (ev.type == XEventName.KeyPress || ev.type == XEventName.KeyRelease)
|
|
{
|
|
if (ActivateTransientChildIfNeeded())
|
|
return;
|
|
var buffer = stackalloc byte[40];
|
|
|
|
var index = ev.KeyEvent.state.HasFlag(XModifierMask.ShiftMask);
|
|
|
|
// We need the latin key, since it's mainly used for hotkeys, we use a different API for text anyway
|
|
var key = (X11Key)XKeycodeToKeysym(_x11.Display, ev.KeyEvent.keycode, index ? 1 : 0).ToInt32();
|
|
|
|
// Manually switch the Shift index for the keypad,
|
|
// there should be a proper way to do this
|
|
if (ev.KeyEvent.state.HasFlag(XModifierMask.Mod2Mask)
|
|
&& key > X11Key.Num_Lock && key <= X11Key.KP_9)
|
|
key = (X11Key)XKeycodeToKeysym(_x11.Display, ev.KeyEvent.keycode, index ? 0 : 1).ToInt32();
|
|
|
|
|
|
ScheduleInput(new RawKeyEventArgs(_keyboard, (ulong)ev.KeyEvent.time.ToInt64(),
|
|
ev.type == XEventName.KeyPress ? RawKeyEventType.KeyDown : RawKeyEventType.KeyUp,
|
|
X11KeyTransform.ConvertKey(key), TranslateModifiers(ev.KeyEvent.state)), ref ev);
|
|
|
|
if (ev.type == XEventName.KeyPress)
|
|
{
|
|
var len = Xutf8LookupString(_xic, ref ev, buffer, 40, out _, out _);
|
|
if (len != 0)
|
|
{
|
|
var text = Encoding.UTF8.GetString(buffer, len);
|
|
if (text.Length == 1)
|
|
{
|
|
if (text[0] < ' ' || text[0] == 0x7f) //Control codes or DEL
|
|
return;
|
|
}
|
|
ScheduleInput(new RawTextInputEventArgs(_keyboard, (ulong)ev.KeyEvent.time.ToInt64(), text),
|
|
ref ev);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private bool UpdateScaling()
|
|
{
|
|
lock (SyncRoot)
|
|
{
|
|
var monitor = _platform.X11Screens.Screens.OrderBy(x => x.PixelDensity)
|
|
.FirstOrDefault(m => m.Bounds.Contains(Position));
|
|
var newScaling = monitor?.PixelDensity ?? Scaling;
|
|
if (Scaling != newScaling)
|
|
{
|
|
Console.WriteLine(
|
|
$"Updating scaling from {Scaling} to {newScaling} as a response to position change to {Position}");
|
|
var oldScaledSize = ClientSize;
|
|
Scaling = newScaling;
|
|
ScalingChanged?.Invoke(Scaling);
|
|
SetMinMaxSize(_scaledMinMaxSize.minSize, _scaledMinMaxSize.maxSize);
|
|
Resize(oldScaledSize, true);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private WindowState _lastWindowState;
|
|
public WindowState WindowState
|
|
{
|
|
get => _lastWindowState;
|
|
set
|
|
{
|
|
if(_lastWindowState == value)
|
|
return;
|
|
_lastWindowState = value;
|
|
if (value == WindowState.Minimized)
|
|
{
|
|
XIconifyWindow(_x11.Display, _handle, _x11.DefaultScreen);
|
|
}
|
|
else if (value == WindowState.Maximized)
|
|
{
|
|
SendNetWMMessage(_x11.Atoms._NET_WM_STATE, (IntPtr)0, _x11.Atoms._NET_WM_STATE_HIDDEN, IntPtr.Zero);
|
|
SendNetWMMessage(_x11.Atoms._NET_WM_STATE, (IntPtr)1, _x11.Atoms._NET_WM_STATE_MAXIMIZED_VERT,
|
|
_x11.Atoms._NET_WM_STATE_MAXIMIZED_HORZ);
|
|
}
|
|
else
|
|
{
|
|
SendNetWMMessage(_x11.Atoms._NET_WM_STATE, (IntPtr)0, _x11.Atoms._NET_WM_STATE_HIDDEN, IntPtr.Zero);
|
|
SendNetWMMessage(_x11.Atoms._NET_WM_STATE, (IntPtr)0, _x11.Atoms._NET_WM_STATE_MAXIMIZED_VERT,
|
|
_x11.Atoms._NET_WM_STATE_MAXIMIZED_HORZ);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void OnPropertyChange(IntPtr atom, bool hasValue)
|
|
{
|
|
if (atom == _x11.Atoms._NET_WM_STATE)
|
|
{
|
|
WindowState state = WindowState.Normal;
|
|
if(hasValue)
|
|
{
|
|
|
|
XGetWindowProperty(_x11.Display, _handle, _x11.Atoms._NET_WM_STATE, IntPtr.Zero, new IntPtr(256),
|
|
false, (IntPtr)Atom.XA_ATOM, out _, out _, out var nitems, out _,
|
|
out var prop);
|
|
int maximized = 0;
|
|
var pitems = (IntPtr*)prop.ToPointer();
|
|
for (var c = 0; c < nitems.ToInt32(); c++)
|
|
{
|
|
if (pitems[c] == _x11.Atoms._NET_WM_STATE_HIDDEN)
|
|
{
|
|
state = WindowState.Minimized;
|
|
break;
|
|
}
|
|
|
|
if (pitems[c] == _x11.Atoms._NET_WM_STATE_MAXIMIZED_HORZ ||
|
|
pitems[c] == _x11.Atoms._NET_WM_STATE_MAXIMIZED_VERT)
|
|
{
|
|
maximized++;
|
|
if (maximized == 2)
|
|
{
|
|
state = WindowState.Maximized;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
XFree(prop);
|
|
}
|
|
if (_lastWindowState != state)
|
|
{
|
|
_lastWindowState = state;
|
|
WindowStateChanged?.Invoke(state);
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
|
|
InputModifiers TranslateModifiers(XModifierMask state)
|
|
{
|
|
var rv = default(InputModifiers);
|
|
if (state.HasFlag(XModifierMask.Button1Mask))
|
|
rv |= InputModifiers.LeftMouseButton;
|
|
if (state.HasFlag(XModifierMask.Button2Mask))
|
|
rv |= InputModifiers.RightMouseButton;
|
|
if (state.HasFlag(XModifierMask.Button2Mask))
|
|
rv |= InputModifiers.MiddleMouseButton;
|
|
if (state.HasFlag(XModifierMask.ShiftMask))
|
|
rv |= InputModifiers.Shift;
|
|
if (state.HasFlag(XModifierMask.ControlMask))
|
|
rv |= InputModifiers.Control;
|
|
if (state.HasFlag(XModifierMask.Mod1Mask))
|
|
rv |= InputModifiers.Alt;
|
|
if (state.HasFlag(XModifierMask.Mod4Mask))
|
|
rv |= InputModifiers.Windows;
|
|
return rv;
|
|
}
|
|
|
|
private bool _systemDecorations = true;
|
|
private bool _canResize = true;
|
|
private const int MaxWindowDimension = 100000;
|
|
|
|
private (Size minSize, Size maxSize) _scaledMinMaxSize =
|
|
(new Size(1, 1), new Size(double.PositiveInfinity, double.PositiveInfinity));
|
|
|
|
private (PixelSize minSize, PixelSize maxSize) _minMaxSize = (new PixelSize(1, 1),
|
|
new PixelSize(MaxWindowDimension, MaxWindowDimension));
|
|
|
|
private double _scaling = 1;
|
|
|
|
void ScheduleInput(RawInputEventArgs args, ref XEvent xev)
|
|
{
|
|
_x11.LastActivityTimestamp = xev.ButtonEvent.time;
|
|
ScheduleInput(args);
|
|
}
|
|
|
|
public void ScheduleInput(RawInputEventArgs args)
|
|
{
|
|
if (args is RawMouseEventArgs mouse)
|
|
mouse.Position = mouse.Position / Scaling;
|
|
if (args is RawDragEvent drag)
|
|
drag.Location = drag.Location / Scaling;
|
|
|
|
_lastEvent = new InputEventContainer() {Event = args};
|
|
_inputQueue.Enqueue(_lastEvent);
|
|
if (_inputQueue.Count == 1)
|
|
{
|
|
Dispatcher.UIThread.Post(() =>
|
|
{
|
|
while (_inputQueue.Count > 0)
|
|
{
|
|
Dispatcher.UIThread.RunJobs(DispatcherPriority.Input + 1);
|
|
var ev = _inputQueue.Dequeue();
|
|
Input?.Invoke(ev.Event);
|
|
}
|
|
}, DispatcherPriority.Input);
|
|
}
|
|
}
|
|
|
|
void MouseEvent(RawMouseEventType type, ref XEvent ev, XModifierMask mods)
|
|
{
|
|
var mev = new RawMouseEventArgs(
|
|
_mouse, (ulong)ev.ButtonEvent.time.ToInt64(), _inputRoot,
|
|
type, new Point(ev.ButtonEvent.x, ev.ButtonEvent.y), TranslateModifiers(mods));
|
|
if(type == RawMouseEventType.Move && _inputQueue.Count>0 && _lastEvent.Event is RawMouseEventArgs ma)
|
|
if (ma.Type == RawMouseEventType.Move)
|
|
{
|
|
_lastEvent.Event = mev;
|
|
return;
|
|
}
|
|
ScheduleInput(mev, ref ev);
|
|
}
|
|
|
|
void DoPaint()
|
|
{
|
|
_invalidated = false;
|
|
Paint?.Invoke(new Rect());
|
|
}
|
|
|
|
public void Invalidate(Rect rect)
|
|
{
|
|
if(_invalidated)
|
|
return;
|
|
_invalidated = true;
|
|
Dispatcher.UIThread.InvokeAsync(() =>
|
|
{
|
|
if (_mapped)
|
|
DoPaint();
|
|
});
|
|
}
|
|
|
|
public IInputRoot InputRoot => _inputRoot;
|
|
|
|
public void SetInputRoot(IInputRoot inputRoot)
|
|
{
|
|
_inputRoot = inputRoot;
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
if (_handle != IntPtr.Zero)
|
|
{
|
|
XDestroyWindow(_x11.Display, _handle);
|
|
Cleanup();
|
|
}
|
|
}
|
|
|
|
void Cleanup()
|
|
{
|
|
SetTransientParent(null, false);
|
|
if (_xic != IntPtr.Zero)
|
|
{
|
|
XDestroyIC(_xic);
|
|
_xic = IntPtr.Zero;
|
|
}
|
|
|
|
if (_handle != IntPtr.Zero)
|
|
{
|
|
XDestroyWindow(_x11.Display, _handle);
|
|
_platform.Windows.Remove(_handle);
|
|
_platform.XI2?.OnWindowDestroyed(_handle);
|
|
_handle = IntPtr.Zero;
|
|
Closed?.Invoke();
|
|
}
|
|
|
|
if (_useRenderWindow && _renderHandle != IntPtr.Zero)
|
|
{
|
|
XDestroyWindow(_x11.Display, _renderHandle);
|
|
_renderHandle = IntPtr.Zero;
|
|
}
|
|
}
|
|
|
|
bool ActivateTransientChildIfNeeded()
|
|
{
|
|
if (_transientChildren.Count == 0)
|
|
return false;
|
|
var child = _transientChildren.First();
|
|
if (!child.ActivateTransientChildIfNeeded())
|
|
child.Activate();
|
|
return true;
|
|
}
|
|
|
|
void SetTransientParent(X11Window window, bool informServer = true)
|
|
{
|
|
_transientParent?._transientChildren.Remove(this);
|
|
_transientParent = window;
|
|
_transientParent?._transientChildren.Add(this);
|
|
if (informServer)
|
|
XSetTransientForHint(_x11.Display, _handle, _transientParent?._handle ?? IntPtr.Zero);
|
|
}
|
|
|
|
public void Show()
|
|
{
|
|
SetTransientParent(null);
|
|
ShowCore();
|
|
}
|
|
|
|
void ShowCore()
|
|
{
|
|
XMapWindow(_x11.Display, _handle);
|
|
XFlush(_x11.Display);
|
|
}
|
|
|
|
public void Hide() => XUnmapWindow(_x11.Display, _handle);
|
|
|
|
|
|
public Point PointToClient(PixelPoint point) => new Point((point.X - Position.X) / Scaling, (point.Y - Position.Y) / Scaling);
|
|
|
|
public PixelPoint PointToScreen(Point point) => new PixelPoint(
|
|
(int)(point.X * Scaling + Position.X),
|
|
(int)(point.Y * Scaling + Position.Y));
|
|
|
|
public void SetSystemDecorations(bool enabled)
|
|
{
|
|
_systemDecorations = enabled;
|
|
UpdateMotifHints();
|
|
}
|
|
|
|
|
|
public void Resize(Size clientSize) => Resize(clientSize, false);
|
|
|
|
PixelSize ToPixelSize(Size size) => new PixelSize((int)(size.Width * Scaling), (int)(size.Height * Scaling));
|
|
|
|
void Resize(Size clientSize, bool force)
|
|
{
|
|
if (!force && clientSize == ClientSize)
|
|
return;
|
|
|
|
var needImmediatePopupResize = clientSize != ClientSize;
|
|
|
|
var pixelSize = ToPixelSize(clientSize);
|
|
UpdateSizeHints(pixelSize);
|
|
XConfigureResizeWindow(_x11.Display, _handle, pixelSize);
|
|
if (_useRenderWindow)
|
|
XConfigureResizeWindow(_x11.Display, _renderHandle, pixelSize);
|
|
XFlush(_x11.Display);
|
|
|
|
if (force || (_popup && needImmediatePopupResize))
|
|
{
|
|
_realSize = pixelSize;
|
|
Resized?.Invoke(ClientSize);
|
|
}
|
|
}
|
|
|
|
public void CanResize(bool value)
|
|
{
|
|
_canResize = value;
|
|
UpdateMotifHints();
|
|
UpdateSizeHints(null);
|
|
}
|
|
|
|
public void SetCursor(IPlatformHandle cursor)
|
|
{
|
|
if (cursor == null)
|
|
XDefineCursor(_x11.Display, _handle, _x11.DefaultCursor);
|
|
else
|
|
{
|
|
if (cursor.HandleDescriptor != "XCURSOR")
|
|
throw new ArgumentException("Expected XCURSOR handle type");
|
|
XDefineCursor(_x11.Display, _handle, cursor.Handle);
|
|
}
|
|
}
|
|
|
|
public IPlatformHandle Handle { get; }
|
|
|
|
public PixelPoint Position
|
|
{
|
|
get => _position ?? default;
|
|
set
|
|
{
|
|
var changes = new XWindowChanges
|
|
{
|
|
x = (int)value.X,
|
|
y = (int)value.Y
|
|
};
|
|
XConfigureWindow(_x11.Display, _handle, ChangeWindowFlags.CWX | ChangeWindowFlags.CWY,
|
|
ref changes);
|
|
XFlush(_x11.Display);
|
|
|
|
}
|
|
}
|
|
|
|
public IMouseDevice MouseDevice => _mouse;
|
|
|
|
public void Activate()
|
|
{
|
|
if (_x11.Atoms._NET_ACTIVE_WINDOW != IntPtr.Zero)
|
|
{
|
|
SendNetWMMessage(_x11.Atoms._NET_ACTIVE_WINDOW, (IntPtr)1, _x11.LastActivityTimestamp,
|
|
IntPtr.Zero);
|
|
}
|
|
else
|
|
{
|
|
XRaiseWindow(_x11.Display, _handle);
|
|
XSetInputFocus(_x11.Display, _handle, 0, IntPtr.Zero);
|
|
}
|
|
}
|
|
|
|
|
|
public IScreenImpl Screen => _platform.Screens;
|
|
|
|
public Size MaxClientSize => _platform.X11Screens.Screens.Select(s => s.Bounds.Size.ToSize(s.PixelDensity))
|
|
.OrderByDescending(x => x.Width + x.Height).FirstOrDefault();
|
|
|
|
|
|
void SendNetWMMessage(IntPtr message_type, IntPtr l0,
|
|
IntPtr? l1 = null, IntPtr? l2 = null, IntPtr? l3 = null, IntPtr? l4 = null)
|
|
{
|
|
var xev = new XEvent
|
|
{
|
|
ClientMessageEvent =
|
|
{
|
|
type = XEventName.ClientMessage,
|
|
send_event = true,
|
|
window = _handle,
|
|
message_type = message_type,
|
|
format = 32,
|
|
ptr1 = l0,
|
|
ptr2 = l1 ?? IntPtr.Zero,
|
|
ptr3 = l2 ?? IntPtr.Zero,
|
|
ptr4 = l3 ?? IntPtr.Zero
|
|
}
|
|
};
|
|
xev.ClientMessageEvent.ptr4 = l4 ?? IntPtr.Zero;
|
|
XSendEvent(_x11.Display, _x11.RootWindow, false,
|
|
new IntPtr((int)(EventMask.SubstructureRedirectMask | EventMask.SubstructureNotifyMask)), ref xev);
|
|
|
|
}
|
|
|
|
void BeginMoveResize(NetWmMoveResize side)
|
|
{
|
|
var pos = GetCursorPos(_x11);
|
|
XUngrabPointer(_x11.Display, new IntPtr(0));
|
|
SendNetWMMessage (_x11.Atoms._NET_WM_MOVERESIZE, (IntPtr) pos.x, (IntPtr) pos.y,
|
|
(IntPtr) side,
|
|
(IntPtr) 1, (IntPtr)1); // left button
|
|
}
|
|
|
|
public void BeginMoveDrag()
|
|
{
|
|
BeginMoveResize(NetWmMoveResize._NET_WM_MOVERESIZE_MOVE);
|
|
}
|
|
|
|
public void BeginResizeDrag(WindowEdge edge)
|
|
{
|
|
var side = NetWmMoveResize._NET_WM_MOVERESIZE_CANCEL;
|
|
if (edge == WindowEdge.East)
|
|
side = NetWmMoveResize._NET_WM_MOVERESIZE_SIZE_RIGHT;
|
|
if (edge == WindowEdge.North)
|
|
side = NetWmMoveResize._NET_WM_MOVERESIZE_SIZE_TOP;
|
|
if (edge == WindowEdge.South)
|
|
side = NetWmMoveResize._NET_WM_MOVERESIZE_SIZE_BOTTOM;
|
|
if (edge == WindowEdge.West)
|
|
side = NetWmMoveResize._NET_WM_MOVERESIZE_SIZE_LEFT;
|
|
if (edge == WindowEdge.NorthEast)
|
|
side = NetWmMoveResize._NET_WM_MOVERESIZE_SIZE_TOPRIGHT;
|
|
if (edge == WindowEdge.NorthWest)
|
|
side = NetWmMoveResize._NET_WM_MOVERESIZE_SIZE_TOPLEFT;
|
|
if (edge == WindowEdge.SouthEast)
|
|
side = NetWmMoveResize._NET_WM_MOVERESIZE_SIZE_BOTTOMRIGHT;
|
|
if (edge == WindowEdge.SouthWest)
|
|
side = NetWmMoveResize._NET_WM_MOVERESIZE_SIZE_BOTTOMLEFT;
|
|
BeginMoveResize(side);
|
|
}
|
|
|
|
public void SetTitle(string title)
|
|
{
|
|
var data = Encoding.UTF8.GetBytes(title);
|
|
fixed (void* pdata = data)
|
|
{
|
|
XChangeProperty(_x11.Display, _handle, _x11.Atoms._NET_WM_NAME, _x11.Atoms.UTF8_STRING, 8,
|
|
PropertyMode.Replace, pdata, data.Length);
|
|
XStoreName(_x11.Display, _handle, title);
|
|
}
|
|
}
|
|
|
|
public void SetWmClass(string wmClass)
|
|
{
|
|
var data = Encoding.ASCII.GetBytes(wmClass);
|
|
fixed (void* pdata = data)
|
|
{
|
|
XChangeProperty(_x11.Display, _handle, _x11.Atoms.XA_WM_CLASS, _x11.Atoms.XA_STRING, 8,
|
|
PropertyMode.Replace, pdata, data.Length);
|
|
}
|
|
}
|
|
|
|
public void SetMinMaxSize(Size minSize, Size maxSize)
|
|
{
|
|
_scaledMinMaxSize = (minSize, maxSize);
|
|
var min = new PixelSize(
|
|
(int)(minSize.Width < 1 ? 1 : minSize.Width * Scaling),
|
|
(int)(minSize.Height < 1 ? 1 : minSize.Height * Scaling));
|
|
|
|
const int maxDim = MaxWindowDimension;
|
|
var max = new PixelSize(
|
|
(int)(maxSize.Width > maxDim ? maxDim : Math.Max(min.Width, maxSize.Width * Scaling)),
|
|
(int)(maxSize.Height > maxDim ? maxDim : Math.Max(min.Height, maxSize.Height * Scaling)));
|
|
|
|
_minMaxSize = (min, max);
|
|
UpdateSizeHints(null);
|
|
}
|
|
|
|
public void SetTopmost(bool value)
|
|
{
|
|
SendNetWMMessage(_x11.Atoms._NET_WM_STATE,
|
|
(IntPtr)(value ? 1 : 0), _x11.Atoms._NET_WM_STATE_ABOVE, IntPtr.Zero);
|
|
}
|
|
|
|
public void ShowDialog(IWindowImpl parent)
|
|
{
|
|
SetTransientParent((X11Window)parent);
|
|
ShowCore();
|
|
}
|
|
|
|
public void SetIcon(IWindowIconImpl icon)
|
|
{
|
|
var data = ((X11IconData)icon).Data;
|
|
fixed (void* pdata = data)
|
|
XChangeProperty(_x11.Display, _handle, _x11.Atoms._NET_WM_ICON,
|
|
new IntPtr((int)Atom.XA_CARDINAL), 32, PropertyMode.Replace,
|
|
pdata, data.Length);
|
|
}
|
|
|
|
public void ShowTaskbarIcon(bool value)
|
|
{
|
|
SendNetWMMessage(_x11.Atoms._NET_WM_STATE,
|
|
(IntPtr)(value ? 0 : 1), _x11.Atoms._NET_WM_STATE_SKIP_TASKBAR, IntPtr.Zero);
|
|
}
|
|
}
|
|
}
|
|
|