• 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

98.88
/src/KeenEyes.UI/Systems/UITooltipSystem.cs
1
using System.Numerics;
2
using KeenEyes.UI.Abstractions;
3

4
namespace KeenEyes.UI;
5

6
/// <summary>
7
/// System that manages tooltip display and popover behavior.
8
/// </summary>
9
/// <remarks>
10
/// <para>
11
/// This system tracks hover state on elements with <see cref="UITooltip"/> components
12
/// and shows/hides tooltips after the configured delay. It also manages popover
13
/// open/close behavior based on their trigger type.
14
/// </para>
15
/// <para>
16
/// This system should run in <see cref="SystemPhase.EarlyUpdate"/> phase after
17
/// <see cref="UISplitterSystem"/>.
18
/// </para>
19
/// </remarks>
20
public sealed class UITooltipSystem : SystemBase
21
{
22
    private EventSubscription? pointerEnterSubscription;
23
    private EventSubscription? pointerExitSubscription;
24
    private EventSubscription? clickSubscription;
25

26
    private Entity currentHoveredElement = Entity.Null;
1✔
27
    private float hoverTime;
28
    private Vector2 hoverPosition;
29
    private Entity activeTooltip = Entity.Null;
1✔
30

31
    /// <inheritdoc />
32
    protected override void OnInitialize()
33
    {
34
        pointerEnterSubscription = World.Subscribe<UIPointerEnterEvent>(OnPointerEnter);
1✔
35
        pointerExitSubscription = World.Subscribe<UIPointerExitEvent>(OnPointerExit);
1✔
36
        clickSubscription = World.Subscribe<UIClickEvent>(OnClick);
1✔
37
    }
1✔
38

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

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

55
    /// <inheritdoc />
56
    public override void Update(float deltaTime)
57
    {
58
        // Update hover timing for tooltip delay
59
        if (currentHoveredElement.IsValid &&
1✔
60
            World.IsAlive(currentHoveredElement) &&
1✔
61
            World.Has<UITooltip>(currentHoveredElement))
1✔
62
        {
63
            hoverTime += deltaTime;
1✔
64
            ref readonly var tooltip = ref World.Get<UITooltip>(currentHoveredElement);
1✔
65

66
            // Show tooltip after delay if not already shown
67
            if (hoverTime >= tooltip.Delay && !activeTooltip.IsValid)
1✔
68
            {
69
                ShowTooltip(currentHoveredElement, tooltip);
1✔
70
            }
71
        }
72

73
        // Update popover positions if needed (for follow-mouse behavior)
74
        // Currently not implemented - popovers stay anchored
75
    }
1✔
76

77
    private void OnPointerEnter(UIPointerEnterEvent e)
78
    {
79
        // Track the hovered element for tooltip timing
80
        if (World.Has<UITooltip>(e.Element))
1✔
81
        {
82
            currentHoveredElement = e.Element;
1✔
83
            hoverTime = 0f;
1✔
84
            hoverPosition = e.Position;
1✔
85
        }
86

87
        // Handle hover-triggered popovers
88
        if (World.Has<UIPopover>(e.Element))
1✔
89
        {
90
            ref var popover = ref World.Get<UIPopover>(e.Element);
1✔
91
            if (popover.Trigger == PopoverTrigger.Hover && !popover.IsOpen)
1✔
92
            {
93
                OpenPopover(e.Element, ref popover);
1✔
94
            }
95
        }
96
    }
1✔
97

98
    private void OnPointerExit(UIPointerExitEvent e)
99
    {
100
        // Hide tooltip when leaving the triggering element
101
        if (e.Element == currentHoveredElement)
1✔
102
        {
103
            HideTooltip();
1✔
104
            currentHoveredElement = Entity.Null;
1✔
105
            hoverTime = 0f;
1✔
106
        }
107

108
        // Handle hover-triggered popover closing
109
        if (World.Has<UIPopover>(e.Element))
1✔
110
        {
111
            ref var popover = ref World.Get<UIPopover>(e.Element);
1✔
112
            if (popover.Trigger == PopoverTrigger.Hover && popover.IsOpen)
1✔
113
            {
114
                ClosePopover(e.Element, ref popover);
1✔
115
            }
116
        }
117
    }
1✔
118

119
    private void OnClick(UIClickEvent e)
120
    {
121
        // Handle click-triggered popovers
122
        if (World.Has<UIPopover>(e.Element))
1✔
123
        {
124
            ref var popover = ref World.Get<UIPopover>(e.Element);
1✔
125
            if (popover.Trigger == PopoverTrigger.Click)
1✔
126
            {
127
                if (popover.IsOpen)
1✔
128
                {
129
                    ClosePopover(e.Element, ref popover);
1✔
130
                }
131
                else
132
                {
133
                    OpenPopover(e.Element, ref popover);
1✔
134
                }
135

136
                return;
1✔
137
            }
138
        }
139

140
        // Close popovers when clicking outside (if configured)
141
        ClosePopoversOnClickOutside(e.Element);
1✔
142
    }
1✔
143

144
    private void ShowTooltip(Entity element, in UITooltip tooltip)
145
    {
146
        // Calculate tooltip position
147
        Vector2 position = CalculateTooltipPosition(element, tooltip);
1✔
148

149
        // Create the tooltip entity with improved styling
150
        activeTooltip = World.Spawn()
1✔
151
            .With(new UIElement { Visible = true, RaycastTarget = false })
1✔
152
            .With(new UIRect
1✔
153
            {
1✔
154
                AnchorMin = Vector2.Zero,
1✔
155
                AnchorMax = Vector2.Zero,
1✔
156
                Pivot = Vector2.Zero,
1✔
157
                Offset = new UIEdges(position.X, position.Y, 0, 0),
1✔
158
                Size = new Vector2(tooltip.MaxWidth > 0 ? tooltip.MaxWidth : 200, 0),
1✔
159
                WidthMode = tooltip.MaxWidth > 0 ? UISizeMode.Fixed : UISizeMode.FitContent,
1✔
160
                HeightMode = UISizeMode.FitContent,
1✔
161
                LocalZIndex = 1000  // Tooltips on top
1✔
162
            })
1✔
163
            .With(new UIStyle
1✔
164
            {
1✔
165
                // Lighter, more modern background color
1✔
166
                BackgroundColor = new Vector4(0.2f, 0.2f, 0.25f, 0.95f),
1✔
167
                // Subtle border for depth
1✔
168
                BorderColor = new Vector4(0.35f, 0.35f, 0.4f, 0.8f),
1✔
169
                BorderWidth = 1,
1✔
170
                CornerRadius = 6,
1✔
171
                // Generous padding for readability
1✔
172
                Padding = new UIEdges(12, 10, 12, 10)
1✔
173
            })
1✔
174
            .With(new UIText
1✔
175
            {
1✔
176
                Content = tooltip.Text,
1✔
177
                Color = new Vector4(0.95f, 0.95f, 0.95f, 1f),
1✔
178
                FontSize = 13,
1✔
179
                WordWrap = tooltip.MaxWidth > 0
1✔
180
            })
1✔
181
            .With(new UITooltipVisibleTag())
1✔
182
            .Build();
1✔
183

184
        // Find a canvas to parent the tooltip to
185
        var canvas = FindCanvas();
1✔
186
        if (canvas.IsValid)
1✔
187
        {
188
            World.SetParent(activeTooltip, canvas);
1✔
189
        }
190

191
        // Fire event
192
        World.Send(new UITooltipShowEvent(element, tooltip.Text, position));
1✔
193
    }
1✔
194

195
    private void HideTooltip()
196
    {
197
        if (activeTooltip.IsValid && World.IsAlive(activeTooltip))
1✔
198
        {
199
            var element = currentHoveredElement;
1✔
200
            World.Despawn(activeTooltip);
1✔
201
            World.Send(new UITooltipHideEvent(element));
1✔
202
        }
203

204
        activeTooltip = Entity.Null;
1✔
205
    }
1✔
206

207
    private Vector2 CalculateTooltipPosition(Entity element, in UITooltip tooltip)
208
    {
209
        // Get element bounds
210
        if (!World.Has<UIRect>(element))
1✔
211
        {
212
            return hoverPosition + new Vector2(10, 10);
1✔
213
        }
214

215
        ref readonly var rect = ref World.Get<UIRect>(element);
1✔
216
        var bounds = rect.ComputedBounds;
1✔
217

218
        // Default to below the element
219
        float x = bounds.X;
1✔
220
        float y = bounds.Y + bounds.Height + 5;
1✔
221

222
        switch (tooltip.Position)
1✔
223
        {
224
            case TooltipPosition.Above:
225
                y = bounds.Y - 35;  // Estimate tooltip height
1✔
226
                break;
1✔
227
            case TooltipPosition.Below:
UNCOV
228
                y = bounds.Y + bounds.Height + 5;
×
UNCOV
229
                break;
×
230
            case TooltipPosition.Left:
231
                x = bounds.X - 210;  // Estimate tooltip width
1✔
232
                y = bounds.Y;
1✔
233
                break;
1✔
234
            case TooltipPosition.Right:
235
                x = bounds.X + bounds.Width + 5;
1✔
236
                y = bounds.Y;
1✔
237
                break;
238
            case TooltipPosition.Auto:
239
            default:
240
                // Default to below, could add screen boundary checking
241
                break;
242
        }
243

244
        return new Vector2(x, y);
1✔
245
    }
246

247
    private void OpenPopover(Entity popoverEntity, ref UIPopover popover)
248
    {
249
        popover.IsOpen = true;
1✔
250

251
        // Make the popover visible
252
        if (World.Has<UIElement>(popoverEntity))
1✔
253
        {
254
            ref var element = ref World.Get<UIElement>(popoverEntity);
1✔
255
            element.Visible = true;
1✔
256
        }
257

258
        if (World.Has<UIHiddenTag>(popoverEntity))
1✔
259
        {
260
            World.Remove<UIHiddenTag>(popoverEntity);
1✔
261
        }
262

263
        // Mark layout dirty
264
        if (!World.Has<UILayoutDirtyTag>(popoverEntity))
1✔
265
        {
266
            World.Add(popoverEntity, new UILayoutDirtyTag());
1✔
267
        }
268

269
        World.Send(new UIPopoverOpenedEvent(popoverEntity, popover.TriggerElement));
1✔
270
    }
1✔
271

272
    private void ClosePopover(Entity popoverEntity, ref UIPopover popover)
273
    {
274
        popover.IsOpen = false;
1✔
275

276
        // Hide the popover
277
        if (World.Has<UIElement>(popoverEntity))
1✔
278
        {
279
            ref var element = ref World.Get<UIElement>(popoverEntity);
1✔
280
            element.Visible = false;
1✔
281
        }
282

283
        if (!World.Has<UIHiddenTag>(popoverEntity))
1✔
284
        {
285
            World.Add(popoverEntity, new UIHiddenTag());
1✔
286
        }
287

288
        World.Send(new UIPopoverClosedEvent(popoverEntity));
1✔
289
    }
1✔
290

291
    private void ClosePopoversOnClickOutside(Entity clickedElement)
292
    {
293
        // Query all open popovers and close those configured to close on outside click
294
        foreach (var entity in World.Query<UIPopover>())
1✔
295
        {
296
            ref var popover = ref World.Get<UIPopover>(entity);
1✔
297
            // Check if click was outside this popover
298
            if (popover.IsOpen &&
1✔
299
                popover.CloseOnClickOutside &&
1✔
300
                !IsDescendantOf(clickedElement, entity) &&
1✔
301
                clickedElement != entity)
1✔
302
            {
303
                ClosePopover(entity, ref popover);
1✔
304
            }
305
        }
306
    }
1✔
307

308
    private bool IsDescendantOf(Entity child, Entity potentialAncestor)
309
    {
310
        var current = child;
1✔
311
        while (current.IsValid)
1✔
312
        {
313
            if (current == potentialAncestor)
1✔
314
            {
315
                return true;
1✔
316
            }
317

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

321
        return false;
1✔
322
    }
323

324
    private Entity FindCanvas()
325
    {
326
        // Find a root canvas to parent tooltips to
327
        foreach (var entity in World.Query<UIRootTag>())
1✔
328
        {
329
            return entity;
1✔
330
        }
331

332
        return Entity.Null;
1✔
333
    }
1✔
334
}
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