• 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

94.08
/src/KeenEyes.UI/Systems/UIWindowSystem.cs
1
using System.Numerics;
2
using KeenEyes.UI.Abstractions;
3

4
namespace KeenEyes.UI;
5

6
/// <summary>
7
/// System that handles floating window behavior including dragging, closing, and z-order.
8
/// </summary>
9
/// <remarks>
10
/// <para>
11
/// This system listens for:
12
/// <list type="bullet">
13
/// <item>Drag events on title bars (<see cref="UIWindowTitleBar"/>) to move windows</item>
14
/// <item>Click events on close buttons (<see cref="UIWindowCloseButton"/>) to hide windows</item>
15
/// <item>Click events on windows to bring them to front (z-order management)</item>
16
/// <item>Drag events on resize handles (<see cref="UIWindowResizeHandle"/>) to resize windows</item>
17
/// </list>
18
/// </para>
19
/// <para>
20
/// This system should run in <see cref="SystemPhase.EarlyUpdate"/> phase after
21
/// <see cref="UIInputSystem"/> and <see cref="UITabSystem"/>.
22
/// </para>
23
/// </remarks>
24
public sealed class UIWindowSystem : SystemBase
25
{
26
    private EventSubscription? dragSubscription;
27
    private EventSubscription? clickSubscription;
28
    private int nextZOrder = 1;
1✔
29

30
    /// <inheritdoc />
31
    protected override void OnInitialize()
32
    {
33
        // Subscribe to drag events for window movement and resizing
34
        dragSubscription = World.Subscribe<UIDragEvent>(OnDrag);
1✔
35

36
        // Subscribe to click events for close buttons and z-order
37
        clickSubscription = World.Subscribe<UIClickEvent>(OnClick);
1✔
38
    }
1✔
39

40
    /// <inheritdoc />
41
    protected override void Dispose(bool disposing)
42
    {
43
        if (disposing)
1✔
44
        {
45
            dragSubscription?.Dispose();
1✔
46
            clickSubscription?.Dispose();
1✔
47
            dragSubscription = null;
1✔
48
            clickSubscription = null;
1✔
49
        }
50

51
        base.Dispose(disposing);
1✔
52
    }
1✔
53

54
    /// <inheritdoc />
55
    public override void Update(float deltaTime)
56
    {
57
        // Window behavior is event-driven, no per-frame work needed
58
    }
1✔
59

60
    private void OnDrag(UIDragEvent e)
61
    {
62
        // Check if dragging a title bar
63
        if (World.Has<UIWindowTitleBar>(e.Element))
1✔
64
        {
65
            HandleTitleBarDrag(e);
1✔
66
            return;
1✔
67
        }
68

69
        // Check if dragging a resize handle
70
        if (World.Has<UIWindowResizeHandle>(e.Element))
1✔
71
        {
72
            HandleResizeDrag(e);
1✔
73
        }
74
    }
1✔
75

76
    private void HandleTitleBarDrag(UIDragEvent e)
77
    {
78
        ref readonly var titleBar = ref World.Get<UIWindowTitleBar>(e.Element);
1✔
79
        var window = titleBar.Window;
1✔
80

81
        if (!World.IsAlive(window) || !World.Has<UIWindow>(window))
1✔
82
        {
83
            return;
1✔
84
        }
85

86
        ref readonly var windowComponent = ref World.Get<UIWindow>(window);
1✔
87
        if (!windowComponent.CanDrag)
1✔
88
        {
89
            return;
1✔
90
        }
91

92
        // Don't allow dragging when maximized
93
        if (windowComponent.State == WindowState.Maximized)
1✔
94
        {
95
            return;
1✔
96
        }
97

98
        // Update the window position
99
        ref var rect = ref World.Get<UIRect>(window);
1✔
100
        rect.Offset = new UIEdges(
1✔
101
            rect.Offset.Left + e.Delta.X,
1✔
102
            rect.Offset.Top + e.Delta.Y,
1✔
103
            rect.Offset.Right,
1✔
104
            rect.Offset.Bottom
1✔
105
        );
1✔
106

107
        // Mark layout dirty
108
        if (!World.Has<UILayoutDirtyTag>(window))
1✔
109
        {
110
            World.Add(window, new UILayoutDirtyTag());
1✔
111
        }
112
    }
1✔
113

114
    private void HandleResizeDrag(UIDragEvent e)
115
    {
116
        ref readonly var handle = ref World.Get<UIWindowResizeHandle>(e.Element);
1✔
117
        var window = handle.Window;
1✔
118

119
        if (!World.IsAlive(window) || !World.Has<UIWindow>(window))
1✔
120
        {
121
            return;
1✔
122
        }
123

124
        ref readonly var windowComponent = ref World.Get<UIWindow>(window);
1✔
125
        if (!windowComponent.CanResize)
1✔
126
        {
127
            return;
1✔
128
        }
129

130
        // Don't allow resizing when maximized or minimized
131
        if (windowComponent.State != WindowState.Normal)
1✔
132
        {
133
            return;
1✔
134
        }
135

136
        ref var rect = ref World.Get<UIRect>(window);
1✔
137
        var newSize = rect.Size;
1✔
138
        var newOffset = rect.Offset;
1✔
139

140
        // Apply delta based on which edge(s) are being dragged
141
        if ((handle.Edge & ResizeEdge.Right) != 0)
1✔
142
        {
143
            newSize.X += e.Delta.X;
1✔
144
        }
145

146
        if ((handle.Edge & ResizeEdge.Left) != 0)
1✔
147
        {
148
            newSize.X -= e.Delta.X;
1✔
149
            newOffset = new UIEdges(newOffset.Left + e.Delta.X, newOffset.Top, newOffset.Right, newOffset.Bottom);
1✔
150
        }
151

152
        if ((handle.Edge & ResizeEdge.Bottom) != 0)
1✔
153
        {
154
            newSize.Y += e.Delta.Y;
1✔
155
        }
156

157
        if ((handle.Edge & ResizeEdge.Top) != 0)
1✔
158
        {
159
            newSize.Y -= e.Delta.Y;
1✔
160
            newOffset = new UIEdges(newOffset.Left, newOffset.Top + e.Delta.Y, newOffset.Right, newOffset.Bottom);
1✔
161
        }
162

163
        // Clamp to min/max size
164
        newSize.X = Math.Max(newSize.X, windowComponent.MinSize.X);
1✔
165
        newSize.Y = Math.Max(newSize.Y, windowComponent.MinSize.Y);
1✔
166

167
        if (windowComponent.MaxSize.X > 0)
1✔
168
        {
169
            newSize.X = Math.Min(newSize.X, windowComponent.MaxSize.X);
1✔
170
        }
171

172
        if (windowComponent.MaxSize.Y > 0)
1✔
173
        {
174
            newSize.Y = Math.Min(newSize.Y, windowComponent.MaxSize.Y);
1✔
175
        }
176

177
        rect.Size = newSize;
1✔
178
        rect.Offset = newOffset;
1✔
179

180
        // Mark layout dirty
181
        if (!World.Has<UILayoutDirtyTag>(window))
1✔
182
        {
183
            World.Add(window, new UILayoutDirtyTag());
1✔
184
        }
185
    }
1✔
186

187
    private void OnClick(UIClickEvent e)
188
    {
189
        // Check if clicking a close button
190
        if (World.Has<UIWindowCloseButton>(e.Element))
1✔
191
        {
192
            HandleCloseClick(e);
1✔
193
            return;
1✔
194
        }
195

196
        // Check if clicking a minimize button
197
        if (World.Has<UIWindowMinimizeButton>(e.Element))
1✔
198
        {
199
            HandleMinimizeClick(e);
1✔
200
            return;
1✔
201
        }
202

203
        // Check if clicking a maximize button
204
        if (World.Has<UIWindowMaximizeButton>(e.Element))
1✔
205
        {
206
            HandleMaximizeClick(e);
1✔
207
            return;
1✔
208
        }
209

210
        // Check if clicking anywhere in a window (for z-order)
211
        var clickedWindow = FindParentWindow(e.Element);
1✔
212
        if (clickedWindow.IsValid)
1✔
213
        {
214
            BringToFront(clickedWindow);
1✔
215
        }
216
    }
1✔
217

218
    private void HandleCloseClick(UIClickEvent e)
219
    {
220
        ref readonly var closeButton = ref World.Get<UIWindowCloseButton>(e.Element);
1✔
221
        var window = closeButton.Window;
1✔
222

223
        if (!World.IsAlive(window) || !World.Has<UIWindow>(window))
1✔
224
        {
225
            return;
1✔
226
        }
227

228
        ref readonly var windowComponent = ref World.Get<UIWindow>(window);
1✔
229
        if (!windowComponent.CanClose)
1✔
230
        {
231
            return;
1✔
232
        }
233

234
        // Hide the window
235
        if (World.Has<UIElement>(window))
1✔
236
        {
237
            ref var element = ref World.Get<UIElement>(window);
1✔
238
            element.Visible = false;
1✔
239
        }
240

241
        // Add hidden tag so layout skips it
242
        if (!World.Has<UIHiddenTag>(window))
1✔
243
        {
244
            World.Add(window, new UIHiddenTag());
1✔
245
        }
246

247
        // Fire window closed event
248
        World.Send(new UIWindowClosedEvent(window));
1✔
249
    }
1✔
250

251
    private void HandleMinimizeClick(UIClickEvent e)
252
    {
253
        ref readonly var minimizeButton = ref World.Get<UIWindowMinimizeButton>(e.Element);
1✔
254
        var window = minimizeButton.Window;
1✔
255

256
        if (!World.IsAlive(window) || !World.Has<UIWindow>(window))
1✔
257
        {
258
            return;
1✔
259
        }
260

261
        ref var windowComponent = ref World.Get<UIWindow>(window);
1✔
262
        if (!windowComponent.CanMinimize)
1✔
263
        {
264
            return;
1✔
265
        }
266

267
        if (windowComponent.State == WindowState.Minimized)
1✔
268
        {
269
            // Restore from minimized
270
            RestoreWindow(window);
1✔
271
        }
272
        else
273
        {
274
            // Minimize the window
275
            MinimizeWindow(window);
1✔
276
        }
277
    }
1✔
278

279
    private void HandleMaximizeClick(UIClickEvent e)
280
    {
281
        ref readonly var maximizeButton = ref World.Get<UIWindowMaximizeButton>(e.Element);
1✔
282
        var window = maximizeButton.Window;
1✔
283

284
        if (!World.IsAlive(window) || !World.Has<UIWindow>(window))
1✔
285
        {
286
            return;
1✔
287
        }
288

289
        ref var windowComponent = ref World.Get<UIWindow>(window);
1✔
290
        if (!windowComponent.CanMaximize)
1✔
291
        {
292
            return;
1✔
293
        }
294

295
        if (windowComponent.State == WindowState.Maximized)
1✔
296
        {
297
            // Restore from maximized
298
            RestoreWindow(window);
1✔
299
        }
300
        else
301
        {
302
            // Maximize the window
303
            MaximizeWindow(window);
1✔
304
        }
305
    }
1✔
306

307
    private Entity FindParentWindow(Entity entity)
308
    {
309
        var current = entity;
1✔
310

311
        while (current.IsValid)
1✔
312
        {
313
            if (World.Has<UIWindow>(current))
1✔
314
            {
315
                return current;
1✔
316
            }
317

318
            current = World.GetParent(current);
1✔
319
        }
320

321
        return Entity.Null;
1✔
322
    }
323

324
    private void BringToFront(Entity window)
325
    {
326
        if (!World.Has<UIWindow>(window))
1✔
327
        {
328
            return;
×
329
        }
330

331
        ref var windowComponent = ref World.Get<UIWindow>(window);
1✔
332

333
        // Only update if not already at the front
334
        // When nextZOrder is 1, no window has been brought to front yet, so always update
335
        if (windowComponent.ZOrder != nextZOrder - 1 || nextZOrder == 1)
1✔
336
        {
337
            windowComponent.ZOrder = nextZOrder++;
1✔
338

339
            // Update the UIRect LocalZIndex to match
340
            if (World.Has<UIRect>(window))
1✔
341
            {
342
                ref var rect = ref World.Get<UIRect>(window);
1✔
343
                rect.LocalZIndex = (short)Math.Min(windowComponent.ZOrder, short.MaxValue);
1✔
344
            }
345
        }
346
    }
1✔
347

348
    /// <summary>
349
    /// Shows a window that was previously hidden.
350
    /// </summary>
351
    /// <param name="window">The window entity to show.</param>
352
    public void ShowWindow(Entity window)
353
    {
354
        if (!World.IsAlive(window) || !World.Has<UIWindow>(window))
1✔
355
        {
356
            return;
1✔
357
        }
358

359
        // Show the window
360
        if (World.Has<UIElement>(window))
1✔
361
        {
362
            ref var element = ref World.Get<UIElement>(window);
1✔
363
            element.Visible = true;
1✔
364
        }
365

366
        // Remove hidden tag
367
        if (World.Has<UIHiddenTag>(window))
1✔
368
        {
369
            World.Remove<UIHiddenTag>(window);
1✔
370
        }
371

372
        // Bring to front
373
        BringToFront(window);
1✔
374

375
        // Mark layout dirty
376
        if (!World.Has<UILayoutDirtyTag>(window))
1✔
377
        {
378
            World.Add(window, new UILayoutDirtyTag());
1✔
379
        }
380
    }
1✔
381

382
    /// <summary>
383
    /// Minimizes a window to show only its title bar.
384
    /// </summary>
385
    /// <param name="window">The window entity to minimize.</param>
386
    public void MinimizeWindow(Entity window)
387
    {
388
        if (!World.IsAlive(window) || !World.Has<UIWindow>(window))
1✔
389
        {
390
            return;
×
391
        }
392

393
        ref var windowComponent = ref World.Get<UIWindow>(window);
1✔
394
        if (!windowComponent.CanMinimize || windowComponent.State == WindowState.Minimized)
1✔
395
        {
396
            return;
1✔
397
        }
398

399
        // Store the current state so we can restore to it later
400
        windowComponent.PreMinimizeState = windowComponent.State;
1✔
401

402
        // Store current position and size for restore (only if Normal)
403
        if (windowComponent.State == WindowState.Normal && World.Has<UIRect>(window))
1✔
404
        {
405
            ref readonly var rect = ref World.Get<UIRect>(window);
1✔
406
            windowComponent.RestorePosition = new Vector2(rect.Offset.Left, rect.Offset.Top);
1✔
407
            windowComponent.RestoreSize = rect.Size;
1✔
408
        }
409

410
        windowComponent.State = WindowState.Minimized;
1✔
411

412
        // Hide the content panel
413
        if (windowComponent.ContentPanel.IsValid && World.Has<UIElement>(windowComponent.ContentPanel))
1✔
414
        {
415
            ref var contentElement = ref World.Get<UIElement>(windowComponent.ContentPanel);
1✔
416
            contentElement.Visible = false;
1✔
417

418
            if (!World.Has<UIHiddenTag>(windowComponent.ContentPanel))
1✔
419
            {
420
                World.Add(windowComponent.ContentPanel, new UIHiddenTag());
1✔
421
            }
422
        }
423

424
        // Collapse window to title bar height
425
        if (World.Has<UIRect>(window))
1✔
426
        {
427
            ref var rect = ref World.Get<UIRect>(window);
1✔
428

429
            // Get title bar height (default to 32px if not available)
430
            float titleBarHeight = 32f;
1✔
431
            if (windowComponent.TitleBar.IsValid && World.Has<UIRect>(windowComponent.TitleBar))
1✔
432
            {
UNCOV
433
                ref readonly var titleRect = ref World.Get<UIRect>(windowComponent.TitleBar);
×
UNCOV
434
                titleBarHeight = titleRect.Size.Y > 0 ? titleRect.Size.Y : titleBarHeight;
×
435
            }
436

437
            // Set window height to just the title bar
438
            rect.Size = new Vector2(rect.Size.X, titleBarHeight);
1✔
439
            rect.HeightMode = UISizeMode.Fixed;
1✔
440
        }
441

442
        // Mark layout dirty
443
        if (!World.Has<UILayoutDirtyTag>(window))
1✔
444
        {
445
            World.Add(window, new UILayoutDirtyTag());
1✔
446
        }
447

448
        // Fire minimized event
449
        World.Send(new UIWindowMinimizedEvent(window));
1✔
450
    }
1✔
451

452
    /// <summary>
453
    /// Maximizes a window to fill its parent's bounds.
454
    /// </summary>
455
    /// <param name="window">The window entity to maximize.</param>
456
    public void MaximizeWindow(Entity window)
457
    {
458
        if (!World.IsAlive(window) || !World.Has<UIWindow>(window))
1✔
459
        {
UNCOV
460
            return;
×
461
        }
462

463
        ref var windowComponent = ref World.Get<UIWindow>(window);
1✔
464
        if (!windowComponent.CanMaximize || windowComponent.State == WindowState.Maximized)
1✔
465
        {
466
            return;
1✔
467
        }
468

469
        // If currently minimized, restore content first
470
        if (windowComponent.State == WindowState.Minimized &&
1✔
471
            windowComponent.ContentPanel.IsValid &&
1✔
472
            World.Has<UIElement>(windowComponent.ContentPanel))
1✔
473
        {
474
            ref var contentElement = ref World.Get<UIElement>(windowComponent.ContentPanel);
1✔
475
            contentElement.Visible = true;
1✔
476

477
            if (World.Has<UIHiddenTag>(windowComponent.ContentPanel))
1✔
478
            {
479
                World.Remove<UIHiddenTag>(windowComponent.ContentPanel);
1✔
480
            }
481
        }
482

483
        // Store current position and size for restore (only if not already saved from minimize)
484
        if (windowComponent.State == WindowState.Normal && World.Has<UIRect>(window))
1✔
485
        {
486
            ref readonly var rect = ref World.Get<UIRect>(window);
1✔
487
            windowComponent.RestorePosition = new Vector2(rect.Offset.Left, rect.Offset.Top);
1✔
488
            windowComponent.RestoreSize = rect.Size;
1✔
489
        }
490

491
        windowComponent.State = WindowState.Maximized;
1✔
492

493
        // Maximize by setting anchors to stretch and clearing offset
494
        if (World.Has<UIRect>(window))
1✔
495
        {
496
            ref var rect = ref World.Get<UIRect>(window);
1✔
497
            rect.AnchorMin = Vector2.Zero;
1✔
498
            rect.AnchorMax = Vector2.One;
1✔
499
            rect.Offset = UIEdges.Zero;
1✔
500
            rect.Size = Vector2.Zero;
1✔
501
            rect.WidthMode = UISizeMode.Fill;
1✔
502
            rect.HeightMode = UISizeMode.Fill;
1✔
503
        }
504

505
        // Mark layout dirty
506
        if (!World.Has<UILayoutDirtyTag>(window))
1✔
507
        {
508
            World.Add(window, new UILayoutDirtyTag());
1✔
509
        }
510

511
        // Fire maximized event
512
        World.Send(new UIWindowMaximizedEvent(window));
1✔
513
    }
1✔
514

515
    /// <summary>
516
    /// Restores a window from minimized or maximized state to its normal state.
517
    /// </summary>
518
    /// <param name="window">The window entity to restore.</param>
519
    public void RestoreWindow(Entity window)
520
    {
521
        if (!World.IsAlive(window) || !World.Has<UIWindow>(window))
1✔
522
        {
UNCOV
523
            return;
×
524
        }
525

526
        ref var windowComponent = ref World.Get<UIWindow>(window);
1✔
527
        if (windowComponent.State == WindowState.Normal)
1✔
528
        {
529
            return;
1✔
530
        }
531

532
        var previousState = windowComponent.State;
1✔
533

534
        // When restoring from minimized, check if we should restore to maximized state
535
        if (previousState == WindowState.Minimized && windowComponent.PreMinimizeState == WindowState.Maximized)
1✔
536
        {
537
            // Restore content panel visibility first
UNCOV
538
            if (windowComponent.ContentPanel.IsValid && World.Has<UIElement>(windowComponent.ContentPanel))
×
539
            {
UNCOV
540
                ref var contentElement = ref World.Get<UIElement>(windowComponent.ContentPanel);
×
UNCOV
541
                contentElement.Visible = true;
×
542

UNCOV
543
                if (World.Has<UIHiddenTag>(windowComponent.ContentPanel))
×
544
                {
UNCOV
545
                    World.Remove<UIHiddenTag>(windowComponent.ContentPanel);
×
546
                }
547
            }
548

549
            // Set state to normal temporarily so MaximizeWindow will work
UNCOV
550
            windowComponent.State = WindowState.Normal;
×
551

552
            // Re-maximize the window
UNCOV
553
            MaximizeWindow(window);
×
554

555
            // Fire restored event indicating we came from minimized
UNCOV
556
            World.Send(new UIWindowRestoredEvent(window, previousState));
×
UNCOV
557
            return;
×
558
        }
559

560
        // Standard restore to Normal state
561
        windowComponent.State = WindowState.Normal;
1✔
562

563
        // Restore content panel visibility
564
        if (windowComponent.ContentPanel.IsValid && World.Has<UIElement>(windowComponent.ContentPanel))
1✔
565
        {
566
            ref var contentElement = ref World.Get<UIElement>(windowComponent.ContentPanel);
1✔
567
            contentElement.Visible = true;
1✔
568

569
            if (World.Has<UIHiddenTag>(windowComponent.ContentPanel))
1✔
570
            {
571
                World.Remove<UIHiddenTag>(windowComponent.ContentPanel);
1✔
572
            }
573
        }
574

575
        // Restore position and size
576
        if (World.Has<UIRect>(window))
1✔
577
        {
578
            ref var rect = ref World.Get<UIRect>(window);
1✔
579
            rect.AnchorMin = Vector2.Zero;
1✔
580
            rect.AnchorMax = Vector2.Zero;
1✔
581
            rect.Pivot = Vector2.Zero;
1✔
582
            rect.Offset = new UIEdges(windowComponent.RestorePosition.X, windowComponent.RestorePosition.Y, 0, 0);
1✔
583
            rect.Size = windowComponent.RestoreSize;
1✔
584
            rect.WidthMode = UISizeMode.Fixed;
1✔
585
            rect.HeightMode = UISizeMode.Fixed;
1✔
586
        }
587

588
        // Mark layout dirty
589
        if (!World.Has<UILayoutDirtyTag>(window))
1✔
590
        {
591
            World.Add(window, new UILayoutDirtyTag());
1✔
592
        }
593

594
        // Fire restored event
595
        World.Send(new UIWindowRestoredEvent(window, previousState));
1✔
596
    }
1✔
597
}
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