Skip to content

Instantly share code, notes, and snippets.

@krypt-lynx
Last active February 10, 2021 19:49
Show Gist options
  • Save krypt-lynx/08d4b4f87e6a2d74437637f2953547b2 to your computer and use it in GitHub Desktop.
Save krypt-lynx/08d4b4f87e6a2d74437637f2953547b2 to your computer and use it in GitHub Desktop.
Cascading menus implementation for RimWorld game
using RimWorld.Planet;
using RWLayout.alpha2;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using UnityEngine;
using Verse;
namespace RWLayoutTests
{
class WindowMenuTest : CWindow
{
public WindowMenuTest()
{
InnerSize = new Vector2(400, 300);
}
public override void ConstructGui()
{
MakeResizable();
draggable = true;
doCloseX = true;
var btn = Gui.AddElement(new CButtonText
{
Title = "test",
Action = (_) =>
{
/*Find.WindowStack.Add(
new WindowAttached(new Rect(this.windowRect.xMax, this.windowRect.yMin, 100, this.windowRect.height))
);*/
Find.WindowStack.Add(
new FloatMenu(CreateMenuOptions()
));
}
});
CButtonText btn2 = null;
btn2 = Gui.AddElement(new CButtonText
{
Title = "test2",
Action = (_) =>
{
Find.WindowStack.Add(
new FixedFloatMenu(this.TranslateToScreenCoordiates(btn2.Bounds),
CreateMenuOptions()
));
}
});
Gui.AddConstraints(btn.left ^ Gui.left, btn.top ^ Gui.top, btn.width ^ 150, btn.height ^ 30);
Gui.AddConstraints(btn2.left ^ Gui.left, btn2.top ^ btn.bottom, btn2.width ^ 150, btn2.height ^ 30);
}
private static List<FloatMenuOption> CreateMenuOptions()
{
return new List<FloatMenuOption> {
new FloatMenuOption("option 1", () => { }),
new FloatMenuOption("option 2", () => { }),
new FloatMenuOption("option 3", () => { }),
new FloatSubmenuOption("option 4", () => new List<FloatMenuOption> {
new FloatSubmenuOption("suboption 1", () => new List<FloatMenuOption> {
new FloatMenuOption("subsuboption 1", () => { }),
}),
new FloatMenuOption("suboption 2", () => { }),
}),
new FloatSubmenuOption("option 5", () => new List<FloatMenuOption> {
new FloatMenuOption("suboption 3", () => { }),
new FloatMenuOption("suboption 4", () => { }),
new FloatMenuOption("suboption 5", () => { }),
}),
new FloatMenuOption("option 6", () => { }),
};
}
public override void PostOpen()
{
$"WindowLocationTest windowRect: {this.windowRect}; InnerSize: {this.InnerSize}".Log();
base.PostOpen();
}
}
/// <summary>
/// Submenu option for FloatMenu
/// </summary>
[StaticConstructorOnStartup]
class FloatSubmenuOption : FloatMenuOption
{
const float indicatorWidth = 10;
static readonly Texture2D TexSubmenuIndicator = ContentFinder<Texture2D>.Get("UI/Buttons/Dev/Reveal", true);
Func<List<FloatMenuOption>> optionsGenerator = null;
bool closed = false;
/// <summary>
///
/// </summary>
/// <param name="label"></param>
/// <param name="optionsGenerator">submenu options generator</param>
/// <param name="priority"></param>
/// <param name="mouseoverGuiAction"></param>
/// <param name="revalidateClickTarget"></param>
/// <param name="extraPartWidth"></param>
/// <param name="extraPartOnGUI"></param>
/// <param name="revalidateWorldClickTarget"></param>
public FloatSubmenuOption(string label, Func<List<FloatMenuOption>> optionsGenerator, MenuOptionPriority priority = MenuOptionPriority.Default,
Action mouseoverGuiAction = null, Thing revalidateClickTarget = null, float extraPartWidth = 0,
Func<Rect, bool> extraPartOnGUI = null, WorldObject revalidateWorldClickTarget = null) :
base(label, null, priority, mouseoverGuiAction, revalidateClickTarget, extraPartWidth + indicatorWidth, extraPartOnGUI, revalidateWorldClickTarget)
{
this.action = OptionSelected;
this.optionsGenerator = optionsGenerator;
}
/// <summary>
///
/// </summary>
/// <param name="label"></param>
/// <param name="optionsGenerator">submenu options generator</param>
/// <param name="shownItemForIcon"></param>
/// <param name="priority"></param>
/// <param name="mouseoverGuiAction"></param>
/// <param name="revalidateClickTarget"></param>
/// <param name="extraPartWidth"></param>
/// <param name="extraPartOnGUI"></param>
/// <param name="revalidateWorldClickTarget"></param>
public FloatSubmenuOption(string label, Func<List<FloatMenuOption>> optionsGenerator, ThingDef shownItemForIcon, MenuOptionPriority priority = MenuOptionPriority.Default,
Action mouseoverGuiAction = null, Thing revalidateClickTarget = null, float extraPartWidth = 0,
Func<Rect, bool> extraPartOnGUI = null, WorldObject revalidateWorldClickTarget = null) :
base(label, null, shownItemForIcon, priority, mouseoverGuiAction, revalidateClickTarget, extraPartWidth + indicatorWidth, extraPartOnGUI, revalidateWorldClickTarget)
{
this.action = OptionSelected;
this.optionsGenerator = optionsGenerator;
}
/// <summary>
///
/// </summary>
/// <param name="label"></param>
/// <param name="optionsGenerator">submenu options generator</param>
/// <param name="itemIcon"></param>
/// <param name="iconColor"></param>
/// <param name="priority"></param>
/// <param name="mouseoverGuiAction"></param>
/// <param name="revalidateClickTarget"></param>
/// <param name="extraPartWidth"></param>
/// <param name="extraPartOnGUI"></param>
/// <param name="revalidateWorldClickTarget"></param>
public FloatSubmenuOption(string label, Func<List<FloatMenuOption>> optionsGenerator, Texture2D itemIcon, Color iconColor, MenuOptionPriority priority = MenuOptionPriority.Default,
Action mouseoverGuiAction = null, Thing revalidateClickTarget = null, float extraPartWidth = 0,
Func<Rect, bool> extraPartOnGUI = null, WorldObject revalidateWorldClickTarget = null) :
base(label, null, itemIcon, iconColor, priority, mouseoverGuiAction, revalidateClickTarget, extraPartWidth + indicatorWidth, extraPartOnGUI, revalidateWorldClickTarget)
{
this.action = OptionSelected;
this.optionsGenerator = optionsGenerator;
}
//static Func<FloatMenu, List<FloatMenuOption>> get_FloatMenu_options = RWLayout.alpha2.FastAccess.Dynamic.InstanceGetField<FloatMenu, List<FloatMenuOption>>("options");
static Func<FloatMenu, List<FloatMenuOption>> get_FloatMenu_options = (x) => (List<FloatMenuOption>)typeof(FloatMenu).GetField("options", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(x);
/// <summary>
/// Show submenu on selection
/// </summary>
void OptionSelected()
{
if (submenu == null)
{
// Close all other opened submenus
if (OwnerMenu != null)
{
CloseAllSubmenus(OwnerMenu);
}
// create and show submenu
submenu = new FixedFloatMenu(GUIUtility.GUIToScreenRect(optionRect), optionsGenerator());
submenu.onCloseCallback = () =>
{
closed = true;
};
Find.WindowStack.Add(submenu);
}
}
private void CloseAllSubmenus(FloatMenu menu)
{
foreach (var menuOption in get_FloatMenu_options(menu) ?? Enumerable.Empty<FloatMenuOption>())
{
if (menuOption is FloatSubmenuOption othersubmenu)
{
othersubmenu.CloseSubmenu();
}
}
}
public void CloseSubmenu()
{
if (submenu != null) {
submenu.onCloseCallback = null; // removing callback to prevent menu tree collapse
CloseAllSubmenus(submenu);
submenu.Close();
submenu = null;
}
}
Rect optionRect;
bool needCaptureOwnerMenu = true;
FixedFloatMenu submenu = null;
private Verse.WeakReference<FloatMenu> ownerMenu = null;
public FloatMenu OwnerMenu
{
get => ownerMenu?.Target;
set => ownerMenu = new Verse.WeakReference<FloatMenu>(value);
}
public bool DoSubmenuIndicator { get; set; } = true;
public override bool DoGUI(Rect rect, bool colonistOrdering, FloatMenu floatMenu)
{
if (needCaptureOwnerMenu)
{
// hihacking owner menu on first interaction
needCaptureOwnerMenu = false;
CaptureOwnerMenu(rect, floatMenu);
}
base.DoGUI(rect, colonistOrdering, floatMenu);
// submenu indicator
if (DoSubmenuIndicator)
{
float offset = (!this.Disabled && Mouse.IsOver(rect)) ? 2 : 0;
var tex = TexSubmenuIndicator;
GUI.DrawTexture(new Rect(rect.xMax - tex.width + offset, rect.yMin + (rect.height - tex.height) / 2, tex.width, tex.height), tex);
}
return closed; // do not close the window untill sub option selected;
}
private void CaptureOwnerMenu(Rect rect, FloatMenu floatMenu)
{
// A bit hacky, but does not require parent menu subclass
floatMenu.vanishIfMouseDistant = false;
var oldCloseCallback = floatMenu.onCloseCallback;
var weakThis = new Verse.WeakReference<FloatSubmenuOption>(this);
floatMenu.onCloseCallback = () =>
{
weakThis?.Target?.submenu?.Close(false);
oldCloseCallback?.Invoke();
};
optionRect = rect;
this.OwnerMenu = floatMenu;
}
}
/// <summary>
/// Menu window with fixed screen location
/// </summary>
class FixedFloatMenu : FloatMenu
{
Rect screenLocation = Rect.zero;
/// <summary>
/// Constructor
/// </summary>
/// <param name="location">Menu location in screen coordinates</param>
/// <param name="options">menu items</param>
/// <remarks> Menu will be shown at the edge of provided rect. Default edge is the right one, but it will be shown at the left if there is not enough space at the right.
///
/// To obtain screen coordinates use GUIUtility.GUIToScreenRect method</remarks>
public FixedFloatMenu(Rect location, List<FloatMenuOption> options) : base(options)
{
onlyOneOfTypeAllowed = false;
vanishIfMouseDistant = false;
screenLocation = location;
}
/// <summary>
/// Constructor
/// </summary>
/// <param name="location">menu location in screen coordinates</param>
/// <param name="options">menu items</param>
/// <param name="needSelection">whatever it is in vanilla</param>
/// <remarks> Menu will be shown at the edge of provided rect. Default edge is the right one, but it will be shown at the left if there is not enough space at the right.
///
/// To obtain screen coordinates use GUIUtility.GUIToScreenRect method</remarks>
public FixedFloatMenu(Rect location, List<FloatMenuOption> options, string title, bool needSelection = false) : base(options, title, needSelection)
{
onlyOneOfTypeAllowed = false;
vanishIfMouseDistant = false;
screenLocation = location;
}
protected override void SetInitialSizeAndPosition()
{
var menuSize = this.InitialSize;
// trying to show menu at the right of provided rect
Vector2 vector = new Vector2(screenLocation.xMax, screenLocation.yMin);
if (vector.x + this.InitialSize.x > (float)UI.screenWidth)
{
// but showing at the left if there is not enough space
vector.x = screenLocation.xMin - menuSize.x;
}
if (vector.y + this.InitialSize.y > (float)UI.screenHeight)
{
vector.y = (float)UI.screenHeight - menuSize.y;
}
this.windowRect = new Rect(vector.x, vector.y, menuSize.x, menuSize.y);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment