From 377d1d5785c81d55b81ce5dea410e23e0736b60e Mon Sep 17 00:00:00 2001 From: Alexander Starikov Date: Wed, 3 Dec 2025 18:49:06 +0300 Subject: [PATCH 1/2] feat: nested SubMenus --- Celeste.Mod.mm/Mod/UI/TextMenuExt.cs | 54 ++++++++++++++++++++-------- 1 file changed, 40 insertions(+), 14 deletions(-) diff --git a/Celeste.Mod.mm/Mod/UI/TextMenuExt.cs b/Celeste.Mod.mm/Mod/UI/TextMenuExt.cs index b2af12c44..2f9f18751 100644 --- a/Celeste.Mod.mm/Mod/UI/TextMenuExt.cs +++ b/Celeste.Mod.mm/Mod/UI/TextMenuExt.cs @@ -428,8 +428,6 @@ public override void Render(Vector2 position, bool highlighted) { /// /// that acts as a Submenu for other Items. - ///

- /// Currently does not support recursive submenus ///
public class SubMenu : patch_Item { public string Label; @@ -507,13 +505,15 @@ public float ScrollTargetY { public float MenuHeight { get; private set; } public bool Focused; + public bool AutoScroll; + + public bool ItemsVisible; + public SubMenu Parent = null; private bool enterOnSelect; private bool entering; private float ease; - private bool containerAutoScroll; - /// /// Create a new SubMenu. /// @@ -558,6 +558,8 @@ public SubMenu Add(TextMenu.Item item) { if (Container != null) { Items.Add(item); item.Container = Container; + if (item is SubMenu menu) + menu.Parent = this; Container.Add(item.ValueWiggler = Wiggler.Create(0.25f, 3f, null, false, false)); Container.Add(item.SelectWiggler = Wiggler.Create(0.25f, 3f, null, false, false)); item.ValueWiggler.UseRawDeltaTime = (item.SelectWiggler.UseRawDeltaTime = true); @@ -580,6 +582,8 @@ public SubMenu Insert(int index, TextMenu.Item item) { if (Container != null) { Items.Insert(index, item); item.Container = Container; + if (item is SubMenu menu) + menu.Parent = this; Container.Add(item.ValueWiggler = Wiggler.Create(0.25f, 3f, null, false, false)); Container.Add(item.SelectWiggler = Wiggler.Create(0.25f, 3f, null, false, false)); item.ValueWiggler.UseRawDeltaTime = (item.SelectWiggler.UseRawDeltaTime = true); @@ -663,7 +667,11 @@ public void MoveSelection(int direction, bool wiggle = false) { // Avoid crash when getting Current item Selection = selection; Exit(); - Container.MoveSelection(direction, true); + if (Parent is null) { + Container.MoveSelection(direction, true); + } else { + Parent.MoveSelection(direction, true); + } return; } } @@ -722,7 +730,8 @@ public void RecalculateSize() { /// public float GetYOffsetOf(TextMenu.Item item) { - float offset = Container.GetYOffsetOf(this) - Height() * 0.5f; + float y_offset = Parent is not null ? Parent.GetYOffsetOf(this) : Container.GetYOffsetOf(this); + float offset = y_offset - Height() * 0.5f; if (item == null) { // common case is all items in submenu are disabled when item is null return offset + TitleHeight * 0.5f; @@ -742,10 +751,17 @@ public float GetYOffsetOf(TextMenu.Item item) { public void Exit() { Current?.OnLeave?.Invoke(); Focused = false; + ItemsVisible = false; if (!Input.MenuUp.Repeating && !Input.MenuDown.Repeating) Audio.Play(SFX.ui_main_button_back); - Container.AutoScroll = containerAutoScroll; - Container.Focused = true; + + if (Parent is null) { + Container.AutoScroll = AutoScroll; + Container.Focused = true; + } else { + Parent.AutoScroll = AutoScroll; + Parent.Focused = true; + } } public override string SearchLabel() { @@ -758,10 +774,20 @@ public override string SearchLabel() { public override void ConfirmPressed() { if (Items.Count > 0) { - Container.Focused = false; + if (Parent is null) + Container.Focused = false; + else + Parent.Focused = false; Focused = true; - containerAutoScroll = Container.AutoScroll; - Container.AutoScroll = false; + ItemsVisible = true; + + if (Parent is null) { + AutoScroll = Container.AutoScroll; + Container.AutoScroll = false; + } else { + AutoScroll = Parent.AutoScroll; + Parent.AutoScroll = false; + } entering = true; if (Input.MenuUp.Pressed) @@ -800,7 +826,7 @@ public override void Added() { } public override void Update() { - if (Focused) + if (ItemsVisible) ease = Calc.Approach(ease, 1f, Engine.RawDeltaTime * 4f); else ease = Calc.Approach(ease, 0f, Engine.RawDeltaTime * 4f); @@ -850,7 +876,7 @@ public override void Update() { } } - if (Focused && containerAutoScroll) { + if (Focused && AutoScroll) { if (Container.Height > Container.ScrollableMinSize) { Container.Position.Y += (ScrollTargetY - Container.Position.Y) * (1f - (float) Math.Pow(0.01f, Engine.RawDeltaTime)); return; @@ -874,7 +900,7 @@ public override void Render(Vector2 position, bool highlighted) { SubMenu.DrawIcon(titlePosition, Icon, iconJustify, true, (Disabled || Items.Count < 1 ? Color.DarkSlateGray : (Focused ? Container.HighlightColor : Color.White)) * alpha, 0.8f); ActiveFont.DrawOutline(Label, titlePosition, justify, Vector2.One, color, 2f, strokeColor); - if (Focused && ease > 0.9f) { + if (ItemsVisible && ease > 0.9f) { Vector2 menuPosition = new Vector2(top.X + ItemIndent, top.Y + TitleHeight + ItemSpacing); RecalculateSize(); foreach (TextMenu.Item item in Items) { From 7925f4e4753af791b7561a6dd984f8ae519e6770 Mon Sep 17 00:00:00 2001 From: wartori Date: Tue, 23 Jun 2026 18:14:08 +0200 Subject: [PATCH 2/2] Clear Parent on SubMenu removal and fix height cache invalidation issues --- Celeste.Mod.mm/Mod/UI/TextMenuExt.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Celeste.Mod.mm/Mod/UI/TextMenuExt.cs b/Celeste.Mod.mm/Mod/UI/TextMenuExt.cs index 2f9f18751..2b27b9016 100644 --- a/Celeste.Mod.mm/Mod/UI/TextMenuExt.cs +++ b/Celeste.Mod.mm/Mod/UI/TextMenuExt.cs @@ -617,6 +617,8 @@ public SubMenu Remove(TextMenu.Item item) { return this; } item.Container = null; + if (item is SubMenu menu) + menu.Parent = null; Container.Remove(item.ValueWiggler); Container.Remove(item.SelectWiggler); RecalculateSize(); @@ -722,6 +724,8 @@ public void RecalculateSize() { } foreach (TextMenu.Item item in Items) { if (item.Visible) { + if (item is SubMenu menu) // The heights of the submenus may change at any time due to the ease parameter, recalculation is needed + menu.RecalculateSize(); MenuHeight += item.Height() + Container.ItemSpacing; } }