• 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

0.65
/src/KeenEyes.Graph/Systems/GraphContextMenuSystem.cs
1
using KeenEyes.Graph.Abstractions;
2
using KeenEyes.Input.Abstractions;
3

4
namespace KeenEyes.Graph;
5

6
/// <summary>
7
/// System that processes context menu interactions for graph editing.
8
/// </summary>
9
/// <remarks>
10
/// <para>
11
/// Handles keyboard navigation (arrow keys, Enter, Escape), search filtering,
12
/// and menu item execution for context menus on graph canvases.
13
/// </para>
14
/// </remarks>
15
public sealed class GraphContextMenuSystem : SystemBase
16
{
17
    private IInputContext? inputContext;
18
    private GraphContext? graphContext;
19
    private PortRegistry? portRegistry;
20
    private NodeTypeRegistry? nodeTypeRegistry;
21

22
    // Key state for debouncing
23
    private readonly HashSet<Key> keysDownLastFrame = [];
1✔
24

25
    /// <inheritdoc />
26
    public override void Update(float deltaTime)
27
    {
28
        // Lazy initialization
29
        if (inputContext is null && !World.TryGetExtension(out inputContext))
×
30
        {
31
            return;
×
32
        }
33

34
        if (graphContext is null && !World.TryGetExtension(out graphContext))
×
35
        {
36
            return;
×
37
        }
38

39
        if (portRegistry is null && !World.TryGetExtension(out portRegistry))
×
40
        {
41
            return;
×
42
        }
43

44
        if (nodeTypeRegistry is null)
×
45
        {
46
            World.TryGetExtension(out nodeTypeRegistry);
×
47
        }
48

49
        var keyboard = inputContext!.Keyboard;
×
50

51
        // Process each canvas with an active context menu
52
        foreach (var canvas in World.Query<GraphCanvas, GraphContextMenu, GraphCanvasTag>())
×
53
        {
54
            ProcessContextMenu(canvas, keyboard);
×
55
        }
56

57
        // Update key state for next frame
58
        UpdateKeyState(keyboard);
×
59
    }
×
60

61
    private void ProcessContextMenu(Entity canvas, IKeyboard keyboard)
62
    {
63
        ref var canvasData = ref World.Get<GraphCanvas>(canvas);
×
64
        ref var menu = ref World.Get<GraphContextMenu>(canvas);
×
65

66
        // Handle Escape - close menu
67
        if (WasKeyJustPressed(keyboard, Key.Escape))
×
68
        {
69
            CloseMenu(canvas, ref canvasData);
×
70
            return;
×
71
        }
72

73
        // Handle menu type-specific logic
74
        switch (menu.MenuType)
×
75
        {
76
            case ContextMenuType.Canvas:
77
                ProcessCanvasMenu(canvas, ref canvasData, ref menu, keyboard);
×
78
                break;
×
79

80
            case ContextMenuType.Node:
81
                ProcessNodeMenu(canvas, ref canvasData, ref menu, keyboard);
×
82
                break;
×
83

84
            case ContextMenuType.Connection:
85
                ProcessConnectionMenu(canvas, ref canvasData, ref menu, keyboard);
×
86
                break;
87
        }
88
    }
×
89

90
    private void ProcessCanvasMenu(Entity canvas, ref GraphCanvas canvasData, ref GraphContextMenu menu, IKeyboard keyboard)
91
    {
92
        // Get filtered node types
93
        var nodeTypes = GetFilteredNodeTypes(menu.SearchFilter);
×
94

95
        if (nodeTypes.Count == 0)
×
96
        {
97
            return;
×
98
        }
99

100
        // Handle arrow key navigation
101
        if (WasKeyJustPressed(keyboard, Key.Down))
×
102
        {
103
            menu.SelectedIndex = (menu.SelectedIndex + 1) % nodeTypes.Count;
×
104
        }
105
        else if (WasKeyJustPressed(keyboard, Key.Up))
×
106
        {
107
            menu.SelectedIndex = (menu.SelectedIndex - 1 + nodeTypes.Count) % nodeTypes.Count;
×
108
        }
109

110
        // Handle Enter - create selected node
111
        if (WasKeyJustPressed(keyboard, Key.Enter))
×
112
        {
113
            var selectedType = nodeTypes[menu.SelectedIndex];
×
114
            graphContext!.CreateNodeUndoable(canvas, selectedType.TypeId, menu.CanvasPosition);
×
115
            CloseMenu(canvas, ref canvasData);
×
116
        }
117

118
        // Handle alphanumeric input for search filtering
119
        UpdateSearchFilter(ref menu, keyboard);
×
120
    }
×
121

122
    private void ProcessNodeMenu(Entity canvas, ref GraphCanvas canvasData, ref GraphContextMenu menu, IKeyboard keyboard)
123
    {
124
        // Node menu options: Delete, Duplicate, Copy, Cut
125
        var options = new[] { "Delete", "Duplicate" };
×
126

127
        // Handle arrow key navigation
128
        if (WasKeyJustPressed(keyboard, Key.Down))
×
129
        {
130
            menu.SelectedIndex = (menu.SelectedIndex + 1) % options.Length;
×
131
        }
132
        else if (WasKeyJustPressed(keyboard, Key.Up))
×
133
        {
134
            menu.SelectedIndex = (menu.SelectedIndex - 1 + options.Length) % options.Length;
×
135
        }
136

137
        // Handle Enter - execute selected option
138
        if (WasKeyJustPressed(keyboard, Key.Enter))
×
139
        {
140
            var selectedOption = options[menu.SelectedIndex];
×
141

142
            switch (selectedOption)
143
            {
144
                case "Delete":
145
                    if (menu.TargetEntity.IsValid)
×
146
                    {
147
                        graphContext!.DeleteNodesUndoable([menu.TargetEntity]);
×
148
                    }
149
                    break;
×
150

151
                case "Duplicate":
152
                    if (menu.TargetEntity.IsValid)
×
153
                    {
154
                        graphContext!.DuplicateSelectionUndoable();
×
155
                    }
156
                    break;
157
            }
158

159
            CloseMenu(canvas, ref canvasData);
×
160
        }
161
    }
×
162

163
    private void ProcessConnectionMenu(Entity canvas, ref GraphCanvas canvasData, ref GraphContextMenu menu, IKeyboard keyboard)
164
    {
165
        // Handle Enter - delete connection
166
        if (WasKeyJustPressed(keyboard, Key.Enter))
×
167
        {
168
            if (menu.TargetEntity.IsValid)
×
169
            {
170
                graphContext!.DeleteConnectionUndoable(menu.TargetEntity);
×
171
            }
172

173
            CloseMenu(canvas, ref canvasData);
×
174
        }
175
    }
×
176

177
    private void CloseMenu(Entity canvas, ref GraphCanvas canvasData)
178
    {
179
        World.Remove<GraphContextMenu>(canvas);
×
180
        canvasData.Mode = GraphInteractionMode.None;
×
181
    }
×
182

183
    private List<PortRegistry.NodeTypeInfo> GetFilteredNodeTypes(string filter)
184
    {
185
        var allTypes = portRegistry!.GetAllNodeTypes().ToList();
×
186

187
        if (string.IsNullOrWhiteSpace(filter))
×
188
        {
189
            // When no filter, sort by category then by name for better organization
190
            return allTypes
×
191
                .OrderBy(t => t.Category)
×
192
                .ThenBy(t => t.Name)
×
193
                .ToList();
×
194
        }
195

196
        var lowerFilter = filter.ToLowerInvariant();
×
197
        return allTypes
×
198
            .Where(t => t.Name.ToLowerInvariant().Contains(lowerFilter) ||
×
199
                        t.Category.ToLowerInvariant().Contains(lowerFilter))
×
200
            .OrderBy(t => t.Category)
×
201
            .ThenBy(t => t.Name)
×
202
            .ToList();
×
203
    }
204

205
    /// <summary>
206
    /// Gets node types organized by category for hierarchical display.
207
    /// </summary>
208
    /// <param name="filter">Optional search filter.</param>
209
    /// <returns>Dictionary of category name to list of node types in that category.</returns>
210
    internal Dictionary<string, List<PortRegistry.NodeTypeInfo>> GetNodeTypesByCategory(string filter)
211
    {
212
        var filtered = GetFilteredNodeTypes(filter);
×
213
        return filtered
×
214
            .GroupBy(t => t.Category)
×
215
            .OrderBy(g => g.Key)
×
216
            .ToDictionary(g => g.Key, g => g.ToList());
×
217
    }
218

219
    /// <summary>
220
    /// Gets all categories with registered node types.
221
    /// </summary>
222
    /// <returns>Sorted list of category names.</returns>
223
    internal IReadOnlyList<string> GetCategories()
224
    {
225
        if (nodeTypeRegistry is not null)
×
226
        {
227
            return nodeTypeRegistry.GetCategories().ToList();
×
228
        }
229

230
        return portRegistry?.GetCategories().ToList() ?? [];
×
231
    }
232

233
    private void UpdateSearchFilter(ref GraphContextMenu menu, IKeyboard keyboard)
234
    {
235
        // Handle backspace
236
        if (WasKeyJustPressed(keyboard, Key.Backspace) && menu.SearchFilter.Length > 0)
×
237
        {
238
            menu.SearchFilter = menu.SearchFilter[..^1];
×
239
            menu.SelectedIndex = 0; // Reset selection when filter changes
×
240
            return;
×
241
        }
242

243
        // Handle alphanumeric keys (simplified - in a real impl, use proper text input)
244
        for (var key = Key.A; key <= Key.Z; key++)
×
245
        {
246
            if (WasKeyJustPressed(keyboard, key))
×
247
            {
248
                var shift = (keyboard.Modifiers & KeyModifiers.Shift) != 0;
×
249
                var ch = shift ? (char)key : char.ToLowerInvariant((char)key);
×
250
                menu.SearchFilter += ch;
×
251
                menu.SelectedIndex = 0;
×
252
                return;
×
253
            }
254
        }
255

256
        // Handle number keys
257
        for (var key = Key.Number0; key <= Key.Number9; key++)
×
258
        {
259
            if (WasKeyJustPressed(keyboard, key))
×
260
            {
261
                var digit = (char)('0' + (key - Key.Number0));
×
262
                menu.SearchFilter += digit;
×
263
                menu.SelectedIndex = 0;
×
264
                return;
×
265
            }
266
        }
267

268
        // Handle space
269
        if (WasKeyJustPressed(keyboard, Key.Space))
×
270
        {
271
            menu.SearchFilter += ' ';
×
272
            menu.SelectedIndex = 0;
×
273
        }
274
    }
×
275

276
    private bool WasKeyJustPressed(IKeyboard keyboard, Key key)
277
    {
278
        var isDownNow = keyboard.IsKeyDown(key);
×
279
        var wasDownLastFrame = keysDownLastFrame.Contains(key);
×
280
        return isDownNow && !wasDownLastFrame;
×
281
    }
282

283
    // Keys that the context menu system cares about
UNCOV
284
    private static readonly Key[] trackedKeys =
×
285
    [
×
UNCOV
286
        Key.Escape,  // Close menu
×
UNCOV
287
        Key.Enter,   // Confirm selection
×
288
        Key.Up,      // Navigate up
×
UNCOV
289
        Key.Down,    // Navigate down
×
290
        Key.Left,    // Navigate left/collapse
×
UNCOV
291
        Key.Right,   // Navigate right/expand
×
292
    ];
×
293

294
    private void UpdateKeyState(IKeyboard keyboard)
295
    {
UNCOV
296
        keysDownLastFrame.Clear();
×
297

298
        // Only track keys relevant to context menu navigation
UNCOV
299
        foreach (var key in trackedKeys)
×
300
        {
UNCOV
301
            if (keyboard.IsKeyDown(key))
×
302
            {
UNCOV
303
                keysDownLastFrame.Add(key);
×
304
            }
305
        }
UNCOV
306
    }
×
307
}
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