diff --git a/UIX.RenderApi.OpenGL/Engine/GLInputSystem.cs b/UIX.RenderApi.OpenGL/Engine/GLInputSystem.cs
index 1ac3f41..4f91a58 100644
--- a/UIX.RenderApi.OpenGL/Engine/GLInputSystem.cs
+++ b/UIX.RenderApi.OpenGL/Engine/GLInputSystem.cs
@@ -1,13 +1,22 @@
+using Microsoft.Iris.Input;
+
namespace Microsoft.Iris.Render.OpenGL
{
///
- /// Holds the current raw-input callback sink. The window (see )
- /// translates Silk.NET input events and dispatches them here.
+ /// Holds the current raw-input callback sink and mouse-capture site. The input
+ /// translator (see ) reads
+ /// each event and dispatches translated Silk.NET input to it.
///
public sealed class GLInputSystem : IInputSystem
{
public IRawInputCallbacks? Callbacks { get; private set; }
+ ///
+ /// The visual that has grabbed the mouse via ,
+ /// or null. When set, mouse events are routed to it regardless of hit-testing.
+ ///
+ public IRawInputSite? CaptureSite { get; set; }
+
public void RegisterRawInputCallbacks(IRawInputCallbacks handlers) => Callbacks = handlers;
public void UnregisterRawInputCallbacks() => Callbacks = null;
diff --git a/UIX.RenderApi.OpenGL/Engine/GLInputTranslator.cs b/UIX.RenderApi.OpenGL/Engine/GLInputTranslator.cs
new file mode 100644
index 0000000..7026330
--- /dev/null
+++ b/UIX.RenderApi.OpenGL/Engine/GLInputTranslator.cs
@@ -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
+{
+ ///
+ /// Translates Silk.NET.Input keyboard/mouse events into the render API's raw-input
+ /// protocol (). Message identifiers match what the
+ /// UIX InputManager expects: keyboard uses the
+ /// ordinals, mouse uses Win32 WM_* codes.
+ ///
+ 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 m_repeat = new Dictionary();
+
+ 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(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,
+ };
+ }
+}
diff --git a/UIX.RenderApi.OpenGL/Engine/GLRenderEngine.cs b/UIX.RenderApi.OpenGL/Engine/GLRenderEngine.cs
index e99e76b..2efda96 100644
--- a/UIX.RenderApi.OpenGL/Engine/GLRenderEngine.cs
+++ b/UIX.RenderApi.OpenGL/Engine/GLRenderEngine.cs
@@ -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)
diff --git a/UIX.RenderApi.OpenGL/Engine/GLRenderWindow.cs b/UIX.RenderApi.OpenGL/Engine/GLRenderWindow.cs
index 218c250..0fecfa7 100644
--- a/UIX.RenderApi.OpenGL/Engine/GLRenderWindow.cs
+++ b/UIX.RenderApi.OpenGL/Engine/GLRenderWindow.cs
@@ -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;
+ /// Frontmost hittable visual under a client-space point, or null.
+ internal GLVisual? HitTest(Vector2D clientPoint)
+ => m_root.HitTest(new Vector2(clientPoint.X, clientPoint.Y), Matrix4X4.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)
diff --git a/UIX.RenderApi.OpenGL/Scene/GLSprite.cs b/UIX.RenderApi.OpenGL/Scene/GLSprite.cs
index 8b19501..f7917da 100644
--- a/UIX.RenderApi.OpenGL/Scene/GLSprite.cs
+++ b/UIX.RenderApi.OpenGL/Scene/GLSprite.cs
@@ -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 parentMatrix)
+ {
+ if (!Visible || (MouseOptions & MouseOptions.Hittable) == 0)
+ return null;
+ Matrix4X4 world = LocalMatrix * parentMatrix;
+ return ContainsPoint(screenPoint, world) ? this : null;
+ }
}
}
diff --git a/UIX.RenderApi.OpenGL/Scene/GLVisual.cs b/UIX.RenderApi.OpenGL/Scene/GLVisual.cs
index 9e308d2..0812d5b 100644
--- a/UIX.RenderApi.OpenGL/Scene/GLVisual.cs
+++ b/UIX.RenderApi.OpenGL/Scene/GLVisual.cs
@@ -116,5 +116,25 @@ namespace Microsoft.Iris.Render.OpenGL
/// Draw this visual (and its subtree) with the accumulated parent transform.
internal abstract void Render(SceneRenderer renderer, Matrix4X4 parentMatrix, float inheritedAlpha);
+
+ ///
+ /// Return the frontmost hittable visual under
+ /// (client pixel space), or null. is the
+ /// accumulated transform of this visual's parent.
+ ///
+ internal abstract GLVisual? HitTest(Vector2 screenPoint, Matrix4X4 parentMatrix);
+
+ /// Is the client-space point inside this visual's local quad?
+ private protected bool ContainsPoint(Vector2 screenPoint, Matrix4X4 worldMatrix)
+ {
+ if (Size.X <= 0f || Size.Y <= 0f)
+ return false;
+ if (!Matrix4X4.Invert(worldMatrix, out Matrix4X4 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 local = Vector3D.Transform(new Vector3D(screenPoint.X, screenPoint.Y, 0f), inverse);
+ return local.X >= 0f && local.X <= Size.X && local.Y >= 0f && local.Y <= Size.Y;
+ }
}
}
diff --git a/UIX.RenderApi.OpenGL/Scene/GLVisualContainer.cs b/UIX.RenderApi.OpenGL/Scene/GLVisualContainer.cs
index 33cef07..2151808 100644
--- a/UIX.RenderApi.OpenGL/Scene/GLVisualContainer.cs
+++ b/UIX.RenderApi.OpenGL/Scene/GLVisualContainer.cs
@@ -81,6 +81,30 @@ namespace Microsoft.Iris.Render.OpenGL
child.Render(renderer, matrix, alpha);
}
+ internal override GLVisual? HitTest(Vector2 screenPoint, Matrix4X4 parentMatrix)
+ {
+ if (!Visible)
+ return null;
+
+ Matrix4X4 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();
}
}
diff --git a/UIX.RenderApi.OpenGL/UIX.RenderApi.OpenGL.csproj b/UIX.RenderApi.OpenGL/UIX.RenderApi.OpenGL.csproj
index 63eb933..d32f262 100644
--- a/UIX.RenderApi.OpenGL/UIX.RenderApi.OpenGL.csproj
+++ b/UIX.RenderApi.OpenGL/UIX.RenderApi.OpenGL.csproj
@@ -8,35 +8,20 @@
true
enable
disable
-
-
- net8.0
- net8.0;net48
-
-
-
-
+
-
-
-
-
+
+
+
+
-
all
runtime; build; native; contentfiles; analyzers
diff --git a/logs/UIX.RenderApi.OpenGL/Implementation.md b/logs/UIX.RenderApi.OpenGL/Implementation.md
index 88396b8..5881d46 100644
--- a/logs/UIX.RenderApi.OpenGL/Implementation.md
+++ b/logs/UIX.RenderApi.OpenGL/Implementation.md
@@ -2,6 +2,39 @@
Reverse-chronological log (prepend new entries; never edit older ones).
+## 2026-07-25 — Input-event translation (Silk.NET.Input)
+
+Wired real keyboard/mouse input via `GLInputTranslator`, created by the engine on
+window Load from `IWindow.CreateInput()`. It hooks every keyboard/mouse (and new
+devices via `ConnectionChanged`) and dispatches to the registered
+`IRawInputCallbacks`.
+
+Message-id conventions were confirmed by reading the UIX consumer, not guessed:
+- Keyboard (`KeyboardDevice.OnRawInput`): the `message` is the `KeyboardMessageId`
+ ordinal — 0 Down, 1 Up, 2 Char, 3 SysDown, 4 SysUp, 5 SysChar. We emit Sys*
+ variants while Alt is held. Character events put the char in
+ `RawKeyboardData._virtualKey` (matches `OnRawKeyCharacter`'s `(char)_virtualKey`).
+- Mouse (`MouseDevice.OnRawInput`): the `message` is the Win32 `WM_*` code
+ (0x200 move, 0x201/0x202 L down/up, 0x204/5 R, 0x207/8 M, 0x20A wheel,
+ 0x20B-D X buttons, dblclk variants 0x203/6/9/20D). Wheel delta is `scroll.Y*120`.
+
+`InputModifiers` is computed from live Silk key/button state each event. Silk
+`Key` → Iris `Keys` (Win32 VK) mapping uses contiguous-range arithmetic for
+A–Z / 0–9 / Keypad / F-keys plus an explicit table for the rest; unmapped → None.
+
+Hit-testing: added `GLVisual.HitTest` (frontmost-first, honoring `Visible` and
+`MouseOptions.Hittable`) so `RawMouseData._visNatural` is the visual under the
+cursor. `_visCapture` is the `SetCapture` site when set (now stored on
+`GLInputSystem.CaptureSite`), else the natural target. Screen<->local uses the
+inverse of the visual's world matrix (same row-vector convention as rendering).
+
+TODOs (still stage-3): HID/AppCommand (media/remote) and drag/drop translation are
+not sourced from Silk yet; `_repCount` is a simple per-key press counter; keyboard
+`_flags` is 0.
+
+Validated the same way as before (compile against the prebuilt `UIX.RenderApi.dll`
++ Silk.NET, incl. Silk.NET.Input) — build succeeded.
+
## 2026-07-25 — Initial in-process OpenGL renderer
### Goal