diff --git a/Source/IMaterialAcrylicPage.cs b/Source/IMaterialAcrylicPage.cs index cd88a20..4cda439 100644 --- a/Source/IMaterialAcrylicPage.cs +++ b/Source/IMaterialAcrylicPage.cs @@ -4,3 +4,10 @@ internal interface IMaterialAcrylicPage { bool SuppressNormalRender { get; set; } void RenderMaterialContent(bool acrylicActive); } + +internal interface IMaterialAcrylicOverlay { + bool HasOverlaysForAcrylic { get; } + void HideOverlaysForAcrylicCapture(); + void RestoreOverlaysAfterAcrylicCapture(); + void RenderOverlaysAboveAcrylic(); +} diff --git a/Source/MaterialAcrylicRenderer.cs b/Source/MaterialAcrylicRenderer.cs index 2683bf7..49bd947 100644 --- a/Source/MaterialAcrylicRenderer.cs +++ b/Source/MaterialAcrylicRenderer.cs @@ -67,13 +67,16 @@ private static void RenderCore(On.Monocle.Engine.orig_RenderCore orig, Engine se rendering = true; activePage = page; + IMaterialAcrylicOverlay? overlay = page as IMaterialAcrylicOverlay; page.SuppressNormalRender = true; try { + overlay?.HideOverlaysForAcrylicCapture(); // Keep the normal Engine render lifecycle intact. In particular, the // Overworld must run BeforeRender exactly once so its mountain and HUD // buffers are ready before RenderScene redirects the final composition. orig(self); } finally { + overlay?.RestoreOverlaysAfterAcrylicCapture(); page.SuppressNormalRender = false; activePage = null; rendering = false; @@ -86,6 +89,7 @@ private static void RenderScene(On.Monocle.Scene.orig_Render orig, Scene self) { orig(self); return; } + IMaterialAcrylicOverlay? overlay = page as IMaterialAcrylicOverlay; GraphicsDevice graphics = Engine.Graphics.GraphicsDevice; RenderTargetBinding[] originalTargets = graphics.GetRenderTargets(); @@ -94,6 +98,7 @@ private static void RenderScene(On.Monocle.Scene.orig_Render orig, Scene self) { try { EnsureTargets(Math.Max(1, originalViewport.Width), Math.Max(1, originalViewport.Height)); if (sceneTarget is null || blurTemporary is null || blurredTarget is null) { + overlay?.RestoreOverlaysAfterAcrylicCapture(); RestoreOutput(graphics, originalTargets, originalViewport); page.SuppressNormalRender = false; orig(self); @@ -103,7 +108,15 @@ private static void RenderScene(On.Monocle.Scene.orig_Render orig, Scene self) { graphics.SetRenderTarget(sceneTarget); graphics.Viewport = new Viewport(0, 0, sceneTarget.Width, sceneTarget.Height); graphics.Clear(Engine.ClearColor); - orig(self); + HiresSnow? snow = (self as Overworld)?.Snow; + bool renderSnow = snow is { Visible: true }; + if (renderSnow) snow!.Visible = false; + try { + orig(self); + } finally { + overlay?.RestoreOverlaysAfterAcrylicCapture(); + if (renderSnow) snow!.Visible = true; + } sceneRendered = true; float sampleScale = MathHelper.Lerp(0.8f, 3.1f, @@ -123,9 +136,12 @@ private static void RenderScene(On.Monocle.Scene.orig_Render orig, Scene self) { page.SuppressNormalRender = false; DrawMaterialPage(page); + DrawOverlays(overlay); + if (renderSnow) snow!.Render(self); successfulFrames++; } catch (Exception exception) { page.SuppressNormalRender = false; + overlay?.RestoreOverlaysAfterAcrylicCapture(); RestoreOutput(graphics, originalTargets, originalViewport); failed = true; Logger.LogDetailed(exception, "MicroblocksQolUtils/MaterialAcrylic"); @@ -163,6 +179,15 @@ private static void DrawTexture( } private static void DrawMaterialPage(IMaterialAcrylicPage page) { + DrawScreenBatch(() => page.RenderMaterialContent(acrylicActive: true)); + } + + private static void DrawOverlays(IMaterialAcrylicOverlay? overlay) { + if (overlay is null || !overlay.HasOverlaysForAcrylic) return; + DrawScreenBatch(() => overlay.RenderOverlaysAboveAcrylic()); + } + + private static void DrawScreenBatch(Action render) { bool begun = false; try { Draw.SpriteBatch.Begin( @@ -175,9 +200,15 @@ private static void DrawMaterialPage(IMaterialAcrylicPage page) { Engine.ScreenMatrix ); begun = true; - page.RenderMaterialContent(acrylicActive: true); + render(); } finally { - if (begun) Draw.SpriteBatch.End(); + if (begun) { + try { + Draw.SpriteBatch.End(); + } catch (Exception exception) { + Logger.LogDetailed(exception, "MicroblocksQolUtils/MaterialAcrylic"); + } + } } } diff --git a/Source/MaterialModOptions.cs b/Source/MaterialModOptions.cs index 56f3ed8..6b635c9 100644 --- a/Source/MaterialModOptions.cs +++ b/Source/MaterialModOptions.cs @@ -14,7 +14,7 @@ namespace Celeste.Mod.MicroblocksQolUtils; /// TextMenu, so custom entries and binding/config sub-pages keep their original behavior; /// this class only owns grouping, navigation, layout and mouse interaction. /// -public sealed class MaterialModOptions : Oui, IMaterialAcrylicPage { +public sealed class MaterialModOptions : Oui, IMaterialAcrylicPage, IMaterialAcrylicOverlay { private const float ScreenWidth = 1920f; private const float ScreenHeight = 1080f; private const float TabHeight = MaterialSplitPageLayout.TabHeight; @@ -90,6 +90,9 @@ public sealed class MaterialModOptions : Oui, IMaterialAcrylicPage { private string settingSearchText = ""; private string imeText = ""; private SearchTarget searchTarget; + private bool configOverlayOpen; + private readonly List externalOverlays = []; + private readonly List<(Entity Entity, bool Visible)> hiddenOverlays = []; public bool SuppressNormalRender { get; set; } @@ -189,6 +192,7 @@ public override void Removed(Scene scene) { public override void Update() { MaterialTouch.BeginUpdate(this); base.Update(); + RefreshExternalOverlays(); ModOptionsLayout layout = ModOptionsLayout.Create(1f - Ease.CubeOut(ease)); tabScroll.Update(MaxTabScroll(layout)); rowScroll.Update(MaxRowScroll(layout)); @@ -206,6 +210,11 @@ public override void Update() { } if (menu is null) return; + if (configOverlayOpen) { + // The key/button config screen owns input while it is open. + menu.Focused = false; + return; + } bool acceptsInput = pauseLevel is not null || Selected && Focused; if (acceptsInput && inputDelay <= 0f && dropdownItem is null && compositePopupItem is null && UpdateSearchInput(layout)) { @@ -1004,6 +1013,46 @@ private void EnsureCompositePopupSelectionVisible(ModOptionsLayout layout, TextM } } + private void RefreshExternalOverlays() { + externalOverlays.Clear(); + configOverlayOpen = false; + if (!Visible || Scene is not { } scene) return; + foreach (Entity entity in scene.Entities) { + if (entity is KeyboardConfigUI or ButtonConfigUI) { + configOverlayOpen = true; + if (entity.Depth >= Depth) entity.Depth = Depth - 1; + } + if (!entity.Visible || !entity.TagCheck(Tags.HUD) || entity.Depth >= Depth) continue; + externalOverlays.Add(entity); + } + } + + bool IMaterialAcrylicOverlay.HasOverlaysForAcrylic => externalOverlays.Count > 0; + + void IMaterialAcrylicOverlay.HideOverlaysForAcrylicCapture() { + hiddenOverlays.Clear(); + foreach (Entity entity in externalOverlays) { + hiddenOverlays.Add((entity, entity.Visible)); + entity.Visible = false; + } + } + + void IMaterialAcrylicOverlay.RestoreOverlaysAfterAcrylicCapture() { + foreach ((Entity entity, bool visible) in hiddenOverlays) entity.Visible = visible; + hiddenOverlays.Clear(); + } + + void IMaterialAcrylicOverlay.RenderOverlaysAboveAcrylic() { + foreach (Entity entity in externalOverlays) { + if (!entity.Visible) continue; + try { + entity.Render(); + } catch (Exception exception) { + Logger.LogDetailed(exception, "MicroblocksQolUtils/MaterialAcrylic"); + } + } + } + private void SelectItem(TextMenu.Item item) { if (menu is null || menu.Current == item) return; menu.Current?.OnLeave?.Invoke(); @@ -1034,6 +1083,11 @@ private void EnsureTabVisible(ModOptionsLayout layout) { } private void UpdateInteractions(ModOptionsLayout layout) { + if (configOverlayOpen) { + // Do not leave hidden setting rows alive as touch targets under the config UI. + MaterialTouch.Suppress(this, motion, TouchContext); + return; + } List targets = [ new MaterialInteractionTarget("mod-options.search.tabs", layout.TabSearch, Focused: searchTarget == SearchTarget.Tabs, TouchKind: "text", TouchText: tabSearchText, diff --git a/Source/MaterialTouch.cs b/Source/MaterialTouch.cs index 3df90d9..efb9f5e 100644 --- a/Source/MaterialTouch.cs +++ b/Source/MaterialTouch.cs @@ -96,6 +96,11 @@ internal static void Publish(Entity owner, IEnumerable orig, string mode) { Entity? owner = ActiveOwner(); if (owner is null || !surfaces.TryGetValue(owner, out Surface? surface)) { orig(mode); return; } diff --git a/Source/QolSettingsOverlay.cs b/Source/QolSettingsOverlay.cs index 45b072a..ceeebe9 100644 --- a/Source/QolSettingsOverlay.cs +++ b/Source/QolSettingsOverlay.cs @@ -108,7 +108,12 @@ public override void Update() { recordingRefreshTimer -= Engine.RawDeltaTime; if (IsRecorderTab && IsRecorderLibrary && recordingRefreshTimer <= 0f) RefreshRecordingFiles(); - if (bindingConfig is not null) return; + if (bindingConfig is not null) { + // The key/button config screen handles its own input; do not leave the hidden + // setting rows alive as touch targets underneath it. + MaterialTouch.Suppress(this, motion, TouchContext); + return; + } OverlayLayout layout = OverlayLayout.Create(1f - Ease.CubeOut(ease)); rowScroll.Update(MaxRowScroll(layout));