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

orion-ecs / keen-eye / 29923911837

22 Jul 2026 01:26PM UTC coverage: 62.597% (-2.6%) from 65.151%
29923911837

push

github

tyevco
test(mcp): Update TestBridge mocks to current interface surface

MockTestBridge and MockCaptureController never implemented the members added
during the MCP Tools Expansion, so tests/KeenEyes.Mcp.TestBridge.Tests did not
compile on main. MockStateController had also drifted (GetComponentAsync and
EntitySnapshot.Components now use JsonElement).

- MockTestBridge: add Window, Time, Systems, Mutation, Profile, Snapshot, AI,
  Replay and InputContext, backed by new hand-written mock controllers plus
  KeenEyes.Testing's MockInputContext, mirroring the existing recording /
  canned-result mock style.
- MockCaptureController: add CaptureRegionAsync, GetRegionScreenshotBytesAsync
  and SaveRegionScreenshotAsync.
- MockStateController: return JsonElement from GetComponentAsync and store
  component data as JsonElement to match the current IStateController.
- Rename pre-existing PascalCase private fields in InputParsingTests to
  camelCase so the project passes dotnet format once it is in CI.

The project was MISSING from KeenEyes.slnx, which is why the compile break went
unnoticed (CI builds the solution). Adding it to the solution is itself the
cheap drift guard requested in the issue: CI now compiles the mocks against the
interfaces, so any future interface addition breaks the build immediately. All
80 tests pass under --max-parallel-test-modules 1.

Fixes #1025

8144 of 12399 branches covered (65.68%)

Branch coverage included in aggregate %.

48975 of 78850 relevant lines covered (62.11%)

0.95 hits per line

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

94.12
/src/KeenEyes.UI/Systems/UIDataGridSystem.cs
1
using KeenEyes.UI.Abstractions;
2

3
namespace KeenEyes.UI;
4

5
/// <summary>
6
/// System that handles data grid interaction including sorting, selection, and column resizing.
7
/// </summary>
8
/// <remarks>
9
/// <para>
10
/// This system manages:
11
/// <list type="bullet">
12
/// <item>Column header click for sorting</item>
13
/// <item>Row click for selection</item>
14
/// <item>Column resize handle dragging</item>
15
/// <item>Row hover states</item>
16
/// </list>
17
/// </para>
18
/// </remarks>
19
public sealed class UIDataGridSystem : SystemBase
20
{
21
    private EventSubscription? clickSubscription;
22
    private EventSubscription? dragStartSubscription;
23
    private EventSubscription? dragSubscription;
24
    private EventSubscription? dragEndSubscription;
25
    private EventSubscription? pointerEnterSubscription;
26
    private EventSubscription? pointerExitSubscription;
27

28
    /// <inheritdoc />
29
    protected override void OnInitialize()
30
    {
31
        // Subscribe to click events for header and row clicks
32
        clickSubscription = World.Subscribe<UIClickEvent>(OnClick);
1✔
33

34
        // Subscribe to drag events for column resizing
35
        dragStartSubscription = World.Subscribe<UIDragStartEvent>(OnDragStart);
1✔
36
        dragSubscription = World.Subscribe<UIDragEvent>(OnDrag);
1✔
37
        dragEndSubscription = World.Subscribe<UIDragEndEvent>(OnDragEnd);
1✔
38

39
        // Subscribe to hover events for row highlighting
40
        pointerEnterSubscription = World.Subscribe<UIPointerEnterEvent>(OnPointerEnter);
1✔
41
        pointerExitSubscription = World.Subscribe<UIPointerExitEvent>(OnPointerExit);
1✔
42
    }
1✔
43

44
    /// <inheritdoc />
45
    protected override void Dispose(bool disposing)
46
    {
47
        if (disposing)
1✔
48
        {
49
            clickSubscription?.Dispose();
1✔
50
            dragStartSubscription?.Dispose();
1✔
51
            dragSubscription?.Dispose();
1✔
52
            dragEndSubscription?.Dispose();
1✔
53
            pointerEnterSubscription?.Dispose();
1✔
54
            pointerExitSubscription?.Dispose();
1✔
55
            clickSubscription = null;
1✔
56
            dragStartSubscription = null;
1✔
57
            dragSubscription = null;
1✔
58
            dragEndSubscription = null;
1✔
59
            pointerEnterSubscription = null;
1✔
60
            pointerExitSubscription = null;
1✔
61
        }
62
        base.Dispose(disposing);
1✔
63
    }
1✔
64

65
    /// <inheritdoc />
66
    public override void Update(float deltaTime)
67
    {
68
        // Most work is event-driven
69
    }
1✔
70

71
    private void OnClick(UIClickEvent evt)
72
    {
73
        // Handle column header click for sorting
74
        if (World.Has<UIDataGridColumn>(evt.Element))
1✔
75
        {
76
            HandleColumnClick(evt.Element);
1✔
77
            return;
1✔
78
        }
79

80
        // Handle row click for selection
81
        if (World.Has<UIDataGridRow>(evt.Element))
1✔
82
        {
83
            HandleRowClick(evt.Element, false, false);
1✔
84
            return;
1✔
85
        }
86

87
        // Handle cell click (select row)
88
        if (World.Has<UIDataGridCell>(evt.Element))
1✔
89
        {
90
            ref readonly var cell = ref World.Get<UIDataGridCell>(evt.Element);
1✔
91
            if (World.IsAlive(cell.Row))
1✔
92
            {
93
                HandleRowClick(cell.Row, false, false);
1✔
94
            }
95
        }
96
    }
1✔
97

98
    private void HandleColumnClick(Entity columnEntity)
99
    {
100
        ref readonly var column = ref World.Get<UIDataGridColumn>(columnEntity);
1✔
101
        var gridEntity = column.DataGrid;
1✔
102

103
        if (!World.IsAlive(gridEntity) || !World.Has<UIDataGrid>(gridEntity))
1✔
104
        {
105
            return;
1✔
106
        }
107

108
        if (!column.IsSortable)
1✔
109
        {
110
            return;
1✔
111
        }
112

113
        ref var grid = ref World.Get<UIDataGrid>(gridEntity);
1✔
114

115
        if (!grid.AllowSorting)
1✔
116
        {
117
            return;
1✔
118
        }
119

120
        // Determine new sort direction
121
        SortDirection newDirection;
122
        if (grid.SortedColumn == columnEntity)
1✔
123
        {
124
            // Toggle between ascending and descending
125
            newDirection = grid.SortDirection == SortDirection.Ascending
1✔
126
                ? SortDirection.Descending
1✔
127
                : SortDirection.Ascending;
1✔
128
        }
129
        else
130
        {
131
            // New column, start with ascending
132
            newDirection = SortDirection.Ascending;
1✔
133
        }
134

135
        // Update previous sorted column indicator
136
        if (World.IsAlive(grid.SortedColumn) && grid.SortedColumn != columnEntity)
1✔
137
        {
138
            ClearSortIndicator(grid.SortedColumn);
1✔
139
        }
140

141
        // Update grid state
142
        grid.SortedColumn = columnEntity;
1✔
143
        grid.SortDirection = newDirection;
1✔
144

145
        // Update sort indicator visual
146
        UpdateSortIndicator(columnEntity, newDirection);
1✔
147

148
        // Fire sort event
149
        World.Send(new UIGridSortEvent(gridEntity, columnEntity, column.ColumnIndex, newDirection));
1✔
150
    }
1✔
151

152
    private void ClearSortIndicator(Entity columnEntity)
153
    {
154
        if (!World.Has<UIDataGridColumn>(columnEntity))
1✔
155
        {
156
            return;
×
157
        }
158

159
        ref readonly var column = ref World.Get<UIDataGridColumn>(columnEntity);
1✔
160
        if (World.IsAlive(column.SortIndicator) && World.Has<UIText>(column.SortIndicator))
1✔
161
        {
162
            ref var text = ref World.Get<UIText>(column.SortIndicator);
1✔
163
            text.Content = string.Empty;
1✔
164
        }
165
    }
1✔
166

167
    private void UpdateSortIndicator(Entity columnEntity, SortDirection direction)
168
    {
169
        if (!World.Has<UIDataGridColumn>(columnEntity))
1✔
170
        {
171
            return;
×
172
        }
173

174
        ref readonly var column = ref World.Get<UIDataGridColumn>(columnEntity);
1✔
175
        if (World.IsAlive(column.SortIndicator) && World.Has<UIText>(column.SortIndicator))
1✔
176
        {
177
            ref var text = ref World.Get<UIText>(column.SortIndicator);
1✔
178
            text.Content = direction switch
1✔
179
            {
1✔
180
                SortDirection.Ascending => "\u25B2", // ▲
1✔
181
                SortDirection.Descending => "\u25BC", // ▼
1✔
182
                _ => string.Empty
1✔
183
            };
1✔
184
        }
185
    }
1✔
186

187
    private void HandleRowClick(Entity rowEntity, bool isShiftHeld, bool isCtrlHeld)
188
    {
189
        ref var row = ref World.Get<UIDataGridRow>(rowEntity);
1✔
190
        var gridEntity = row.DataGrid;
1✔
191

192
        if (!World.IsAlive(gridEntity) || !World.Has<UIDataGrid>(gridEntity))
1✔
193
        {
194
            return;
1✔
195
        }
196

197
        ref var grid = ref World.Get<UIDataGrid>(gridEntity);
1✔
198

199
        if (grid.SelectionMode == GridSelectionMode.None)
1✔
200
        {
201
            return;
1✔
202
        }
203

204
        if (grid.SelectionMode == GridSelectionMode.Single)
1✔
205
        {
206
            // Deselect previous row
207
            if (World.IsAlive(grid.SelectedRow) && grid.SelectedRow != rowEntity)
1✔
208
            {
209
                DeselectRow(grid.SelectedRow, gridEntity);
1✔
210
            }
211

212
            // Select new row
213
            SelectRow(rowEntity, gridEntity);
1✔
214
            grid.SelectedRow = rowEntity;
1✔
215
        }
216
        else if (grid.SelectionMode == GridSelectionMode.Multiple)
1✔
217
        {
218
            if (isCtrlHeld)
1✔
219
            {
220
                // Toggle selection
221
                if (row.IsSelected)
×
222
                {
223
                    DeselectRow(rowEntity, gridEntity);
×
224
                }
225
                else
226
                {
227
                    SelectRow(rowEntity, gridEntity);
×
228
                }
229
            }
230
            else
231
            {
232
                // Clear all and select this row
233
                ClearAllRowSelections(gridEntity);
1✔
234
                SelectRow(rowEntity, gridEntity);
1✔
235
                grid.SelectedRow = rowEntity;
1✔
236
            }
237
        }
238
    }
1✔
239

240
    private void SelectRow(Entity rowEntity, Entity gridEntity)
241
    {
242
        if (!World.Has<UIDataGridRow>(rowEntity))
1✔
243
        {
244
            return;
×
245
        }
246

247
        ref var row = ref World.Get<UIDataGridRow>(rowEntity);
1✔
248

249
        if (row.IsSelected)
1✔
250
        {
251
            return;
1✔
252
        }
253

254
        row.IsSelected = true;
1✔
255

256
        // Add tag
257
        if (!World.Has<UIDataGridRowSelectedTag>(rowEntity))
1✔
258
        {
259
            World.Add(rowEntity, new UIDataGridRowSelectedTag());
1✔
260
        }
261

262
        // Update visual
263
        UpdateRowVisual(rowEntity, ref row);
1✔
264

265
        // Fire event
266
        World.Send(new UIGridRowSelectedEvent(gridEntity, rowEntity, row.RowIndex, true));
1✔
267
    }
1✔
268

269
    private void DeselectRow(Entity rowEntity, Entity gridEntity)
270
    {
271
        if (!World.Has<UIDataGridRow>(rowEntity))
1✔
272
        {
273
            return;
×
274
        }
275

276
        ref var row = ref World.Get<UIDataGridRow>(rowEntity);
1✔
277

278
        if (!row.IsSelected)
1✔
279
        {
280
            return;
×
281
        }
282

283
        row.IsSelected = false;
1✔
284

285
        // Remove tag
286
        if (World.Has<UIDataGridRowSelectedTag>(rowEntity))
1✔
287
        {
288
            World.Remove<UIDataGridRowSelectedTag>(rowEntity);
1✔
289
        }
290

291
        // Update visual
292
        UpdateRowVisual(rowEntity, ref row);
1✔
293

294
        // Fire event
295
        World.Send(new UIGridRowSelectedEvent(gridEntity, rowEntity, row.RowIndex, false));
1✔
296
    }
1✔
297

298
    private void ClearAllRowSelections(Entity gridEntity)
299
    {
300
        foreach (var rowEntity in World.Query<UIDataGridRow>())
1✔
301
        {
302
            ref readonly var row = ref World.Get<UIDataGridRow>(rowEntity);
1✔
303
            if (row.DataGrid == gridEntity && row.IsSelected)
1✔
304
            {
305
                DeselectRow(rowEntity, gridEntity);
1✔
306
            }
307
        }
308
    }
1✔
309

310
    private void UpdateRowVisual(Entity rowEntity, ref UIDataGridRow row)
311
    {
312
        if (!World.Has<UIStyle>(rowEntity))
1✔
313
        {
314
            return;
1✔
315
        }
316

317
        ref var style = ref World.Get<UIStyle>(rowEntity);
1✔
318

319
        if (row.IsSelected)
1✔
320
        {
321
            style.BackgroundColor = new System.Numerics.Vector4(0.3f, 0.5f, 0.9f, 1f);
×
322
        }
323
        else if (row.IsHovered)
1✔
324
        {
325
            style.BackgroundColor = new System.Numerics.Vector4(0.35f, 0.35f, 0.35f, 1f);
1✔
326
        }
327
        else
328
        {
329
            // Alternating row colors
330
            if (World.Has<UIDataGrid>(row.DataGrid))
1✔
331
            {
332
                ref readonly var grid = ref World.Get<UIDataGrid>(row.DataGrid);
1✔
333
                if (grid.AlternatingRowColors && row.RowIndex % 2 == 1)
1✔
334
                {
335
                    style.BackgroundColor = new System.Numerics.Vector4(0.22f, 0.22f, 0.22f, 1f);
1✔
336
                }
337
                else
338
                {
339
                    style.BackgroundColor = new System.Numerics.Vector4(0.25f, 0.25f, 0.25f, 1f);
1✔
340
                }
341
            }
342
        }
343
    }
1✔
344

345
    private void OnPointerEnter(UIPointerEnterEvent evt)
346
    {
347
        // Handle row hover enter
348
        if (World.Has<UIDataGridRow>(evt.Element))
1✔
349
        {
350
            ref var row = ref World.Get<UIDataGridRow>(evt.Element);
1✔
351
            row.IsHovered = true;
1✔
352
            UpdateRowVisual(evt.Element, ref row);
1✔
353
        }
354
    }
1✔
355

356
    private void OnPointerExit(UIPointerExitEvent evt)
357
    {
358
        // Handle row hover exit
359
        if (World.Has<UIDataGridRow>(evt.Element))
1✔
360
        {
361
            ref var row = ref World.Get<UIDataGridRow>(evt.Element);
1✔
362
            row.IsHovered = false;
1✔
363
            UpdateRowVisual(evt.Element, ref row);
1✔
364
        }
365
    }
1✔
366

367
    private void OnDragStart(UIDragStartEvent evt)
368
    {
369
        // Handle column resize handle drag start
370
        if (World.Has<UIDataGridResizeHandle>(evt.Element))
1✔
371
        {
372
            ref var handle = ref World.Get<UIDataGridResizeHandle>(evt.Element);
1✔
373
            handle.IsDragging = true;
1✔
374
            handle.DragStartX = evt.StartPosition.X;
1✔
375

376
            if (World.Has<UIDataGridColumn>(handle.Column))
1✔
377
            {
378
                ref readonly var column = ref World.Get<UIDataGridColumn>(handle.Column);
1✔
379
                handle.StartWidth = column.Width;
1✔
380
            }
381
        }
382
    }
1✔
383

384
    private void OnDrag(UIDragEvent evt)
385
    {
386
        // Handle column resize handle drag
387
        if (World.Has<UIDataGridResizeHandle>(evt.Element))
1✔
388
        {
389
            ref var handle = ref World.Get<UIDataGridResizeHandle>(evt.Element);
1✔
390

391
            if (!handle.IsDragging)
1✔
392
            {
393
                return;
1✔
394
            }
395

396
            if (!World.Has<UIDataGridColumn>(handle.Column))
1✔
397
            {
398
                return;
1✔
399
            }
400

401
            ref var column = ref World.Get<UIDataGridColumn>(handle.Column);
1✔
402

403
            if (!column.IsResizable)
1✔
404
            {
405
                return;
×
406
            }
407

408
            float deltaX = evt.Position.X - handle.DragStartX;
1✔
409
            float newWidth = handle.StartWidth + deltaX;
1✔
410

411
            // Clamp to minimum width
412
            if (newWidth < column.MinWidth)
1✔
413
            {
414
                newWidth = column.MinWidth;
1✔
415
            }
416

417
            column.Width = newWidth;
1✔
418

419
            // Update column visual width
420
            if (World.Has<UIRect>(handle.Column))
1✔
421
            {
422
                ref var rect = ref World.Get<UIRect>(handle.Column);
1✔
423
                rect.Size = new System.Numerics.Vector2(newWidth, rect.Size.Y);
1✔
424
            }
425
        }
426
    }
1✔
427

428
    private void OnDragEnd(UIDragEndEvent evt)
429
    {
430
        // Handle column resize handle drag end
431
        if (World.Has<UIDataGridResizeHandle>(evt.Element))
1✔
432
        {
433
            ref var handle = ref World.Get<UIDataGridResizeHandle>(evt.Element);
1✔
434

435
            if (!handle.IsDragging)
1✔
436
            {
437
                return;
1✔
438
            }
439

440
            handle.IsDragging = false;
1✔
441

442
            if (World.Has<UIDataGridColumn>(handle.Column))
1✔
443
            {
444
                ref readonly var column = ref World.Get<UIDataGridColumn>(handle.Column);
1✔
445
                float oldWidth = handle.StartWidth;
1✔
446
                float newWidth = column.Width;
1✔
447

448
                if (System.Math.Abs(oldWidth - newWidth) > 0.1f)
1✔
449
                {
450
                    World.Send(new UIGridColumnResizedEvent(
×
451
                        column.DataGrid,
×
452
                        handle.Column,
×
453
                        column.ColumnIndex,
×
454
                        oldWidth,
×
455
                        newWidth));
×
456
                }
457
            }
458
        }
459
    }
1✔
460

461
    /// <summary>
462
    /// Selects a row by index.
463
    /// </summary>
464
    /// <param name="gridEntity">The data grid entity.</param>
465
    /// <param name="rowIndex">The row index to select.</param>
466
    public void SelectRowByIndex(Entity gridEntity, int rowIndex)
467
    {
468
        foreach (var rowEntity in World.Query<UIDataGridRow>())
1✔
469
        {
470
            ref readonly var row = ref World.Get<UIDataGridRow>(rowEntity);
1✔
471
            if (row.DataGrid == gridEntity && row.RowIndex == rowIndex)
1✔
472
            {
473
                HandleRowClick(rowEntity, false, false);
1✔
474
                return;
1✔
475
            }
476
        }
477
    }
1✔
478

479
    /// <summary>
480
    /// Clears all row selections in a data grid.
481
    /// </summary>
482
    /// <param name="gridEntity">The data grid entity.</param>
483
    public void ClearSelection(Entity gridEntity)
484
    {
485
        if (!World.IsAlive(gridEntity) || !World.Has<UIDataGrid>(gridEntity))
1✔
486
        {
487
            return;
1✔
488
        }
489

490
        ClearAllRowSelections(gridEntity);
1✔
491

492
        ref var grid = ref World.Get<UIDataGrid>(gridEntity);
1✔
493
        grid.SelectedRow = Entity.Null;
1✔
494
    }
1✔
495

496
    /// <summary>
497
    /// Sorts a data grid by column index.
498
    /// </summary>
499
    /// <param name="gridEntity">The data grid entity.</param>
500
    /// <param name="columnIndex">The column index to sort by.</param>
501
    /// <param name="direction">The sort direction.</param>
502
    public void SortByColumn(Entity gridEntity, int columnIndex, SortDirection direction)
503
    {
504
        foreach (var columnEntity in World.Query<UIDataGridColumn>())
1✔
505
        {
506
            ref readonly var column = ref World.Get<UIDataGridColumn>(columnEntity);
1✔
507
            if (column.DataGrid == gridEntity && column.ColumnIndex == columnIndex)
1✔
508
            {
509
                if (!World.Has<UIDataGrid>(gridEntity))
1✔
510
                {
511
                    return;
×
512
                }
513

514
                ref var grid = ref World.Get<UIDataGrid>(gridEntity);
1✔
515

516
                // Clear previous sort indicator
517
                if (World.IsAlive(grid.SortedColumn) && grid.SortedColumn != columnEntity)
1✔
518
                {
519
                    ClearSortIndicator(grid.SortedColumn);
1✔
520
                }
521

522
                grid.SortedColumn = columnEntity;
1✔
523
                grid.SortDirection = direction;
1✔
524

525
                UpdateSortIndicator(columnEntity, direction);
1✔
526
                World.Send(new UIGridSortEvent(gridEntity, columnEntity, columnIndex, direction));
1✔
527
                return;
1✔
528
            }
529
        }
530
    }
1✔
531

532
    /// <summary>
533
    /// Gets the currently selected row indices.
534
    /// </summary>
535
    /// <param name="gridEntity">The data grid entity.</param>
536
    /// <returns>Array of selected row indices.</returns>
537
    public int[] GetSelectedRowIndices(Entity gridEntity)
538
    {
539
        var indices = new List<int>();
1✔
540

541
        foreach (var rowEntity in World.Query<UIDataGridRow>())
1✔
542
        {
543
            ref readonly var row = ref World.Get<UIDataGridRow>(rowEntity);
1✔
544
            if (row.DataGrid == gridEntity && row.IsSelected)
1✔
545
            {
546
                indices.Add(row.RowIndex);
1✔
547
            }
548
        }
549

550
        return [.. indices];
1✔
551
    }
552

553
    /// <summary>
554
    /// Sets the column width.
555
    /// </summary>
556
    /// <param name="gridEntity">The data grid entity.</param>
557
    /// <param name="columnIndex">The column index.</param>
558
    /// <param name="width">The new width.</param>
559
    public void SetColumnWidth(Entity gridEntity, int columnIndex, float width)
560
    {
561
        foreach (var columnEntity in World.Query<UIDataGridColumn>())
1✔
562
        {
563
            ref var column = ref World.Get<UIDataGridColumn>(columnEntity);
1✔
564
            if (column.DataGrid == gridEntity && column.ColumnIndex == columnIndex)
1✔
565
            {
566
                float oldWidth = column.Width;
1✔
567
                column.Width = Math.Max(width, column.MinWidth);
1✔
568

569
                if (World.Has<UIRect>(columnEntity))
1✔
570
                {
571
                    ref var rect = ref World.Get<UIRect>(columnEntity);
1✔
572
                    rect.Size = new System.Numerics.Vector2(column.Width, rect.Size.Y);
1✔
573
                }
574

575
                if (Math.Abs(oldWidth - column.Width) > 0.1f)
1✔
576
                {
577
                    World.Send(new UIGridColumnResizedEvent(
1✔
578
                        gridEntity,
1✔
579
                        columnEntity,
1✔
580
                        columnIndex,
1✔
581
                        oldWidth,
1✔
582
                        column.Width));
1✔
583
                }
584
                return;
1✔
585
            }
586
        }
587
    }
1✔
588
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc