Instantly share code, notes, and snippets.
Last active
February 10, 2021 19:49
-
Star
(0)
0
You must be signed in to star a gist -
Fork
(0)
0
You must be signed in to fork a gist
-
Save krypt-lynx/08d4b4f87e6a2d74437637f2953547b2 to your computer and use it in GitHub Desktop.
Cascading menus implementation for RimWorld game
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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