• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

orion-ecs / keen-eye / 20871273650

10 Jan 2026 02:23AM UTC coverage: 86.895% (-0.07%) from 86.962%
20871273650

push

github

tyevco
fix(ui): Fix UI widget tests for arrows, TabView visibility, and TextInput

- Use Unicode arrows (▼/▶) instead of ASCII (v/>) for accordion and tree view expand/collapse indicators
- Set visibility, UITabPanel, and UIHiddenTag on both scrollView and scrollContent in CreateTabView
- Fix UITextInput test assertions to expect ShowingPlaceholder=false when no placeholder text is provided

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

9264 of 12568 branches covered (73.71%)

Branch coverage included in aggregate %.

15 of 15 new or added lines in 6 files covered. (100.0%)

321 existing lines in 16 files now uncovered.

159741 of 181925 relevant lines covered (87.81%)

1.0 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

68.46
/src/KeenEyes.UI/Systems/UIInputSystem.cs
1
using System.Numerics;
2
using KeenEyes.Common;
3
using KeenEyes.Input.Abstractions;
4
using KeenEyes.UI.Abstractions;
5

6
namespace KeenEyes.UI;
7

8
/// <summary>
9
/// System that processes input events and updates UI interaction states.
10
/// </summary>
11
/// <remarks>
12
/// <para>
13
/// The input system performs hit testing to determine which UI element is under the cursor,
14
/// updates hover/pressed states on <see cref="UIInteractable"/> components, and fires
15
/// appropriate events.
16
/// </para>
17
/// <para>
18
/// This system should run in <see cref="SystemPhase.EarlyUpdate"/> phase to process
19
/// input before other systems.
20
/// </para>
21
/// </remarks>
22
public sealed class UIInputSystem : SystemBase
23
{
24
    private UIHitTester? hitTester;
25
    private UIContext? uiContext;
26
    private IInputContext? inputContext;
27
    private Entity hoveredEntity = Entity.Null;
1✔
28
    private Entity pressedEntity = Entity.Null;
1✔
29
    private Vector2 dragStartPosition;
30
    private Vector2 lastDragPosition;
31
    private bool isDragging;
32
    private double lastClickTime;
33
    private const double DoubleClickTime = 0.3; // seconds
34
    private bool scrollSubscribed;
35

36
    /// <inheritdoc />
37
    protected override void OnInitialize()
38
    {
39
        hitTester = new UIHitTester(World);
1✔
40
    }
1✔
41

42
    /// <inheritdoc />
43
    protected override void OnBeforeUpdate(float deltaTime)
44
    {
45
        // Clear pending events from previous frame
46
        foreach (var entity in World.Query<UIInteractable>())
×
47
        {
UNCOV
48
            ref var interactable = ref World.Get<UIInteractable>(entity);
×
49
            interactable.PendingEvents = UIEventType.None;
×
50
        }
UNCOV
51
    }
×
52

53
    /// <inheritdoc />
54
    public override void Update(float deltaTime)
55
    {
56
        // Lazy initialization
57
        if (inputContext is null && !World.TryGetExtension(out inputContext))
1✔
58
        {
59
            return;
1✔
60
        }
61

62
        if (uiContext is null && !World.TryGetExtension(out uiContext))
1✔
63
        {
64
            return;
1✔
65
        }
66

67
        if (hitTester is null)
1✔
68
        {
UNCOV
69
            return;
×
70
        }
71

72
        // Local copies for null safety - fields are verified non-null above
73
        var input = inputContext!;
1✔
74
        var mouse = input.Mouse;
1✔
75
        var mousePos = mouse.Position;
1✔
76

77
        // Subscribe to scroll events once we have input context
78
        if (!scrollSubscribed)
1✔
79
        {
80
            mouse.OnScroll += OnMouseScroll;
1✔
81
            scrollSubscribed = true;
1✔
82
        }
83

84
        // Hit test
85
        var hitEntity = hitTester.HitTest(mousePos);
1✔
86

87
        // Handle hover changes
88
        ProcessHover(hitEntity, mousePos);
1✔
89

90
        // Handle mouse button input
91
        ProcessMouseInput(mouse, mousePos, hitEntity);
1✔
92
    }
1✔
93

94
    private void ProcessHover(Entity hitEntity, Vector2 mousePos)
95
    {
96
        // Hover exit
97
        if (hoveredEntity.IsValid &&
1✔
98
            hoveredEntity != hitEntity &&
1✔
99
            World.IsAlive(hoveredEntity) &&
1✔
100
            World.Has<UIInteractable>(hoveredEntity))
1✔
101
        {
102
            ref var interactable = ref World.Get<UIInteractable>(hoveredEntity);
1✔
103
            interactable.State &= ~UIInteractionState.Hovered;
1✔
104
            interactable.PendingEvents |= UIEventType.PointerExit;
1✔
105

106
            World.Send(new UIPointerExitEvent(hoveredEntity));
1✔
107
        }
108

109
        // Hover enter
110
        if (hitEntity.IsValid &&
1✔
111
            hitEntity != hoveredEntity &&
1✔
112
            World.Has<UIInteractable>(hitEntity))
1✔
113
        {
114
            ref var interactable = ref World.Get<UIInteractable>(hitEntity);
1✔
115
            interactable.State |= UIInteractionState.Hovered;
1✔
116
            interactable.PendingEvents |= UIEventType.PointerEnter;
1✔
117

118
            World.Send(new UIPointerEnterEvent(hitEntity, mousePos));
1✔
119
        }
120

121
        hoveredEntity = hitEntity;
1✔
122
    }
1✔
123

124
    private void ProcessMouseInput(IMouse mouse, Vector2 mousePos, Entity hitEntity)
125
    {
126
        // Check for mouse button down
127
        if (mouse.IsButtonDown(MouseButton.Left))
1✔
128
        {
129
            if (!pressedEntity.IsValid && hitEntity.IsValid && World.Has<UIInteractable>(hitEntity))
1✔
130
            {
131
                ref var interactable = ref World.Get<UIInteractable>(hitEntity);
1✔
132

133
                if (interactable.CanClick || interactable.CanDrag)
1✔
134
                {
135
                    pressedEntity = hitEntity;
1✔
136
                    interactable.State |= UIInteractionState.Pressed;
1✔
137
                    interactable.PendingEvents |= UIEventType.PointerDown;
1✔
138
                    dragStartPosition = mousePos;
1✔
139
                    lastDragPosition = mousePos;
1✔
140

141
                    // Request focus if element is focusable
142
                    if (interactable.CanFocus && uiContext is not null)
1✔
143
                    {
144
                        uiContext.RequestFocus(hitEntity);
1✔
145
                    }
146
                }
147
            }
148

149
            // Handle dragging
150
            if (pressedEntity.IsValid && World.Has<UIInteractable>(pressedEntity))
1✔
151
            {
152
                ref var interactable = ref World.Get<UIInteractable>(pressedEntity);
1✔
153

154
                if (interactable.CanDrag && !isDragging)
1✔
155
                {
156
                    // Start drag if moved enough
157
                    var delta = mousePos - dragStartPosition;
1✔
158
                    if (delta.LengthSquared() > 25) // 5 pixel threshold squared
1✔
159
                    {
160
                        isDragging = true;
1✔
161
                        interactable.State |= UIInteractionState.Dragging;
1✔
162
                        interactable.PendingEvents |= UIEventType.DragStart;
1✔
163

164
                        World.Send(new UIDragStartEvent(pressedEntity, dragStartPosition));
1✔
165
                    }
166
                }
167

168
                if (isDragging)
1✔
169
                {
170
                    var delta = mousePos - lastDragPosition;
1✔
171
                    lastDragPosition = mousePos;
1✔
172
                    World.Send(new UIDragEvent(pressedEntity, mousePos, delta));
1✔
173
                }
174
            }
175
        }
176
        else
177
        {
178
            // Mouse button released
179
            if (pressedEntity.IsValid && World.IsAlive(pressedEntity) && World.Has<UIInteractable>(pressedEntity))
1✔
180
            {
181
                ref var interactable = ref World.Get<UIInteractable>(pressedEntity);
1✔
182
                interactable.State &= ~UIInteractionState.Pressed;
1✔
183
                interactable.PendingEvents |= UIEventType.PointerUp;
1✔
184

185
                if (isDragging)
1✔
186
                {
187
                    // End drag
188
                    interactable.State &= ~UIInteractionState.Dragging;
1✔
189
                    interactable.PendingEvents |= UIEventType.DragEnd;
1✔
190
                    isDragging = false;
1✔
191

192
                    World.Send(new UIDragEndEvent(pressedEntity, mousePos));
1✔
193
                }
194
                else if (interactable.CanClick && pressedEntity == hitEntity)
1✔
195
                {
196
                    // Click occurred
197
                    interactable.PendingEvents |= UIEventType.Click;
1✔
198

199
                    // Check for double-click
200
                    var currentTime = Environment.TickCount / 1000.0;
1✔
201
                    if (currentTime - lastClickTime < DoubleClickTime)
1✔
202
                    {
203
                        interactable.PendingEvents |= UIEventType.DoubleClick;
1✔
204
                    }
205
                    lastClickTime = currentTime;
1✔
206

207
                    World.Send(new UIClickEvent(pressedEntity, mousePos, MouseButton.Left));
1✔
208
                }
209
            }
210

211
            pressedEntity = Entity.Null;
1✔
212
        }
213
    }
1✔
214

215
    private void OnMouseScroll(MouseScrollEventArgs args)
216
    {
UNCOV
217
        if (hitTester is null)
×
218
        {
UNCOV
219
            return;
×
220
        }
221

222
        // Hit test at the scroll position
UNCOV
223
        var hitEntity = hitTester.HitTest(args.Position);
×
224

225
        // Walk up hierarchy to find nearest UIScrollable
UNCOV
226
        var scrollableEntity = FindScrollable(hitEntity);
×
UNCOV
227
        if (!scrollableEntity.IsValid)
×
228
        {
UNCOV
229
            return;
×
230
        }
231

UNCOV
232
        ref var scrollable = ref World.Get<UIScrollable>(scrollableEntity);
×
233

234
        // Get viewport size for clamping
UNCOV
235
        Vector2 viewportSize = Vector2.Zero;
×
UNCOV
236
        if (World.Has<UIRect>(scrollableEntity))
×
237
        {
UNCOV
238
            ref readonly var rect = ref World.Get<UIRect>(scrollableEntity);
×
UNCOV
239
            viewportSize = rect.ComputedBounds.Size;
×
240
        }
241

UNCOV
242
        var maxScroll = scrollable.GetMaxScroll(viewportSize);
×
UNCOV
243
        var sensitivity = scrollable.ScrollSensitivity > 0 ? scrollable.ScrollSensitivity : 20f;
×
244

245
        // Apply scroll delta (negative deltaY = scroll down = increase scroll position)
UNCOV
246
        if (scrollable.VerticalScroll && !args.DeltaY.IsApproximatelyZero())
×
247
        {
UNCOV
248
            var newY = scrollable.ScrollPosition.Y - args.DeltaY * sensitivity;
×
UNCOV
249
            scrollable.ScrollPosition = new Vector2(
×
UNCOV
250
                scrollable.ScrollPosition.X,
×
UNCOV
251
                Math.Clamp(newY, 0, maxScroll.Y));
×
252
        }
253

UNCOV
254
        if (scrollable.HorizontalScroll && !args.DeltaX.IsApproximatelyZero())
×
255
        {
UNCOV
256
            var newX = scrollable.ScrollPosition.X - args.DeltaX * sensitivity;
×
UNCOV
257
            scrollable.ScrollPosition = new Vector2(
×
UNCOV
258
                Math.Clamp(newX, 0, maxScroll.X),
×
UNCOV
259
                scrollable.ScrollPosition.Y);
×
260
        }
UNCOV
261
    }
×
262

263
    private Entity FindScrollable(Entity entity)
264
    {
UNCOV
265
        var current = entity;
×
UNCOV
266
        while (current.IsValid && World.IsAlive(current))
×
267
        {
UNCOV
268
            if (World.Has<UIScrollable>(current))
×
269
            {
UNCOV
270
                ref readonly var scrollable = ref World.Get<UIScrollable>(current);
×
UNCOV
271
                if (scrollable.VerticalScroll || scrollable.HorizontalScroll)
×
272
                {
UNCOV
273
                    return current;
×
274
                }
275
            }
276

UNCOV
277
            current = World.GetParent(current);
×
278
        }
279

UNCOV
280
        return Entity.Null;
×
281
    }
282

283
    /// <inheritdoc />
284
    protected override void Dispose(bool disposing)
285
    {
286
        if (disposing && scrollSubscribed && inputContext is not null)
1✔
287
        {
288
            inputContext.Mouse.OnScroll -= OnMouseScroll;
1✔
289
            scrollSubscribed = false;
1✔
290
        }
291

292
        base.Dispose(disposing);
1✔
293
    }
1✔
294
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc