Gondwana Game Engine Development (master) - 2.6.1
Gondwana is a cross-platform 2.5D game and rendering engine written in C#/.NET 8. It provides fine-grained control over rendering, timing, and scene composition, with built-in support for parallax, z-ordering, pixel overhang, collision detection, and particle effects. Gondwana targets desktop and web platforms using SkiaSharp for graphics and pluggable audio backends for desktop and browser targets.
Loading...
Searching...
No Matches
MenuBarWidget.cs
1using System.Drawing;
2using System.Numerics;
7
9
13public sealed class MenuBarWidget : ContainerWidget, IWidgetKeyboardFallback
14{
15 private readonly View _view;
16 private readonly Rectangle _bounds;
17 private readonly MenuBarTheme _theme;
18 private readonly DismissLayerWidget _dismissLayer;
19 private readonly List<MenuBarMenu> _menus = new();
20
21 private readonly Dictionary<string, MenuItemWidget> _itemsByKey = new(StringComparer.Ordinal);
22 private readonly Dictionary<KeyGesture, MenuItemWidget> _shortcuts = new();
23 internal bool IsAvailable => !_disposed && Visible && IsInputEnabled && IsKeyboardInputEnabled;
24
28 public MenuItemWidget GetItem(string key) => _itemsByKey[key];
31 public MenuItemWidget this[string key] => GetItem(key);
36 public bool TryGetItem(string key, [System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out MenuItemWidget? item) =>
37 _itemsByKey.TryGetValue(key, out item);
38
39 internal void ValidateRegistration(string? key, KeyGesture? shortcut)
40 {
41 ObjectDisposedException.ThrowIf(_disposed, this);
42 if (key is not null)
43 {
44 ArgumentException.ThrowIfNullOrWhiteSpace(key);
45 if (_itemsByKey.ContainsKey(key)) throw new ArgumentException($"Duplicate menu item key '{key}'.", nameof(key));
46 }
47 if (shortcut is { } gesture)
48 {
49 if (gesture.Key <= 0) throw new ArgumentException("A shortcut requires a nonzero key.", nameof(shortcut));
50 if (_shortcuts.ContainsKey(gesture)) throw new ArgumentException($"Duplicate menu shortcut '{gesture}'.", nameof(shortcut));
51 }
52 }
53
54 internal void RegisterItem(MenuItemWidget item)
55 {
56 if (item.Key is not null) _itemsByKey.Add(item.Key, item);
57 if (item.Shortcut is { } gesture) _shortcuts.Add(gesture, item);
58 }
59
60 internal void UnregisterItem(MenuItemWidget item)
61 {
62 if (item.Key is not null && _itemsByKey.GetValueOrDefault(item.Key) == item) _itemsByKey.Remove(item.Key);
63 if (item.Shortcut is { } gesture && _shortcuts.GetValueOrDefault(gesture) == item) _shortcuts.Remove(gesture);
64 }
65
66 private WidgetBase? _previousFocus;
67 private int _openMenuIndex = -1;
68 private int _nextHeaderX;
69 private int _menuZOrder = 20_000;
70 private bool _disposed;
71
73 public event Action<MenuBarMenu>? MenuOpened;
74
76 public event Action<MenuBarMenu>? MenuClosed;
77
79 public event Action<MenuItemWidget>? ItemInvoked;
80
90 View view,
91 Rectangle bounds,
92 MenuBarTheme? theme = null,
93 string? nickname = null)
94 : base(host, DirectDrawingMode.View, bounds.Location, nickname)
95 {
96 ArgumentNullException.ThrowIfNull(view);
97
98 if (bounds.Width <= 0 || bounds.Height <= 0)
99 {
100 throw new ArgumentOutOfRangeException(
101 nameof(bounds),
102 bounds,
103 "Menu bar bounds must have positive width and height.");
104 }
105
106 _view = view;
107 _bounds = bounds;
108 _theme = theme ?? MenuBarTheme.Default;
109 _nextHeaderX = bounds.X;
110
112 _theme.BarBackgroundColor,
113 host,
114 view,
115 bounds,
116 $"{Nickname}.background")
117 .SetFilled(true)
118 .SetBorderColor(_theme.BarBorderColor)
119 .SetStrokeWidth(_theme.BorderWidth);
120
121 Rectangle dismissBounds = view.Viewport.TargetRectPx;
122 _dismissLayer = new DismissLayerWidget(
123 host,
124 view,
125 dismissBounds,
126 $"{Nickname}.dismiss");
127
128 _dismissLayer.DismissRequested += OnDismissRequested;
129 _dismissLayer.SetDismissEnabled(false);
130
131 Add(
132 _dismissLayer,
133 new Vector2(
134 dismissBounds.X - bounds.X,
135 dismissBounds.Y - bounds.Y));
136
138
139 CanReceiveFocus = false;
142
143 SetMenuZOrder(_menuZOrder);
144 }
145
148
150 public IReadOnlyList<MenuBarMenu> Menus => _menus.AsReadOnly();
151
153 public int OpenMenuIndex => _openMenuIndex;
154
157 _openMenuIndex >= 0 && _openMenuIndex < _menus.Count
158 ? _menus[_openMenuIndex]
159 : null;
160
163 MenuDropDownAnimation.FadeAndReveal;
164
166 public float DropDownAnimationDurationSec { get; set; } = 0.13f;
167
176 public MenuBarWidget AddMenu(string text,
177 Action<MenuDropDownWidget>? configure = null,
178 int? headerWidth = null,
179 char? mnemonic = null)
180 {
181 ObjectDisposedException.ThrowIf(_disposed, this);
182 ArgumentNullException.ThrowIfNull(text);
183 if (mnemonic is char character && _menus.Any(menu => menu.Header.Mnemonic is char existing &&
184 char.ToUpperInvariant(existing) == char.ToUpperInvariant(character)))
185 throw new ArgumentException($"Duplicate top-level mnemonic '{character}'.", nameof(mnemonic));
186
187 int resolvedHeaderWidth = headerWidth ?? EstimateHeaderWidth(text);
188
189 if (resolvedHeaderWidth <= 0)
190 {
191 throw new ArgumentOutOfRangeException(
192 nameof(headerWidth),
193 resolvedHeaderWidth,
194 "Menu header width must be positive.");
195 }
196
197 var headerBounds = new Rectangle(
198 _nextHeaderX,
199 _bounds.Y,
200 resolvedHeaderWidth,
201 _bounds.Height);
202
203 int menuIndex = _menus.Count;
204
205 var header = new MenuHeaderWidget(
207 _view,
208 headerBounds,
209 text,
210 _theme,
211 $"{Nickname}.header.{menuIndex}", mnemonic);
212
213 var dropDown = new MenuDropDownWidget(
215 _view,
216 new Point(headerBounds.X, _bounds.Bottom),
217 _theme,
218 $"{Nickname}.dropdown.{menuIndex}", this);
219
220 try
221 {
222 configure?.Invoke(dropDown);
223 }
224 catch
225 {
226 dropDown.Dispose();
227 header.Dispose();
228 throw;
229 }
230 PositionDropDown(headerBounds, dropDown);
231
232 var menu = new MenuBarMenu(header, dropDown);
233 _menus.Add(menu);
234
235 header.Invoked += OnHeaderInvoked;
236 header.Hovered += OnHeaderHovered;
237
238 Add(
239 header,
240 new Vector2(
241 headerBounds.X - _bounds.X,
242 headerBounds.Y - _bounds.Y));
243
244 Add(
245 dropDown,
246 new Vector2(
247 dropDown.GetPosition().X - _bounds.X,
248 dropDown.GetPosition().Y - _bounds.Y));
249
250 // Dropdown drawings are visible immediately after construction; menus start closed.
251 dropDown.CloseAnimated(MenuDropDownAnimation.None, 0f, immediate: true);
252
253 _nextHeaderX += resolvedHeaderWidth;
254 ApplyMenuZOrder(_menuZOrder);
255
256 return this;
257 }
258
261 public void OpenMenuAt(int index)
262 {
263 if (index < 0 || index >= _menus.Count)
264 throw new ArgumentOutOfRangeException(nameof(index));
265
266 if (!IsAvailable || _openMenuIndex == index)
267 return;
268
269 CloseMenuInternal(immediate: true);
270
271 _previousFocus = WidgetInputRouterRegistry.GetFocusedWidget(this);
272 _openMenuIndex = index;
273 MenuBarMenu menu = _menus[index];
274
275 menu.Header.SetOpen(true);
276
277 foreach (MenuBarMenu candidate in _menus)
278 candidate.Header.SetMenuBarActive(true);
279
280 _dismissLayer.SetDismissEnabled(true);
281
282 // Input routing is registration ordered rather than drawing-Z ordered.
283 // Promote the accelerator participant without recursively activating its children.
284 WidgetInputRouterRegistry.TryBringToFront(this);
285 _dismissLayer.Activate();
286
287 foreach (MenuBarMenu candidate in _menus)
288 candidate.Header.Activate();
289
290 PositionDropDown(menu.Header.Background.ScreenBounds, menu.DropDown);
291 WidgetInputRouterRegistry.TryFocus(menu.Header);
292
293 menu.DropDown.OpenAnimated(
295 Math.Max(0f, DropDownAnimationDurationSec));
296
297 menu.DropDown.Activate();
298 menu.DropDown.SelectFirstEnabled();
299
300 MenuOpened?.Invoke(menu);
301 }
302
304 public void CloseMenu()
305 {
306 CloseMenuInternal(immediate: false);
307 }
308
311 public void ToggleMenu(int index)
312 {
313 if (_openMenuIndex == index)
314 CloseMenu();
315 else
316 OpenMenuAt(index);
317 }
318
322 public MenuBarWidget SetMenuZOrder(int zOrder)
323 {
324 _menuZOrder = zOrder;
325 ApplyMenuZOrder(_menuZOrder);
326 return this;
327 }
328
330 protected override void ProcessShown()
331 {
332 base.ProcessShown();
333
334 _dismissLayer.SetDismissEnabled(false);
335 _openMenuIndex = -1;
336
337 foreach (MenuBarMenu menu in _menus)
338 {
339 menu.Header.SetOpen(false);
340 menu.Header.SetMenuBarActive(false);
341 menu.DropDown.CloseAnimated(MenuDropDownAnimation.None, 0f, immediate: true);
342 }
343 }
344
346 protected override void ProcessHidden()
347 {
348 CloseMenuInternal(immediate: true);
349 _dismissLayer.SetDismissEnabled(false);
350 base.ProcessHidden();
351 }
352
354 protected override void OnKeyboardInput(WidgetKeyboardEventArgs args)
355 {
356 base.OnKeyboardInput(args);
357 HandleKeyboard(args);
358 }
359
360 void IWidgetKeyboardFallback.HandleUnhandledKeyboardInput(WidgetKeyboardEventArgs args) => HandleKeyboard(args);
361
362 private void HandleKeyboard(WidgetKeyboardEventArgs args)
363 {
364 if (args.Handled || args.KeyAction != KeyAction.Pressed || !IsAvailable) return;
365 if (args.Key > 0 && _shortcuts.TryGetValue(new KeyGesture(args.Key, args.Modifiers), out var command) && command.CanInvoke)
366 {
367 args.Handled = true;
368 command.PerformClick();
369 return;
370 }
371 if (args.Modifiers == KeyboardModifierState.Alt)
372 {
373 int index = _menus.FindIndex(menu => MatchesMnemonic(menu.Header.Mnemonic, args.Key));
374 if (index >= 0)
375 {
376 args.Handled = true;
377 OpenMenuAt(index);
378 _menus[index].DropDown.SelectFirstEnabled();
379 return;
380 }
381 }
382 if (_openMenuIndex < 0 || args.Modifiers != KeyboardModifierState.None) return;
383 var dropDown = _menus[_openMenuIndex].DropDown.ActiveMenu;
384 switch (args.Key)
385 {
386 case 27:
387 if (dropDown.ParentMenu is { } parent) parent.CloseChild();
388 else CloseMenu();
389 break;
390 case 37:
391 if (dropDown.ParentMenu is { } previous) previous.CloseChild();
392 else OpenMenuAt((_openMenuIndex - 1 + _menus.Count) % _menus.Count);
393 break;
394 case 39:
395 if (dropDown.SelectedItem is { SubMenu: not null } owner)
396 dropDown.OpenSubMenu(owner, selectFirst: true);
397 else if (dropDown.ParentMenu is null)
398 OpenMenuAt((_openMenuIndex + 1) % _menus.Count);
399 break;
400 case 38: dropDown.SelectPreviousEnabled(); break;
401 case 40: dropDown.SelectNextEnabled(); break;
402 case 13:
403 case 32: dropDown.InvokeSelectedItem(); break;
404 default:
405 var item = dropDown.Items.FirstOrDefault(item => item.CanInvoke && MatchesMnemonic(item.Mnemonic, args.Key));
406 if (item is null) return;
407 item.PerformClick();
408 break;
409 }
410 args.Handled = true;
411 }
412
413 private static bool MatchesMnemonic(char? mnemonic, int key) => mnemonic is char character &&
414 key <= char.MaxValue && key >= 0 && char.ToUpperInvariant(character) == char.ToUpperInvariant((char)key);
415
417 public override void Dispose()
418 {
419 if (_disposed)
420 return;
421
422 _disposed = true;
423 CloseMenuInternal(immediate: true);
424
425 _dismissLayer.DismissRequested -= OnDismissRequested;
426
427 foreach (MenuBarMenu menu in _menus)
428 {
429 menu.Header.Invoked -= OnHeaderInvoked;
430 menu.Header.Hovered -= OnHeaderHovered;
431 }
432
433 base.Dispose();
434 MenuOpened = null;
435 MenuClosed = null;
436 ItemInvoked = null;
437 }
438
439 private void OnHeaderInvoked(MenuHeaderWidget header)
440 {
441 int index = _menus.FindIndex(menu => ReferenceEquals(menu.Header, header));
442
443 if (index >= 0)
444 ToggleMenu(index);
445 }
446
447 private void OnHeaderHovered(MenuHeaderWidget header)
448 {
449 if (_openMenuIndex < 0)
450 return;
451
452 int index = _menus.FindIndex(menu => ReferenceEquals(menu.Header, header));
453
454 if (index >= 0 && index != _openMenuIndex)
455 OpenMenuAt(index);
456 }
457
458 internal void OnItemInvoked(MenuItemWidget item)
459 {
460 CloseMenuInternal(immediate: true);
461 ItemInvoked?.Invoke(item);
462 }
463
464 private void OnDismissRequested()
465 {
466 CloseMenu();
467 }
468
469 private void CloseMenuInternal(bool immediate)
470 {
471 if (_openMenuIndex < 0 || _openMenuIndex >= _menus.Count)
472 {
473 _dismissLayer.SetDismissEnabled(false);
474 _openMenuIndex = -1;
475 return;
476 }
477
478 MenuBarMenu menu = _menus[_openMenuIndex];
479 _openMenuIndex = -1;
480
481 menu.Header.SetOpen(false);
482
483 foreach (MenuBarMenu candidate in _menus)
484 candidate.Header.SetMenuBarActive(false);
485
486 _dismissLayer.SetDismissEnabled(false);
487
488 menu.DropDown.CloseAnimated(
490 Math.Max(0f, DropDownAnimationDurationSec),
491 immediate);
492
493 var focused = WidgetInputRouterRegistry.GetFocusedWidget(this);
494 if (focused is null || ContainsWidget(this, focused))
495 WidgetInputRouterRegistry.RestoreFocus(this, _previousFocus);
496 _previousFocus = null;
497 MenuClosed?.Invoke(menu);
498 }
499
500 private static bool ContainsWidget(ContainerWidget container, WidgetBase widget) =>
501 container.ChildWidgets.Any(child => ReferenceEquals(child, widget) ||
502 child is ContainerWidget nested && ContainsWidget(nested, widget));
503
504 private void PositionDropDown(Rectangle headerBounds,
505 MenuDropDownWidget dropDown)
506 {
507 Rectangle viewport = _view.Viewport.TargetRectPx;
508 int x = Math.Clamp(
509 headerBounds.X,
510 viewport.Left,
511 Math.Max(viewport.Left, viewport.Right - dropDown.Width));
512
513 int y = Math.Clamp(_bounds.Bottom, viewport.Top, Math.Max(viewport.Top, viewport.Bottom - dropDown.Height));
514 if (Children.Contains(dropDown))
515 SetLocalOffset(dropDown, new Vector2(x - GetPosition().X, y - GetPosition().Y));
516 else
517 dropDown.SetPosition(x, y);
518 }
519
520 private int EstimateHeaderWidth(string text)
521 {
522 int estimatedTextWidth = (int)Math.Ceiling(
523 text.Length * _theme.EstimatedGlyphWidth);
524
525 return Math.Max(
526 _theme.MinimumHeaderWidth,
527 estimatedTextWidth + (_theme.HeaderHorizontalPadding * 2));
528 }
529
530 private void ApplyMenuZOrder(int zOrder)
531 {
532 _dismissLayer.SetLayerZOrder(zOrder);
533 BarBackground.ZOrder = zOrder + 1;
534
535 foreach (MenuBarMenu menu in _menus)
536 {
537 menu.Header.SetHeaderZOrder(zOrder + 2);
538 menu.DropDown.SetDropDownZOrder(zOrder + 10);
539 }
540 }
541
542 private sealed class DismissLayerWidget : WidgetBase
543 {
544 internal event Action? DismissRequested;
545
546 internal DismissLayerWidget(RenderSurfaceHostBase host,
547 View view,
548 Rectangle bounds,
549 string? nickname)
550 : base(host, DirectDrawingMode.View, bounds.Location, nickname)
551 {
552 HitArea = new DirectRectangle(
553 Color.Transparent,
554 host,
555 view,
556 bounds,
557 $"{Nickname}.hit-area")
558 .SetFilled(true)
559 .SetStrokeWidth(0f);
560
561 Add(HitArea);
562
563 CanReceiveFocus = false;
565 }
566
567 internal DirectRectangle HitArea { get; }
568
569 internal void SetDismissEnabled(bool enabled)
570 {
571 IsInputEnabled = enabled;
572 IsPointerInputEnabled = enabled;
573
574 if (enabled && !Visible)
575 Show();
576 else if (!enabled && Visible)
577 Hide();
578 }
579
580 internal void SetLayerZOrder(int zOrder)
581 {
582 HitArea.ZOrder = zOrder;
583 }
584
586 protected override void OnPointerClick(WidgetPointerEventArgs args)
587 {
588 base.OnPointerClick(args);
589
590 if (!args.IsPrimaryButton)
591 return;
592
593 args.Handled = true;
594 DismissRequested?.Invoke();
595 }
596 }
597}
bool Visible
Gets or sets the visibility of all descendants.
ReadOnlyCollection< IDirectCompositeChild > Children
Gets the children owned by the composite.
RenderSurfaceHostBase RenderSurfaceHost
Gets the render surface host shared by the composite and its children.
DirectComposite SetLocalOffset(IDirectCompositeChild child, Vector2 newLocalOffsetPx)
Changes one child's local offset and reapplies its position.
View? View
Gets the view shared by all children in view mode.
Vector2 GetPosition()
Gets the current composite anchor.
virtual DirectComposite Add(IDirectCompositeChild child, bool keepCurrentOffset=true, Vector2? explicitLocalOffsetPx=null)
Adds a composite-compatible direct drawable as a child.
Renders a configurable rectangle with support for fill, border, rounded corners, dash patterns,...
DirectRectangle SetFilled(bool isFilled)
Sets whether the rectangle interior is filled with the base color (or pattern).
DirectRectangle SetBorderColor(Color color)
Sets a distinct border (stroke) color, independent of the fill color.
DirectRectangle SetStrokeWidth(float width)
Sets the width (thickness) of the rectangle's stroke (outline) in pixels.
Represents a base class for hosting a render surface, providing functionality for managing rendering ...
Viewport Viewport
Gets the viewport that defines the screen-space rectangle, zoom level, and other rendering parameters...
Definition View.cs:44
Rectangle TargetRectPx
Screen-space rectangle (in SCREEN / RenderSurface pixels) where this view is drawn....
Definition Viewport.cs:35
override DirectComposite Add(IDirectCompositeChild child, bool keepCurrentOffset=true, Vector2? explicitLocalOffsetPx=null)
Adds a composite-compatible direct drawable as a child.The current composite.
ContainerWidget(RenderSurfaceHostBase renderSurfaceHost, DirectDrawingMode mode, PointF anchor=default, string? nickname=null)
Initializes a new instance of the ContainerWidget class.
Associates one top-level menu header with its dropdown.
Definition MenuBarMenu.cs:7
MenuDropDownWidget DropDown
Gets the dropdown owned by the header.
MenuHeaderWidget Header
Gets the top-level menu header.
void ToggleMenu(int index)
Toggles the specified top-level menu.
MenuDropDownAnimation DropDownAnimation
Gets or sets the dropdown animation.
MenuBarWidget SetMenuZOrder(int zOrder)
Applies a drawing Z-order to the menu-bar family.
Action< MenuBarMenu >? MenuOpened
Occurs after a top-level menu opens.
Action< MenuBarMenu >? MenuClosed
Occurs after a top-level menu begins closing.
MenuBarWidget(RenderSurfaceHostBase host, View view, Rectangle bounds, MenuBarTheme? theme=null, string? nickname=null)
Creates a menu bar attached to a view.
MenuBarMenu? OpenMenu
Gets the currently open menu, or null.
MenuItemWidget GetItem(string key)
Gets an item by its stable, case-sensitive key, including nested descendants.
MenuBarWidget AddMenu(string text, Action< MenuDropDownWidget >? configure=null, int? headerWidth=null, char? mnemonic=null)
Adds a top-level menu and optionally configures its dropdown.
void OpenMenuAt(int index)
Opens the specified top-level menu.
DirectRectangle BarBackground
Gets the menu bar background drawing.
IReadOnlyList< MenuBarMenu > Menus
Gets the configured top-level menus.
override void ProcessShown()
Performs processing when the widget is shown before notification callbacks are invoked.
float DropDownAnimationDurationSec
Gets or sets the dropdown animation duration, in seconds.
Action< MenuItemWidget >? ItemInvoked
Occurs when a command item is invoked.
void CloseMenu()
Closes the open menu, if any.
override void Dispose()
Disposes the composite and every child it owns.
int OpenMenuIndex
Gets the zero-based open-menu index, or -1.
override void ProcessHidden()
Performs processing when the widget is hidden before notification callbacks are invoked.
override void OnKeyboardInput(WidgetKeyboardEventArgs args)
Called when keyboard input is routed to the widget.
bool TryGetItem(string key, [System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out MenuItemWidget? item)
Looks up a stable key; returns false and null when absent.
Represents a popup command list owned by a header or submenu item.
Represents a top-level menu header such as File, Edit, or Help.
char? Mnemonic
Gets the explicit, case-insensitive Alt access character.
A command, check/radio item, or submenu owner in a menu dropdown.
KeyGesture? Shortcut
Gets the executable gesture, if any.
string? Key
Gets the immutable optional key, unique within the menu bar.
WidgetBase Show()
Shows the widget and registers it for input routing.
bool CanReceiveFocus
Gets or sets a value indicating whether the widget can receive keyboard focus.
bool IsPointerInputEnabled
Gets or sets a value indicating whether the widget can receive pointer input.
WidgetBase(RenderSurfaceHostBase renderSurfaceHost, DirectDrawingMode mode, PointF anchor=default, string? nickname=null)
Initializes a new instance of the WidgetBase class.
Definition WidgetBase.cs:95
WidgetBase Activate()
Activates the widget and brings it to the front of the input order.
WidgetBase Hide()
Hides the widget and releases any routed input state.
bool IsKeyboardInputEnabled
Gets or sets a value indicating whether the widget can receive keyboard input.
bool IsInputEnabled
Gets or sets a value indicating whether the widget can participate in input routing.
bool Handled
Gets or sets whether this widget interaction was handled.
Provides data for widget keyboard input events routed by WidgetInputRouter.
KeyboardModifierState Modifiers
Gets the keyboard modifier state at the time of the event.
int Key
Gets the integer key identifier.
KeyAction KeyAction
Gets the action associated with this key event.
@ Rectangle
Particles are spawned uniformly within a rectangular area. This is the default distribution.
DirectDrawingMode
Specifies where direct drawing operations are applied within the rendering pipeline.
KeyboardModifierState
Represents the state of keyboard modifier keys (Shift, Ctrl, Alt) that can be pressed simultaneously ...
KeyAction
Represents the type of action performed on a keyboard key, indicating whether the key was pressed,...
Definition KeyAction.cs:10
MenuDropDownAnimation
Specifies how a menu dropdown is animated when opened or closed.
record MenuBarTheme
Defines the dimensions and colors used by menu bar widgets.
A host key code and an exact combination of keyboard modifiers.
Definition KeyGesture.cs:9