A game framework written with osu! in mind.
at master 814 lines 28 kB view raw
1// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. 2// See the LICENCE file in the repository root for full licence text. 3 4using System; 5using System.Collections.Generic; 6using System.Linq; 7using osu.Framework.Extensions.EnumExtensions; 8using osu.Framework.Extensions.IEnumerableExtensions; 9using osuTK.Graphics; 10using osu.Framework.Graphics.Containers; 11using osu.Framework.Graphics.Shapes; 12using osu.Framework.Graphics.Sprites; 13using osu.Framework.Input.Events; 14using osu.Framework.Layout; 15using osu.Framework.Utils; 16using osu.Framework.Threading; 17using osuTK; 18using osuTK.Input; 19 20namespace osu.Framework.Graphics.UserInterface 21{ 22 public abstract class Menu : CompositeDrawable, IStateful<MenuState> 23 { 24 /// <summary> 25 /// Invoked when this <see cref="Menu"/>'s <see cref="State"/> changes. 26 /// </summary> 27 public event Action<MenuState> StateChanged; 28 29 /// <summary> 30 /// Gets or sets the delay before opening sub-<see cref="Menu"/>s when menu items are hovered. 31 /// </summary> 32 protected double HoverOpenDelay = 100; 33 34 /// <summary> 35 /// Whether this menu is always displayed in an open state (ie. a menu bar). 36 /// Clicks are required to activate <see cref="DrawableMenuItem"/>. 37 /// </summary> 38 protected readonly bool TopLevelMenu; 39 40 /// <summary> 41 /// The <see cref="Container{T}"/> that contains the content of this <see cref="Menu"/>. 42 /// </summary> 43 protected readonly ScrollContainer<Drawable> ContentContainer; 44 45 /// <summary> 46 /// The <see cref="Container{T}"/> that contains the items of this <see cref="Menu"/>. 47 /// </summary> 48 protected FillFlowContainer<DrawableMenuItem> ItemsContainer; 49 50 /// <summary> 51 /// The container that provides the masking effects for this <see cref="Menu"/>. 52 /// </summary> 53 protected readonly Container MaskingContainer; 54 55 /// <summary> 56 /// Gets the item representations contained by this <see cref="Menu"/>. 57 /// </summary> 58 protected internal IReadOnlyList<DrawableMenuItem> Children => ItemsContainer.Children; 59 60 protected readonly Direction Direction; 61 62 private Menu parentMenu; 63 private Menu submenu; 64 65 private readonly Box background; 66 67 private readonly LayoutValue sizeCache = new LayoutValue(Invalidation.RequiredParentSizeToFit, InvalidationSource.Child); 68 69 private readonly Container<Menu> submenuContainer; 70 71 /// <summary> 72 /// Constructs a menu. 73 /// </summary> 74 /// <param name="direction">The direction of layout for this menu.</param> 75 /// <param name="topLevelMenu">Whether the resultant menu is always displayed in an open state (ie. a menu bar).</param> 76 protected Menu(Direction direction, bool topLevelMenu = false) 77 { 78 Direction = direction; 79 TopLevelMenu = topLevelMenu; 80 81 if (topLevelMenu) 82 state = MenuState.Open; 83 84 InternalChildren = new Drawable[] 85 { 86 MaskingContainer = new Container 87 { 88 Name = "Our contents", 89 RelativeSizeAxes = Axes.Both, 90 Masking = true, 91 Children = new Drawable[] 92 { 93 background = new Box 94 { 95 RelativeSizeAxes = Axes.Both, 96 Colour = Color4.Black 97 }, 98 ContentContainer = CreateScrollContainer(direction).With(d => 99 { 100 d.RelativeSizeAxes = Axes.Both; 101 d.Masking = false; 102 d.Child = ItemsContainer = new FillFlowContainer<DrawableMenuItem> { Direction = direction == Direction.Horizontal ? FillDirection.Horizontal : FillDirection.Vertical }; 103 }) 104 } 105 }, 106 submenuContainer = new Container<Menu> 107 { 108 Name = "Sub menu container", 109 AutoSizeAxes = Axes.Both 110 } 111 }; 112 113 switch (direction) 114 { 115 case Direction.Horizontal: 116 ItemsContainer.AutoSizeAxes = Axes.X; 117 break; 118 119 case Direction.Vertical: 120 ItemsContainer.AutoSizeAxes = Axes.Y; 121 break; 122 } 123 124 // The menu will provide a valid size for the items container based on our own size 125 ItemsContainer.RelativeSizeAxes = Axes.Both & ~ItemsContainer.AutoSizeAxes; 126 127 AddLayout(sizeCache); 128 } 129 130 protected override void LoadComplete() 131 { 132 base.LoadComplete(); 133 updateState(); 134 } 135 136 /// <summary> 137 /// Gets or sets the <see cref="MenuItem"/>s contained within this <see cref="Menu"/>. 138 /// </summary> 139 public IReadOnlyList<MenuItem> Items 140 { 141 get => ItemsContainer.Select(r => r.Item).ToList(); 142 set 143 { 144 Clear(); 145 value?.ForEach(Add); 146 } 147 } 148 149 /// <summary> 150 /// Gets or sets the background colour of this <see cref="Menu"/>. 151 /// </summary> 152 public Color4 BackgroundColour 153 { 154 get => background.Colour; 155 set => background.Colour = value; 156 } 157 158 /// <summary> 159 /// Gets or sets whether the scroll bar of this <see cref="Menu"/> should be visible. 160 /// </summary> 161 public bool ScrollbarVisible 162 { 163 get => ContentContainer.ScrollbarVisible; 164 set => ContentContainer.ScrollbarVisible = value; 165 } 166 167 private float maxWidth = float.MaxValue; 168 169 /// <summary> 170 /// Gets or sets the maximum allowable width by this <see cref="Menu"/>. 171 /// </summary> 172 public float MaxWidth 173 { 174 get => maxWidth; 175 set 176 { 177 if (Precision.AlmostEquals(maxWidth, value)) 178 return; 179 180 maxWidth = value; 181 182 sizeCache.Invalidate(); 183 } 184 } 185 186 private float maxHeight = float.PositiveInfinity; 187 188 /// <summary> 189 /// Gets or sets the maximum allowable height by this <see cref="Menu"/>. 190 /// </summary> 191 public float MaxHeight 192 { 193 get => maxHeight; 194 set 195 { 196 if (Precision.AlmostEquals(maxHeight, value)) 197 return; 198 199 maxHeight = value; 200 201 sizeCache.Invalidate(); 202 } 203 } 204 205 private MenuState state = MenuState.Closed; 206 207 /// <summary> 208 /// Gets or sets the current state of this <see cref="Menu"/>. 209 /// </summary> 210 public virtual MenuState State 211 { 212 get => state; 213 set 214 { 215 if (TopLevelMenu) 216 { 217 submenu?.Close(); 218 return; 219 } 220 221 if (state == value) 222 return; 223 224 state = value; 225 226 updateState(); 227 StateChanged?.Invoke(State); 228 } 229 } 230 231 private void updateState() 232 { 233 if (!IsLoaded) 234 return; 235 236 resetState(); 237 238 switch (State) 239 { 240 case MenuState.Closed: 241 AnimateClose(); 242 243 if (HasFocus) 244 GetContainingInputManager()?.ChangeFocus(parentMenu); 245 break; 246 247 case MenuState.Open: 248 AnimateOpen(); 249 250 // We may not be present at this point, so must run on the next frame. 251 if (!TopLevelMenu) 252 { 253 Schedule(delegate 254 { 255 if (State == MenuState.Open) GetContainingInputManager().ChangeFocus(this); 256 }); 257 } 258 259 break; 260 } 261 } 262 263 private void resetState() 264 { 265 if (!IsLoaded) 266 return; 267 268 submenu?.Close(); 269 sizeCache.Invalidate(); 270 } 271 272 /// <summary> 273 /// Adds a <see cref="MenuItem"/> to this <see cref="Menu"/>. 274 /// </summary> 275 /// <param name="item">The <see cref="MenuItem"/> to add.</param> 276 public virtual void Add(MenuItem item) 277 { 278 var drawableItem = CreateDrawableMenuItem(item); 279 drawableItem.Clicked = menuItemClicked; 280 drawableItem.Hovered = menuItemHovered; 281 drawableItem.StateChanged += s => itemStateChanged(drawableItem, s); 282 283 drawableItem.SetFlowDirection(Direction); 284 285 ItemsContainer.Add(drawableItem); 286 sizeCache.Invalidate(); 287 } 288 289 private void itemStateChanged(DrawableMenuItem item, MenuItemState state) 290 { 291 if (state != MenuItemState.Selected) return; 292 293 if (item != selectedItem && selectedItem != null) 294 selectedItem.State = MenuItemState.NotSelected; 295 selectedItem = item; 296 } 297 298 /// <summary> 299 /// Removes a <see cref="MenuItem"/> from this <see cref="Menu"/>. 300 /// </summary> 301 /// <param name="item">The <see cref="MenuItem"/> to remove.</param> 302 /// <returns>Whether <paramref name="item"/> was successfully removed.</returns> 303 public bool Remove(MenuItem item) 304 { 305 bool result = ItemsContainer.RemoveAll(d => d.Item == item) > 0; 306 sizeCache.Invalidate(); 307 308 return result; 309 } 310 311 /// <summary> 312 /// Clears all <see cref="MenuItem"/>s in this <see cref="Menu"/>. 313 /// </summary> 314 public void Clear() 315 { 316 ItemsContainer.Clear(); 317 resetState(); 318 } 319 320 /// <summary> 321 /// Opens this <see cref="Menu"/>. 322 /// </summary> 323 public void Open() => State = MenuState.Open; 324 325 /// <summary> 326 /// Closes this <see cref="Menu"/>. 327 /// </summary> 328 public void Close() => State = MenuState.Closed; 329 330 /// <summary> 331 /// Toggles the state of this <see cref="Menu"/>. 332 /// </summary> 333 public void Toggle() => State = State == MenuState.Closed ? MenuState.Open : MenuState.Closed; 334 335 /// <summary> 336 /// Animates the opening of this <see cref="Menu"/>. 337 /// </summary> 338 protected virtual void AnimateOpen() => Show(); 339 340 /// <summary> 341 /// Animates the closing of this <see cref="Menu"/>. 342 /// </summary> 343 protected virtual void AnimateClose() => Hide(); 344 345 protected override void UpdateAfterChildren() 346 { 347 base.UpdateAfterChildren(); 348 349 if (!sizeCache.IsValid) 350 { 351 // Our children will be relatively-sized on the axis separate to the menu direction, so we need to compute 352 // that size ourselves, based on the content size of our children, to give them a valid relative size 353 354 float width = 0; 355 float height = 0; 356 357 foreach (var item in Children) 358 { 359 width = Math.Max(width, item.ContentDrawWidth); 360 height = Math.Max(height, item.ContentDrawHeight); 361 } 362 363 // When scrolling in one direction, ItemsContainer is auto-sized in that direction and relative-sized in the other 364 // In the case of the auto-sized direction, we want to use its size. In the case of the relative-sized direction, we want 365 // to use the (above) computed size. 366 width = Direction == Direction.Horizontal ? ItemsContainer.Width : width; 367 height = Direction == Direction.Vertical ? ItemsContainer.Height : height; 368 369 width = Math.Min(MaxWidth, width); 370 height = Math.Min(MaxHeight, height); 371 372 // Regardless of the above result, if we are relative-sizing, just use the stored width/height 373 width = RelativeSizeAxes.HasFlagFast(Axes.X) ? Width : width; 374 height = RelativeSizeAxes.HasFlagFast(Axes.Y) ? Height : height; 375 376 if (State == MenuState.Closed && Direction == Direction.Horizontal) 377 width = 0; 378 if (State == MenuState.Closed && Direction == Direction.Vertical) 379 height = 0; 380 381 UpdateSize(new Vector2(width, height)); 382 383 sizeCache.Validate(); 384 } 385 } 386 387 /// <summary> 388 /// Resizes this <see cref="Menu"/>. 389 /// </summary> 390 /// <param name="newSize">The new size.</param> 391 protected virtual void UpdateSize(Vector2 newSize) => Size = newSize; 392 393 #region Hover/Focus logic 394 395 private void menuItemClicked(DrawableMenuItem item) 396 { 397 // We only want to close the sub-menu if we're not a sub menu - if we are a sub menu 398 // then clicks should instead cause the sub menus to instantly show up 399 if (TopLevelMenu && submenu?.State == MenuState.Open) 400 { 401 submenu.Close(); 402 return; 403 } 404 405 // Check if there is a sub menu to display 406 if (item.Item.Items?.Count == 0) 407 { 408 // This item must have attempted to invoke an action - close all menus if item allows 409 if (item.CloseMenuOnClick) 410 closeAll(); 411 412 return; 413 } 414 415 openDelegate?.Cancel(); 416 417 openSubmenuFor(item); 418 } 419 420 private DrawableMenuItem selectedItem; 421 422 /// <summary> 423 /// The item which triggered opening us as a submenu. 424 /// </summary> 425 private MenuItem triggeringItem; 426 427 private void openSubmenuFor(DrawableMenuItem item) 428 { 429 item.State = MenuItemState.Selected; 430 431 if (submenu == null) 432 { 433 submenuContainer.Add(submenu = CreateSubMenu()); 434 submenu.parentMenu = this; 435 submenu.StateChanged += submenuStateChanged; 436 } 437 438 submenu.triggeringItem = item.Item; 439 440 submenu.Items = item.Item.Items; 441 submenu.Position = item.ToSpaceOfOtherDrawable(new Vector2( 442 Direction == Direction.Vertical ? item.DrawWidth : 0, 443 Direction == Direction.Horizontal ? item.DrawHeight : 0), this); 444 445 if (item.Item.Items.Count > 0) 446 { 447 if (submenu.State == MenuState.Open) 448 Schedule(delegate { GetContainingInputManager().ChangeFocus(submenu); }); 449 else 450 submenu.Open(); 451 } 452 else 453 submenu.Close(); 454 } 455 456 private void submenuStateChanged(MenuState state) 457 { 458 switch (state) 459 { 460 case MenuState.Closed: 461 selectedItem.State = MenuItemState.NotSelected; 462 break; 463 464 case MenuState.Open: 465 selectedItem.State = MenuItemState.Selected; 466 break; 467 } 468 } 469 470 private ScheduledDelegate openDelegate; 471 472 private void menuItemHovered(DrawableMenuItem item) 473 { 474 // If we're not a sub-menu, then hover shouldn't display a sub-menu unless an item is clicked 475 if (TopLevelMenu && submenu?.State != MenuState.Open) 476 return; 477 478 openDelegate?.Cancel(); 479 480 if (TopLevelMenu || HoverOpenDelay == 0) 481 openSubmenuFor(item); 482 else 483 { 484 openDelegate = Scheduler.AddDelayed(() => 485 { 486 if (item.IsHovered) 487 openSubmenuFor(item); 488 }, HoverOpenDelay); 489 } 490 } 491 492 public override bool HandleNonPositionalInput => State == MenuState.Open; 493 494 protected override bool OnKeyDown(KeyDownEvent e) 495 { 496 if (e.Key == Key.Escape && !TopLevelMenu) 497 { 498 Close(); 499 return true; 500 } 501 502 return base.OnKeyDown(e); 503 } 504 505 protected override bool OnClick(ClickEvent e) => true; 506 protected override bool OnHover(HoverEvent e) => true; 507 508 public override bool AcceptsFocus => !TopLevelMenu; 509 510 public override bool RequestsFocus => !TopLevelMenu && State == MenuState.Open; 511 512 protected override void OnFocusLost(FocusLostEvent e) 513 { 514 // Case where a sub-menu was opened the focus will be transferred to that sub-menu while this menu will receive OnFocusLost 515 if (submenu?.State == MenuState.Open) 516 return; 517 518 if (!TopLevelMenu) 519 // At this point we should have lost focus due to clicks outside the menu structure 520 closeAll(); 521 } 522 523 /// <summary> 524 /// Closes all open <see cref="Menu"/>s. 525 /// </summary> 526 private void closeAll() 527 { 528 Close(); 529 parentMenu?.closeFromChild(triggeringItem); 530 } 531 532 private void closeFromChild(MenuItem source) 533 { 534 if (IsHovered || (parentMenu?.IsHovered ?? false)) return; 535 536 if (triggeringItem?.Items?.Contains(source) ?? triggeringItem == null) 537 { 538 Close(); 539 parentMenu?.closeFromChild(triggeringItem); 540 } 541 } 542 543 #endregion 544 545 /// <summary> 546 /// Creates a sub-menu for <see cref="MenuItem.Items"/> of <see cref="MenuItem"/>s added to this <see cref="Menu"/>. 547 /// </summary> 548 protected abstract Menu CreateSubMenu(); 549 550 /// <summary> 551 /// Creates the visual representation for a <see cref="MenuItem"/>. 552 /// </summary> 553 /// <param name="item">The <see cref="MenuItem"/> that is to be visualised.</param> 554 /// <returns>The visual representation.</returns> 555 protected abstract DrawableMenuItem CreateDrawableMenuItem(MenuItem item); 556 557 /// <summary> 558 /// Creates the <see cref="ScrollContainer{T}"/> to hold the items of this <see cref="Menu"/>. 559 /// </summary> 560 /// <param name="direction">The scrolling direction.</param> 561 /// <returns>The <see cref="ScrollContainer{T}"/>.</returns> 562 protected abstract ScrollContainer<Drawable> CreateScrollContainer(Direction direction); 563 564 #region DrawableMenuItem 565 566 // must be public due to mono bug(?) https://github.com/ppy/osu/issues/1204 567 public abstract class DrawableMenuItem : CompositeDrawable, IStateful<MenuItemState> 568 { 569 /// <summary> 570 /// Invoked when this <see cref="DrawableMenuItem"/>'s <see cref="State"/> changes. 571 /// </summary> 572 public event Action<MenuItemState> StateChanged; 573 574 /// <summary> 575 /// Invoked when this <see cref="DrawableMenuItem"/> is clicked. This occurs regardless of whether or not <see cref="MenuItem.Action"/> was 576 /// invoked or not, or whether <see cref="Item"/> contains any sub-<see cref="MenuItem"/>s. 577 /// </summary> 578 internal Action<DrawableMenuItem> Clicked; 579 580 /// <summary> 581 /// Invoked when this <see cref="DrawableMenuItem"/> is hovered. This runs one update frame behind the actual hover event. 582 /// </summary> 583 internal Action<DrawableMenuItem> Hovered; 584 585 /// <summary> 586 /// The <see cref="MenuItem"/> which this <see cref="DrawableMenuItem"/> represents. 587 /// </summary> 588 public readonly MenuItem Item; 589 590 /// <summary> 591 /// The content of this <see cref="DrawableMenuItem"/>, created through <see cref="CreateContent"/>. 592 /// </summary> 593 protected readonly Drawable Content; 594 595 /// <summary> 596 /// The background of this <see cref="DrawableMenuItem"/>. 597 /// </summary> 598 protected readonly Drawable Background; 599 600 /// <summary> 601 /// The foreground of this <see cref="DrawableMenuItem"/>. This contains the content of this <see cref="DrawableMenuItem"/>. 602 /// </summary> 603 protected readonly Container Foreground; 604 605 /// <summary> 606 /// Whether to close all menus when this action <see cref="DrawableMenuItem"/> is clicked. 607 /// </summary> 608 public virtual bool CloseMenuOnClick => true; 609 610 protected DrawableMenuItem(MenuItem item) 611 { 612 Item = item; 613 614 InternalChildren = new[] 615 { 616 Background = CreateBackground(), 617 Foreground = new Container 618 { 619 AutoSizeAxes = Axes.Both, 620 Child = Content = CreateContent() 621 }, 622 }; 623 624 if (Content is IHasText textContent) 625 { 626 textContent.Text = item.Text.Value; 627 Item.Text.ValueChanged += e => textContent.Text = e.NewValue; 628 } 629 } 630 631 /// <summary> 632 /// Sets various properties of this <see cref="DrawableMenuItem"/> that depend on the direction in which 633 /// <see cref="DrawableMenuItem"/>s flow inside the containing <see cref="Menu"/> (e.g. sizing axes). 634 /// </summary> 635 /// <param name="direction">The direction in which <see cref="DrawableMenuItem"/>s will be flowed.</param> 636 public virtual void SetFlowDirection(Direction direction) 637 { 638 RelativeSizeAxes = direction == Direction.Horizontal ? Axes.Y : Axes.X; 639 AutoSizeAxes = direction == Direction.Horizontal ? Axes.X : Axes.Y; 640 } 641 642 private Color4 backgroundColour = Color4.DarkSlateGray; 643 644 /// <summary> 645 /// Gets or sets the default background colour. 646 /// </summary> 647 public Color4 BackgroundColour 648 { 649 get => backgroundColour; 650 set 651 { 652 backgroundColour = value; 653 UpdateBackgroundColour(); 654 } 655 } 656 657 private Color4 foregroundColour = Color4.White; 658 659 /// <summary> 660 /// Gets or sets the default foreground colour. 661 /// </summary> 662 public Color4 ForegroundColour 663 { 664 get => foregroundColour; 665 set 666 { 667 foregroundColour = value; 668 UpdateForegroundColour(); 669 } 670 } 671 672 private Color4 backgroundColourHover = Color4.DarkGray; 673 674 /// <summary> 675 /// Gets or sets the background colour when this <see cref="DrawableMenuItem"/> is hovered. 676 /// </summary> 677 public Color4 BackgroundColourHover 678 { 679 get => backgroundColourHover; 680 set 681 { 682 backgroundColourHover = value; 683 UpdateBackgroundColour(); 684 } 685 } 686 687 private Color4 foregroundColourHover = Color4.White; 688 689 /// <summary> 690 /// Gets or sets the foreground colour when this <see cref="DrawableMenuItem"/> is hovered. 691 /// </summary> 692 public Color4 ForegroundColourHover 693 { 694 get => foregroundColourHover; 695 set 696 { 697 foregroundColourHover = value; 698 UpdateForegroundColour(); 699 } 700 } 701 702 private MenuItemState state; 703 704 public MenuItemState State 705 { 706 get => state; 707 set 708 { 709 state = value; 710 711 UpdateForegroundColour(); 712 UpdateBackgroundColour(); 713 714 StateChanged?.Invoke(state); 715 } 716 } 717 718 /// <summary> 719 /// The draw width of the text of this <see cref="DrawableMenuItem"/>. 720 /// </summary> 721 public float ContentDrawWidth => Content.DrawWidth; 722 723 /// <summary> 724 /// The draw width of the text of this <see cref="DrawableMenuItem"/>. 725 /// </summary> 726 public float ContentDrawHeight => Content.DrawHeight; 727 728 /// <summary> 729 /// Called after the <see cref="BackgroundColour"/> is modified or the hover state changes. 730 /// </summary> 731 protected virtual void UpdateBackgroundColour() 732 { 733 Background.FadeColour(IsHovered ? BackgroundColourHover : BackgroundColour); 734 } 735 736 /// <summary> 737 /// Called after the <see cref="ForegroundColour"/> is modified or the hover state changes. 738 /// </summary> 739 protected virtual void UpdateForegroundColour() 740 { 741 Foreground.FadeColour(IsHovered ? ForegroundColourHover : ForegroundColour); 742 } 743 744 protected override void LoadComplete() 745 { 746 base.LoadComplete(); 747 Background.Colour = BackgroundColour; 748 Foreground.Colour = ForegroundColour; 749 } 750 751 protected override bool OnHover(HoverEvent e) 752 { 753 UpdateBackgroundColour(); 754 UpdateForegroundColour(); 755 756 Schedule(() => 757 { 758 if (IsHovered) 759 Hovered?.Invoke(this); 760 }); 761 762 return false; 763 } 764 765 protected override void OnHoverLost(HoverLostEvent e) 766 { 767 UpdateBackgroundColour(); 768 UpdateForegroundColour(); 769 base.OnHoverLost(e); 770 } 771 772 private bool hasSubmenu => Item.Items?.Count > 0; 773 774 protected override bool OnClick(ClickEvent e) 775 { 776 if (Item.Action.Disabled) 777 return true; 778 779 if (!hasSubmenu) 780 Item.Action.Value?.Invoke(); 781 782 Clicked?.Invoke(this); 783 784 return true; 785 } 786 787 /// <summary> 788 /// Creates the background of this <see cref="DrawableMenuItem"/>. 789 /// </summary> 790 protected virtual Drawable CreateBackground() => new Box { RelativeSizeAxes = Axes.Both }; 791 792 /// <summary> 793 /// Creates the content which will be displayed in this <see cref="DrawableMenuItem"/>. 794 /// If the <see cref="Drawable"/> returned implements <see cref="IHasText"/>, the text will be automatically 795 /// updated when the <see cref="MenuItem.Text"/> is updated. 796 /// </summary> 797 protected abstract Drawable CreateContent(); 798 } 799 800 #endregion 801 } 802 803 public enum MenuState 804 { 805 Closed, 806 Open 807 } 808 809 public enum MenuItemState 810 { 811 NotSelected, 812 Selected 813 } 814}