using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Input; namespace Theriapolis.Game.CodexUI.Core; /// /// Per-frame input snapshot used by every CodexUI widget. /// is called once at the top of ; widgets /// read the resulting edge-detected event flags during their own Update pass. /// /// Text input piggybacks on MonoGame's /// event — the screen subscribes during Initialize and pushes characters /// into . Backspace, Enter and arrow keys /// route through instead. /// /// /// Mouse clipping: a parent widget can set before /// updating a region of the tree, which causes /// to report off-screen for any cursor outside the clip rect. Children that /// rely on Bounds.Contains(input.MousePosition) to detect hover/click /// then ignore events that visually land in some other layer (e.g. a /// stepper bar painted over a scrolled card). Without this, both the /// clipped widget and the chrome above it would handle the same click. /// /// public sealed class CodexInput { public Point MousePosition { get; private set; } public Point PreviousMousePosition { get; private set; } public Point MouseDelta { get; private set; } public bool LeftButtonDown { get; private set; } public bool LeftJustPressed { get; private set; } public bool LeftJustReleased { get; private set; } public bool RightJustPressed { get; private set; } public int ScrollDelta { get; private set; } private MouseState _prev; private KeyboardState _prevKb; private KeyboardState _curKb; private Rectangle? _mouseClip; private Point _rawMouse; /// Characters typed this frame (collected from the window's TextInput event). public string TextEnteredThisFrame { get; private set; } = ""; private System.Text.StringBuilder _textBuf = new(); public void OnTextInput(char c) => _textBuf.Append(c); public void Tick() { var m = Mouse.GetState(); var kb = Keyboard.GetState(); PreviousMousePosition = _rawMouse; _rawMouse = new Point(m.X, m.Y); MouseDelta = _rawMouse - PreviousMousePosition; ScrollDelta = m.ScrollWheelValue - _prev.ScrollWheelValue; bool prevLeft = _prev.LeftButton == ButtonState.Pressed; bool curLeft = m.LeftButton == ButtonState.Pressed; LeftButtonDown = curLeft; LeftJustPressed = !prevLeft && curLeft; LeftJustReleased = prevLeft && !curLeft; bool prevRight = _prev.RightButton == ButtonState.Pressed; bool curRight = m.RightButton == ButtonState.Pressed; RightJustPressed = !prevRight && curRight; _prev = m; _prevKb = _curKb; _curKb = kb; TextEnteredThisFrame = _textBuf.ToString(); _textBuf.Clear(); ApplyClip(); } /// /// Restrict reads to . /// Outside the rect the position is reported far off-screen so widgets /// that hit-test by Bounds.Contains stop registering hover/click. /// Pair with after the clipped subtree's /// Update completes. /// public void SetMouseClip(Rectangle clipRect) { _mouseClip = clipRect; ApplyClip(); } public void ClearMouseClip() { _mouseClip = null; ApplyClip(); } /// Read the current clip rectangle, if any. Used by widgets that /// nest a tighter clip and need to restore the outer one after their /// child subtree has finished updating. public Rectangle? GetMouseClip() => _mouseClip; private void ApplyClip() { if (_mouseClip is Rectangle r && !r.Contains(_rawMouse)) MousePosition = new Point(-99999, -99999); else MousePosition = _rawMouse; } public bool KeyDown(Keys k) => _curKb.IsKeyDown(k); public bool KeyJustPressed(Keys k) => _curKb.IsKeyDown(k) && !_prevKb.IsKeyDown(k); public bool KeyJustReleased(Keys k) => !_curKb.IsKeyDown(k) && _prevKb.IsKeyDown(k); }