Add Silk.NET.Input handling and hit-testing

This commit is contained in:
Yoshi Askharoun
2026-07-25 19:11:36 -05:00
parent 7d343e646f
commit 2bd15a4562
9 changed files with 443 additions and 23 deletions
+11 -2
View File
@@ -1,13 +1,22 @@
using Microsoft.Iris.Input;
namespace Microsoft.Iris.Render.OpenGL
{
/// <summary>
/// Holds the current raw-input callback sink. The window (see <see cref="GLRenderWindow"/>)
/// translates Silk.NET input events and dispatches them here.
/// Holds the current raw-input callback sink and mouse-capture site. The input
/// translator (see <see cref="GLInputTranslator"/>) reads <see cref="Callbacks"/>
/// each event and dispatches translated Silk.NET input to it.
/// </summary>
public sealed class GLInputSystem : IInputSystem
{
public IRawInputCallbacks? Callbacks { get; private set; }
/// <summary>
/// The visual that has grabbed the mouse via <see cref="IRenderWindow.SetCapture"/>,
/// or null. When set, mouse events are routed to it regardless of hit-testing.
/// </summary>
public IRawInputSite? CaptureSite { get; set; }
public void RegisterRawInputCallbacks(IRawInputCallbacks handlers) => Callbacks = handlers;
public void UnregisterRawInputCallbacks() => Callbacks = null;
@@ -0,0 +1,326 @@
using System;
using System.Collections.Generic;
using Microsoft.Iris.Input;
using Silk.NET.Input;
using SilkKey = Silk.NET.Input.Key;
using SilkMouseButton = Silk.NET.Input.MouseButton;
namespace Microsoft.Iris.Render.OpenGL
{
/// <summary>
/// Translates Silk.NET.Input keyboard/mouse events into the render API's raw-input
/// protocol (<see cref="IRawInputCallbacks"/>). Message identifiers match what the
/// UIX <c>InputManager</c> expects: keyboard uses the <see cref="KeyboardMessageId"/>
/// ordinals, mouse uses Win32 <c>WM_*</c> codes.
/// </summary>
public sealed class GLInputTranslator : IDisposable
{
// Win32 mouse messages (the values MouseDevice.OnRawInput switches on).
private const uint WM_MOUSEMOVE = 0x0200;
private const uint WM_LBUTTONDOWN = 0x0201, WM_LBUTTONUP = 0x0202, WM_LBUTTONDBLCLK = 0x0203;
private const uint WM_RBUTTONDOWN = 0x0204, WM_RBUTTONUP = 0x0205, WM_RBUTTONDBLCLK = 0x0206;
private const uint WM_MBUTTONDOWN = 0x0207, WM_MBUTTONUP = 0x0208, WM_MBUTTONDBLCLK = 0x0209;
private const uint WM_MOUSEWHEEL = 0x020A;
private const uint WM_XBUTTONDOWN = 0x020B, WM_XBUTTONUP = 0x020C, WM_XBUTTONDBLCLK = 0x020D;
private readonly IInputContext m_context;
private readonly GLInputSystem m_input;
private readonly GLRenderWindow m_window;
// Per-key repeat counters so RawKeyboardData._repCount reflects auto-repeat,
// which the InputManager uses for key coalescing.
private readonly Dictionary<SilkKey, uint> m_repeat = new Dictionary<SilkKey, uint>();
public GLInputTranslator(IInputContext context, GLInputSystem input, GLRenderWindow window)
{
m_context = context;
m_input = input;
m_window = window;
foreach (IKeyboard keyboard in context.Keyboards)
Hook(keyboard);
foreach (IMouse mouse in context.Mice)
Hook(mouse);
context.ConnectionChanged += OnConnectionChanged;
}
private void OnConnectionChanged(IInputDevice device, bool connected)
{
if (!connected)
return;
if (device is IKeyboard keyboard)
Hook(keyboard);
else if (device is IMouse mouse)
Hook(mouse);
}
private void Hook(IKeyboard keyboard)
{
keyboard.KeyDown += OnKeyDown;
keyboard.KeyUp += OnKeyUp;
keyboard.KeyChar += OnKeyChar;
}
private void Hook(IMouse mouse)
{
mouse.MouseMove += OnMouseMove;
mouse.MouseDown += OnMouseDown;
mouse.MouseUp += OnMouseUp;
mouse.Scroll += OnScroll;
mouse.DoubleClick += OnDoubleClick;
}
// ---- Keyboard ------------------------------------------------------------
private void OnKeyDown(IKeyboard keyboard, SilkKey key, int scanCode)
{
IRawInputCallbacks? cb = m_input.Callbacks;
if (cb == null)
return;
uint repeat = m_repeat.TryGetValue(key, out uint prev) ? prev + 1 : 1;
m_repeat[key] = repeat;
bool system = IsAltDown();
uint message = (uint)(system ? KeyboardMessageId.SysDown : KeyboardMessageId.Down);
var data = new RawKeyboardData(MapKey(key), scanCode, repeat, 0, InputDeviceType.Keyboard);
cb.HandleRawKeyboardInput(message, ComputeModifiers(), ref data);
}
private void OnKeyUp(IKeyboard keyboard, SilkKey key, int scanCode)
{
m_repeat.Remove(key);
IRawInputCallbacks? cb = m_input.Callbacks;
if (cb == null)
return;
bool system = IsAltDown();
uint message = (uint)(system ? KeyboardMessageId.SysUp : KeyboardMessageId.Up);
var data = new RawKeyboardData(MapKey(key), scanCode, 1, 0, InputDeviceType.Keyboard);
cb.HandleRawKeyboardInput(message, ComputeModifiers(), ref data);
}
private void OnKeyChar(IKeyboard keyboard, char character)
{
IRawInputCallbacks? cb = m_input.Callbacks;
if (cb == null)
return;
bool system = IsAltDown();
uint message = (uint)(system ? KeyboardMessageId.SysChar : KeyboardMessageId.Char);
// Character messages carry the char in _virtualKey (see KeyboardDevice.OnRawKeyCharacter).
var data = new RawKeyboardData((Keys)character, 0, 1, 0, InputDeviceType.Keyboard);
cb.HandleRawKeyboardInput(message, ComputeModifiers(), ref data);
}
// ---- Mouse ---------------------------------------------------------------
private void OnMouseMove(IMouse mouse, System.Numerics.Vector2 position)
=> DispatchMouse(mouse, WM_MOUSEMOVE, MouseButtons.None, 0);
private void OnMouseDown(IMouse mouse, SilkMouseButton button)
{
(uint message, MouseButtons iris) = button switch
{
SilkMouseButton.Left => (WM_LBUTTONDOWN, MouseButtons.Left),
SilkMouseButton.Right => (WM_RBUTTONDOWN, MouseButtons.Right),
SilkMouseButton.Middle => (WM_MBUTTONDOWN, MouseButtons.Middle),
SilkMouseButton.Button4 => (WM_XBUTTONDOWN, MouseButtons.XButton1),
SilkMouseButton.Button5 => (WM_XBUTTONDOWN, MouseButtons.XButton2),
_ => (0u, MouseButtons.None),
};
if (message != 0)
DispatchMouse(mouse, message, iris, 0);
}
private void OnMouseUp(IMouse mouse, SilkMouseButton button)
{
(uint message, MouseButtons iris) = button switch
{
SilkMouseButton.Left => (WM_LBUTTONUP, MouseButtons.Left),
SilkMouseButton.Right => (WM_RBUTTONUP, MouseButtons.Right),
SilkMouseButton.Middle => (WM_MBUTTONUP, MouseButtons.Middle),
SilkMouseButton.Button4 => (WM_XBUTTONUP, MouseButtons.XButton1),
SilkMouseButton.Button5 => (WM_XBUTTONUP, MouseButtons.XButton2),
_ => (0u, MouseButtons.None),
};
if (message != 0)
DispatchMouse(mouse, message, iris, 0);
}
private void OnDoubleClick(IMouse mouse, SilkMouseButton button, System.Numerics.Vector2 position)
{
(uint message, MouseButtons iris) = button switch
{
SilkMouseButton.Left => (WM_LBUTTONDBLCLK, MouseButtons.Left),
SilkMouseButton.Right => (WM_RBUTTONDBLCLK, MouseButtons.Right),
SilkMouseButton.Middle => (WM_MBUTTONDBLCLK, MouseButtons.Middle),
SilkMouseButton.Button4 => (WM_XBUTTONDBLCLK, MouseButtons.XButton1),
SilkMouseButton.Button5 => (WM_XBUTTONDBLCLK, MouseButtons.XButton2),
_ => (0u, MouseButtons.None),
};
if (message != 0)
DispatchMouse(mouse, message, iris, 0, doubleClick: true);
}
private void OnScroll(IMouse mouse, ScrollWheel wheel)
=> DispatchMouse(mouse, WM_MOUSEWHEEL, MouseButtons.None, (int)(wheel.Y * 120f));
private void DispatchMouse(IMouse mouse, uint message, MouseButtons button, int wheelDelta, bool doubleClick = false)
{
IRawInputCallbacks? cb = m_input.Callbacks;
if (cb == null)
return;
System.Numerics.Vector2 pos = mouse.Position;
int clientX = (int)pos.X;
int clientY = (int)pos.Y;
IVisual? natural = m_window.HitTest(new Silk.NET.Maths.Vector2D<float>(pos.X, pos.Y));
IVisual? capture = m_input.CaptureSite as IVisual ?? natural;
var data = new RawMouseData(
visCapture: capture!,
visNatural: natural!,
positionX: clientX,
positionY: clientY,
naturalX: clientX,
naturalY: clientY,
physicalX: clientX,
physicalY: clientY,
screenX: clientX + m_window.Left,
screenY: clientY + m_window.Top,
button: button,
wheelDelta: wheelDelta);
InputModifiers modifiers = ComputeModifiers(mouse);
if (doubleClick)
modifiers |= InputModifiers.DoubleClick;
cb.HandleRawMouseInput(message, modifiers, ref data);
}
// ---- Modifiers -----------------------------------------------------------
private bool IsAltDown()
{
foreach (IKeyboard k in m_context.Keyboards)
{
if (k.IsKeyPressed(SilkKey.AltLeft) || k.IsKeyPressed(SilkKey.AltRight))
return true;
}
return false;
}
private InputModifiers ComputeModifiers(IMouse? mouse = null)
{
InputModifiers m = InputModifiers.None;
foreach (IKeyboard k in m_context.Keyboards)
{
if (k.IsKeyPressed(SilkKey.ControlLeft) || k.IsKeyPressed(SilkKey.ControlRight)) m |= InputModifiers.ControlKey;
if (k.IsKeyPressed(SilkKey.ShiftLeft) || k.IsKeyPressed(SilkKey.ShiftRight)) m |= InputModifiers.ShiftKey;
if (k.IsKeyPressed(SilkKey.AltLeft) || k.IsKeyPressed(SilkKey.AltRight)) m |= InputModifiers.AltKey;
if (k.IsKeyPressed(SilkKey.SuperLeft) || k.IsKeyPressed(SilkKey.SuperRight)) m |= InputModifiers.WindowsKey;
}
mouse ??= FirstMouse();
if (mouse != null)
{
if (mouse.IsButtonPressed(SilkMouseButton.Left)) m |= InputModifiers.LeftMouse;
if (mouse.IsButtonPressed(SilkMouseButton.Right)) m |= InputModifiers.RightMouse;
if (mouse.IsButtonPressed(SilkMouseButton.Middle)) m |= InputModifiers.MiddleMouse;
if (mouse.IsButtonPressed(SilkMouseButton.Button4)) m |= InputModifiers.XMouse1;
if (mouse.IsButtonPressed(SilkMouseButton.Button5)) m |= InputModifiers.XMouse2;
}
return m;
}
private IMouse? FirstMouse()
{
foreach (IMouse mouse in m_context.Mice)
return mouse;
return null;
}
public void Dispose()
{
m_context.ConnectionChanged -= OnConnectionChanged;
foreach (IKeyboard keyboard in m_context.Keyboards)
{
keyboard.KeyDown -= OnKeyDown;
keyboard.KeyUp -= OnKeyUp;
keyboard.KeyChar -= OnKeyChar;
}
foreach (IMouse mouse in m_context.Mice)
{
mouse.MouseMove -= OnMouseMove;
mouse.MouseDown -= OnMouseDown;
mouse.MouseUp -= OnMouseUp;
mouse.Scroll -= OnScroll;
mouse.DoubleClick -= OnDoubleClick;
}
m_context.Dispose();
}
// ---- Key mapping (Silk.NET.Input.Key -> Win32 virtual key codes) ---------
private static Keys MapKey(SilkKey key) => key switch
{
>= SilkKey.A and <= SilkKey.Z => Keys.A + (key - SilkKey.A),
>= SilkKey.Number0 and <= SilkKey.Number9 => Keys.D0 + (key - SilkKey.Number0),
>= SilkKey.Keypad0 and <= SilkKey.Keypad9 => Keys.NumPad0 + (key - SilkKey.Keypad0),
>= SilkKey.F1 and <= SilkKey.F24 => Keys.F1 + (key - SilkKey.F1),
SilkKey.Space => Keys.Space,
SilkKey.Enter => Keys.Return,
SilkKey.KeypadEnter => Keys.Return,
SilkKey.Escape => Keys.Escape,
SilkKey.Tab => Keys.Tab,
SilkKey.Backspace => Keys.Back,
SilkKey.Insert => Keys.Insert,
SilkKey.Delete => Keys.Delete,
SilkKey.Home => Keys.Home,
SilkKey.End => Keys.End,
SilkKey.PageUp => Keys.PageUp,
SilkKey.PageDown => Keys.PageDown,
SilkKey.Left => Keys.Left,
SilkKey.Right => Keys.Right,
SilkKey.Up => Keys.Up,
SilkKey.Down => Keys.Down,
SilkKey.CapsLock => Keys.CapsLock,
SilkKey.NumLock => Keys.NumLock,
SilkKey.ScrollLock => Keys.Scroll,
SilkKey.PrintScreen => Keys.PrintScreen,
SilkKey.Pause => Keys.Pause,
SilkKey.Menu => Keys.Apps,
SilkKey.ShiftLeft => Keys.LShiftKey,
SilkKey.ShiftRight => Keys.RShiftKey,
SilkKey.ControlLeft => Keys.LControlKey,
SilkKey.ControlRight => Keys.RControlKey,
SilkKey.AltLeft => Keys.LMenu,
SilkKey.AltRight => Keys.RMenu,
SilkKey.SuperLeft => Keys.LWin,
SilkKey.SuperRight => Keys.RWin,
SilkKey.KeypadDivide => Keys.Divide,
SilkKey.KeypadMultiply => Keys.Multiply,
SilkKey.KeypadSubtract => Keys.Subtract,
SilkKey.KeypadAdd => Keys.Add,
SilkKey.KeypadDecimal => Keys.Decimal,
SilkKey.Semicolon => Keys.OemSemicolon,
SilkKey.Equal => Keys.OemPlus,
SilkKey.Comma => Keys.OemComma,
SilkKey.Minus => Keys.OemMinus,
SilkKey.Period => Keys.OemPeriod,
SilkKey.Slash => Keys.OemQuestion,
SilkKey.GraveAccent => Keys.OemTilde,
SilkKey.LeftBracket => Keys.OemOpenBrackets,
SilkKey.BackSlash => Keys.OemPipe,
SilkKey.RightBracket => Keys.OemCloseBrackets,
SilkKey.Apostrophe => Keys.OemQuotes,
_ => Keys.None,
};
}
}
@@ -1,5 +1,6 @@
using System;
using System.Threading;
using Silk.NET.Input;
using Silk.NET.Maths;
using Silk.NET.OpenGL;
using Silk.NET.Windowing;
@@ -23,6 +24,8 @@ namespace Microsoft.Iris.Render.OpenGL
private GL? m_gl;
private SceneRenderer? m_renderer;
private GLDisplayManager? m_displayManager;
private IInputContext? m_inputContext;
private GLInputTranslator? m_inputTranslator;
private GraphicsRenderingQuality m_quality;
private SoundDeviceType m_soundType;
@@ -73,6 +76,10 @@ namespace Microsoft.Iris.Render.OpenGL
m_displayManager = new GLDisplayManager(m_silkWindow);
m_session.GraphicsDevice = new GLGraphicsDevice(m_gl, m_quality, RenderNow);
m_session.SoundDevice = new GLSoundDevice(m_soundType);
m_inputContext = m_silkWindow.CreateInput();
m_inputTranslator = new GLInputTranslator(m_inputContext, (GLInputSystem)m_session.InputSystem, m_window);
m_window.RaiseLoad();
}
@@ -121,6 +128,7 @@ namespace Microsoft.Iris.Render.OpenGL
public void Dispose()
{
m_inputTranslator?.Dispose();
m_renderer?.Dispose();
m_session.Dispose();
if (!m_silkWindow.IsClosing)
@@ -13,18 +13,24 @@ namespace Microsoft.Iris.Render.OpenGL
public sealed class GLRenderWindow : IRenderWindow
{
private readonly SilkWindow m_window;
private readonly GLRenderSession m_session;
private readonly GLVisualContainer m_root;
private Size m_initialClientSize = new Size(1024, 768);
public GLRenderWindow(SilkWindow window, GLRenderSession session)
{
m_window = window;
m_session = session;
m_root = new GLVisualContainer(session, null!, isRoot: true);
m_root.RegisterUsage(this);
}
internal GLVisualContainer Root => m_root;
/// <summary>Frontmost hittable visual under a client-space point, or null.</summary>
internal GLVisual? HitTest(Vector2D<float> clientPoint)
=> m_root.HitTest(new Vector2(clientPoint.X, clientPoint.Y), Matrix4X4<float>.Identity);
// ---- Geometry ------------------------------------------------------------
public int Left => m_window.Position.X;
public int Top => m_window.Position.Y;
@@ -125,7 +131,8 @@ namespace Microsoft.Iris.Render.OpenGL
public void SetEdgeImages(bool fActiveEdges, ShadowEdgePart[] edges) { /* TODO(stage 3): window shadow edges */ }
public void SetWindowOptions(WindowOptions options, bool enable) { /* TODO(stage 3): map to Silk window flags */ }
public void SetMouseIdleOptions(Size sizeMouseIdleTolerance, uint nMouseIdleDelay) { }
public void SetCapture(IRawInputSite captureSite, bool state) { }
public void SetCapture(IRawInputSite captureSite, bool state)
=> ((GLInputSystem)m_session.InputSystem).CaptureSite = state ? captureSite : null;
public void SetDragDropResult(uint nDragOverResult, uint nDragDropResult) { }
public void ClientToScreen(ref Point point)
+8
View File
@@ -57,5 +57,13 @@ namespace Microsoft.Iris.Render.OpenGL
renderer.DrawColoredQuad(matrix, Size.X, Size.Y, c, alpha);
}
}
internal override GLVisual? HitTest(Vector2 screenPoint, Matrix4X4<float> parentMatrix)
{
if (!Visible || (MouseOptions & MouseOptions.Hittable) == 0)
return null;
Matrix4X4<float> world = LocalMatrix * parentMatrix;
return ContainsPoint(screenPoint, world) ? this : null;
}
}
}
+20
View File
@@ -116,5 +116,25 @@ namespace Microsoft.Iris.Render.OpenGL
/// <summary>Draw this visual (and its subtree) with the accumulated parent transform.</summary>
internal abstract void Render(SceneRenderer renderer, Matrix4X4<float> parentMatrix, float inheritedAlpha);
/// <summary>
/// Return the frontmost hittable visual under <paramref name="screenPoint"/>
/// (client pixel space), or null. <paramref name="parentMatrix"/> is the
/// accumulated transform of this visual's parent.
/// </summary>
internal abstract GLVisual? HitTest(Vector2 screenPoint, Matrix4X4<float> parentMatrix);
/// <summary>Is the client-space point inside this visual's local quad?</summary>
private protected bool ContainsPoint(Vector2 screenPoint, Matrix4X4<float> worldMatrix)
{
if (Size.X <= 0f || Size.Y <= 0f)
return false;
if (!Matrix4X4.Invert(worldMatrix, out Matrix4X4<float> inverse))
return false;
// Our world matrix maps local -> screen as (local * world) under the
// renderer's convention, so the inverse maps screen -> local the same way.
Vector3D<float> local = Vector3D.Transform(new Vector3D<float>(screenPoint.X, screenPoint.Y, 0f), inverse);
return local.X >= 0f && local.X <= Size.X && local.Y >= 0f && local.Y <= Size.Y;
}
}
}
@@ -81,6 +81,30 @@ namespace Microsoft.Iris.Render.OpenGL
child.Render(renderer, matrix, alpha);
}
internal override GLVisual? HitTest(Vector2 screenPoint, Matrix4X4<float> parentMatrix)
{
if (!Visible)
return null;
Matrix4X4<float> world = LocalMatrix * parentMatrix;
// Children draw ascending by layer (back-to-front), so the frontmost hit is
// found by testing in reverse order.
m_children.Sort((a, b) => a.Layer.CompareTo(b.Layer));
for (int i = m_children.Count - 1; i >= 0; i--)
{
GLVisual? hit = m_children[i].HitTest(screenPoint, world);
if (hit != null)
return hit;
}
// Otherwise the container itself, if it is hittable and has extent.
if ((MouseOptions & MouseOptions.Hittable) != 0 && ContainsPoint(screenPoint, world))
return this;
return null;
}
protected override void DisposeCore() => RemoveAllChildren();
}
}
@@ -8,35 +8,20 @@
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Nullable>enable</Nullable>
<ImplicitUsings>disable</ImplicitUsings>
<!-- The task requires targeting both modern .NET and .NET Framework 4.8.
net48 only builds on Windows in this repo (the projects we reference,
e.g. UIX.RenderApi, only produce their netfx output there — see the
MicrosoftIris Directory.Build.props and the note in ZuneDBApi/CLAUDE.md
that "on Linux only net8.0 TFMs build"). We mirror that so the project
stays buildable on both hosts. -->
<TargetFrameworks>net8.0</TargetFrameworks>
<TargetFrameworks Condition=" $([MSBuild]::IsOsPlatform('Windows')) ">net8.0;net48</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<!-- We only implement the interfaces defined in UIX.RenderApi; per the task we
must not modify that project, so this is a plain reference. -->
<ProjectReference Include="..\UIX.RenderApi\UIX.RenderApi.csproj" />
</ItemGroup>
<ItemGroup>
<!-- Silk.NET provides the windowing/GL/input abstractions and OpenGL bindings.
The meta-packages register the GLFW/SDL backends automatically. -->
<PackageReference Include="Silk.NET.Windowing" Version="2.21.0" />
<PackageReference Include="Silk.NET.Input" Version="2.21.0" />
<PackageReference Include="Silk.NET.OpenGL" Version="2.21.0" />
<PackageReference Include="Silk.NET.Maths" Version="2.21.0" />
<PackageReference Include="Silk.NET.Windowing" Version="2.23.0" />
<PackageReference Include="Silk.NET.Input" Version="2.23.0" />
<PackageReference Include="Silk.NET.OpenGL" Version="2.23.0" />
<PackageReference Include="Silk.NET.Maths" Version="2.23.0" />
</ItemGroup>
<ItemGroup Condition=" $(TargetFramework.StartsWith('net4')) ">
<!-- PolySharp backfills modern C# language features (init accessors,
nullable attributes, records, etc.) for the netfx target. -->
<PackageReference Include="PolySharp" Version="1.16.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>