diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/config/ConfigDiffPanelComponent.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/config/ConfigDiffPanelComponent.java index 105c95b..55df4dc 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/config/ConfigDiffPanelComponent.java +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/config/ConfigDiffPanelComponent.java @@ -4,56 +4,32 @@ import dev.loki.lovisual.config.profile.diff.ConfigProfileDiff; import dev.loki.lovisual.config.profile.snapshot.ConfigProfileMeta; import dev.loki.lovisual.features.gui.clickgui.render.ClickGuiRenderer; import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.SettingsGuiPalette; +import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.implement.config.diff.ConfigDiffBodyRenderer; +import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.implement.config.diff.ConfigDiffHeaderRenderer; +import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.implement.config.diff.ConfigDiffScrollbar; import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.render.LayoutRender2D; import dev.loki.lovisual.features.gui.clickgui.util.ClickGuiI18n; import dev.loki.lovisual.features.gui.clickgui.util.ClickGuiMath; import dev.loki.lovisual.render.engine.animation.AnimationUtility; -import dev.loki.lovisual.render.engine.renderer.Renderer2D; -import dev.loki.lovisual.render.engine.svg.model.SvgRenderOptions; import dev.loki.lovisual.render.helpers.clip.ScissorFunction; -import dev.loki.lovisual.render.helpers.util.SystemCursor; +/** + * Facade of the config diff popup: open/close animation, panel chrome, input + * dispatch. The header buttons, body text and scrollbar live in the + * {@code config.diff} helpers (see mod/TODO.md 8.5.3). + */ public final class ConfigDiffPanelComponent { - private float scroll; - private float smoothedScroll; + private final ConfigDiffHeaderRenderer header = new ConfigDiffHeaderRenderer(); + private final ConfigDiffScrollbar scrollbar = new ConfigDiffScrollbar(); private float openAnim; private boolean openTarget; - private float saveHoverAnim; - private float closeHoverAnim; - private boolean scrollbarVisible; - private boolean draggingScrollbar; - private float scrollbarX; - private float scrollbarY; - private float scrollbarW; - private float scrollbarH; - private float scrollbarThumbY; - private float scrollbarThumbH; - private float scrollbarMaxScroll; - private float scrollbarDragOffset; private float panelX; private float panelY; private float panelW; private float panelH; - private float saveX; - private float saveY; - private float saveW; - private float saveH; - private float closeX; - private float closeY; - private float closeW; - private float closeH; - - private static String stringify(Object value) { - if (value == null) return "null"; - String text = String.valueOf(value); - if (text.length() > 34) text = text.substring(0, 31) + "..."; - return text; - } public void resetScroll() { - scroll = 0f; - smoothedScroll = 0f; - draggingScrollbar = false; + scrollbar.reset(); } public void open() { @@ -62,7 +38,7 @@ public final class ConfigDiffPanelComponent { public void close() { openTarget = false; - draggingScrollbar = false; + scrollbar.release(); } public boolean isActive() { @@ -127,264 +103,62 @@ public final class ConfigDiffPanelComponent { text, false ); - renderHeaderActions(x, y, w, mx(), my(), scale, palette); + header.render(x, y, w, ClickGuiRenderer.getMouseX(), ClickGuiRenderer.getMouseY(), scale, palette, openAnim); float bodyX = x + 8f * scale; float bodyY = y + 27f * scale; float bodyW = w - 16f * scale; float bodyH = h - 31f * scale; - float contentH = contentHeight(selected, diff, error, scale); + float contentH = ConfigDiffBodyRenderer.contentHeight(selected, diff, error, scale); float maxScroll = Math.max(0f, contentH - bodyH); - updateScrollbarMetrics(bodyX, bodyY, bodyW, bodyH, maxScroll, scale); - if (draggingScrollbar) scrollToMouse(ClickGuiRenderer.getMouseY()); + scrollbar.begin(bodyX, bodyY, bodyW, bodyH, maxScroll, scale); + if (scrollbar.dragging()) scrollbar.dragToMouse(ClickGuiRenderer.getMouseY()); + scrollbar.smooth(); - scroll = ClickGuiMath.clamp(scroll, -maxScroll, 0f); - smoothedScroll = AnimationUtility.approach(smoothedScroll, scroll, draggingScrollbar ? 0.55f : 0.2f); - smoothedScroll = AnimationUtility.snap(smoothedScroll, scroll, draggingScrollbar ? 0.01f : 0.05f); boolean clipped = ScissorFunction.pushRaw(bodyX, bodyY, bodyW, bodyH); - renderBody(bodyX, bodyY + smoothedScroll, bodyW - (scrollbarVisible ? 7f * scale : 0f), bodyY, bodyH, selected, diff, error, scale, palette); + ConfigDiffBodyRenderer.render( + bodyX, + bodyY + scrollbar.offset(), + bodyW - scrollbar.gutter(scale), + bodyY, + bodyH, + selected, + diff, + error, + scale, + palette, + openAnim + ); if (clipped) ScissorFunction.pop(); - renderScrollbar(scale, palette); + scrollbar.draw(scale, palette, openAnim); } public HeaderAction headerAction(float mx, float my, int button) { if (!openTarget || openAnim <= 0.01f) return HeaderAction.NONE; if (button != 0) return HeaderAction.NONE; - if (ClickGuiMath.insideRect(mx, my, closeX, closeY, closeW, closeH)) return HeaderAction.CLOSE; - if (ClickGuiMath.insideRect(mx, my, saveX, saveY, saveW, saveH)) return HeaderAction.SAVE; + if (header.closeHit(mx, my)) return HeaderAction.CLOSE; + if (header.saveHit(mx, my)) return HeaderAction.SAVE; return HeaderAction.NONE; } public boolean mousePressedScrollbar(float mx, float my, int button) { if (!openTarget || openAnim <= 0.01f) return false; if (button != 0) return false; - if (!isScrollbarHovered(mx, my)) return false; - draggingScrollbar = true; - scrollbarDragOffset = ClickGuiMath.insideRect(mx, my, scrollbarX, scrollbarThumbY, scrollbarW, scrollbarThumbH) - ? my - scrollbarThumbY - : scrollbarThumbH * 0.5f; - scrollToMouse(my); - return true; + return scrollbar.press(mx, my); } public void mouseReleased(int button) { - if (button == 0) draggingScrollbar = false; + if (button == 0) scrollbar.release(); } public boolean scroll(float mx, float my, double amount) { if (!openTarget || openAnim <= 0.01f) return false; if (!ClickGuiMath.insideRect(mx, my, panelX, panelY, panelW, panelH)) return false; - if (!scrollbarVisible && scrollbarMaxScroll <= 0.5f) return true; - scroll += (float) (amount * 20f); + scrollbar.wheel(amount); return true; } - private void renderHeaderActions(float x, - float y, - float w, - float mx, - float my, - float scale, - SettingsGuiPalette palette) { - float btn = 16f * scale; - closeX = x + w - btn - 4f * scale; - closeY = y + 3f * scale; - closeW = btn; - closeH = btn; - saveX = closeX - btn - scale; - saveY = closeY; - saveW = btn; - saveH = btn; - - boolean saveHover = ClickGuiMath.insideRect(mx, my, saveX, saveY, saveW, saveH); - boolean closeHover = ClickGuiMath.insideRect(mx, my, closeX, closeY, closeW, closeH); - saveHoverAnim = AnimationUtility.approach(saveHoverAnim, saveHover ? 1f : 0f, AnimationUtility.deltaTime(), 10f); - closeHoverAnim = AnimationUtility.approach(closeHoverAnim, closeHover ? 1f : 0f, AnimationUtility.deltaTime(), 10f); - - drawHeaderIcon(saveX, saveY, btn, "save", saveHoverAnim, scale, palette); - LayoutRender2D.rect(closeX - scale, y + 6f * scale, 0.5f * scale, 10f * scale, LayoutRender2D.alpha(palette.panelDivider(), openAnim)); - drawHeaderIcon(closeX, closeY, btn, "x", closeHoverAnim, scale, palette); - if (saveHover || closeHover) SystemCursor.set(SystemCursor.CursorType.MOVE); - } - - private void drawHeaderIcon(float x, - float y, - float size, - String icon, - float hover, - float scale, - SettingsGuiPalette palette) { - boolean save = "save".equals(icon); - int bgA = LayoutRender2D.alpha(SettingsGuiPalette.mix(palette.moduleCardTop(), save ? palette.menuCategorySelectedLeft() : palette.menuCategoryHoverLeft(), 0.14f + hover * 0.22f), openAnim); - int bgB = LayoutRender2D.alpha(SettingsGuiPalette.mix(palette.moduleCardTopStrong(), save ? palette.menuCategorySelectedRight() : palette.menuCategoryHoverRight(), 0.12f + hover * 0.18f), openAnim); - int bgC = LayoutRender2D.alpha(SettingsGuiPalette.mix(SettingsGuiPalette.darken(palette.moduleCardTopStrong(), 0.12f), save ? palette.menuCategorySelectedRight() : palette.menuCategoryHoverRight(), 0.08f + hover * 0.15f), openAnim); - LayoutRender2D.roundedSoftShadow( - x, - y + 0.4f * scale, - size, - size, - 3.5f * scale, - 4f * scale, - 0.015f + hover * 0.02f, - LayoutRender2D.alpha(palette.menuShadow(), openAnim * (0.50f + hover * 0.45f)) - ); - LayoutRender2D.roundedQuad(x, y, size, size, 3.5f * scale, bgA, bgB, bgC, bgA); - LayoutRender2D.roundedStrokeQuad( - x, - y, - size, - size, - 3.5f * scale, - (0.35f + hover * 0.18f) * scale, - LayoutRender2D.alpha(palette.moduleDividerStart(), openAnim * (0.54f + hover * 0.24f)), - LayoutRender2D.alpha(palette.moduleDividerEnd(), openAnim * (0.62f + hover * 0.24f)), - LayoutRender2D.alpha(palette.moduleDividerEnd(), openAnim * (0.48f + hover * 0.20f)), - LayoutRender2D.alpha(palette.moduleDividerStart(), openAnim * (0.54f + hover * 0.24f)) - ); - - int color = LayoutRender2D.alpha(SettingsGuiPalette.mix(palette.panelText(), palette.menuCategoryText(), 0.16f + hover * 0.38f), openAnim); - Renderer2D.COLOR.svg( - icon, - x + 4.6f * scale, - y + 4.6f * scale, - size - 9.2f * scale, - size - 9.2f * scale, - SvgRenderOptions.overrideColor(color) - ); - } - - private float mx() { - return ClickGuiRenderer.getMouseX(); - } - - private float my() { - return ClickGuiRenderer.getMouseY(); - } - - private void renderBody(float x, - float y, - float w, - float clipY, - float clipH, - ConfigProfileMeta selected, - ConfigProfileDiff diff, - String error, - float scale, - SettingsGuiPalette palette) { - if (error != null && !error.isBlank()) { - drawLine(ClickGuiI18n.tr("clickgui.settings.config.diff.error", "Diff error: %s", error), x, y, 7.2f * scale, palette.panelMuted()); - return; - } - if (selected == null) { - drawLine(ClickGuiI18n.tr("clickgui.settings.config.diff.select_config", "Select a config and press diff."), x, y, 7.2f * scale, palette.panelMuted()); - return; - } - if (diff == null) { - drawLine(selected.name(), x, y, 7.2f * scale, palette.panelText()); - drawLine(ClickGuiI18n.tr("clickgui.settings.config.diff.not_loaded", "Diff is not loaded."), x, y + 10f * scale, 7.2f * scale, palette.panelMuted()); - return; - } - if (diff.isEmpty()) { - drawLine(selected.name(), x, y, 7.2f * scale, palette.panelText()); - drawLine(ClickGuiI18n.tr("clickgui.settings.config.diff.no_differences", "No differences."), x, y + 11f * scale, 7.2f * scale, palette.panelMuted()); - return; - } - - float cy = y; - drawVisibleLine(ClickGuiRenderer.fitText(ClickGuiRenderer.getInterMedium(), selected.name(), 7.4f * scale, w), x, cy, 7.4f * scale, palette.panelText(), clipY, clipH, scale); - cy += 11f * scale; - drawVisibleLine(ClickGuiI18n.tr("clickgui.settings.config.diff.changed", "Changes: %s", diff.valueChangeCount()), x, cy, 7f * scale, palette.panelMuted(), clipY, clipH, scale); - cy += 13f * scale; - - for (ConfigProfileDiff.Entry entry : diff.entries()) { - String owner = ClickGuiRenderer.fitText(ClickGuiRenderer.getInterMedium(), entry.displayName(), 7.4f * scale, w); - drawVisibleLine(owner, x, cy, 7.4f * scale, palette.panelText(), clipY, clipH, scale); - cy += 9.5f * scale; - - for (ConfigProfileDiff.Value value : entry.values()) { - String text = value.key() + ": " + stringify(value.currentValue()) + " -> " + stringify(value.selectedValue()); - text = ClickGuiRenderer.fitText(ClickGuiRenderer.getInterRegular(), text, 6.7f * scale, w - 5f * scale); - drawVisibleLine(text, x + 5f * scale, cy, 6.7f * scale, palette.panelMuted(), clipY, clipH, scale); - cy += 8.5f * scale; - } - cy += 4f * scale; - } - } - - private float contentHeight(ConfigProfileMeta selected, ConfigProfileDiff diff, String error, float scale) { - if (error != null && !error.isBlank()) return 11f * scale; - if (selected == null) return 11f * scale; - if (diff == null || diff.isEmpty()) return 23f * scale; - - float h = 24f * scale; - for (ConfigProfileDiff.Entry entry : diff.entries()) { - h += 9.5f * scale; - h += entry.values().size() * 8.5f * scale; - h += 4f * scale; - } - return h; - } - - private void renderScrollbar(float scale, SettingsGuiPalette palette) { - if (!scrollbarVisible) return; - float mx = ClickGuiRenderer.getMouseX(); - float my = ClickGuiRenderer.getMouseY(); - if (isScrollbarHovered(mx, my) || draggingScrollbar) SystemCursor.set(SystemCursor.CursorType.SCROLL); - - LayoutRender2D.roundedQuad(scrollbarX, scrollbarY, scrollbarW, scrollbarH, 2f * scale, - LayoutRender2D.alpha(palette.moduleScrollTrackA(), openAnim), - LayoutRender2D.alpha(palette.moduleScrollTrackB(), openAnim), - LayoutRender2D.alpha(palette.moduleScrollTrackB(), openAnim), - LayoutRender2D.alpha(palette.moduleScrollTrackA(), openAnim)); - LayoutRender2D.roundedQuad(scrollbarX, scrollbarThumbY, scrollbarW, scrollbarThumbH, 2f * scale, - LayoutRender2D.alpha(palette.moduleScrollHandleA(), openAnim), - LayoutRender2D.alpha(palette.moduleScrollHandleB(), openAnim), - LayoutRender2D.alpha(palette.moduleScrollHandleB(), openAnim), - LayoutRender2D.alpha(palette.moduleScrollHandleA(), openAnim)); - } - - private void updateScrollbarMetrics(float x, float y, float w, float h, float maxScroll, float scale) { - scrollbarVisible = maxScroll > 0.5f; - scrollbarMaxScroll = Math.max(0f, maxScroll); - if (!scrollbarVisible) { - draggingScrollbar = false; - scrollbarX = scrollbarY = scrollbarW = scrollbarH = scrollbarThumbY = scrollbarThumbH = 0f; - return; - } - scrollbarW = 3f * scale; - scrollbarX = x + w - scrollbarW - 1.5f * scale; - scrollbarY = y + 2f * scale; - scrollbarH = Math.max(1f, h - 4f * scale); - scrollbarThumbH = Math.max(16f * scale, scrollbarH * (scrollbarH / (scrollbarMaxScroll + scrollbarH))); - float scrollRatio = scrollbarMaxScroll <= 0f ? 0f : (-smoothedScroll / scrollbarMaxScroll); - scrollbarThumbY = scrollbarY + (scrollbarH - scrollbarThumbH) * AnimationUtility.clamp(scrollRatio, 0f, 1f); - } - - private boolean isScrollbarHovered(float mx, float my) { - if (!scrollbarVisible) return false; - float pad = 4f; - return ClickGuiMath.insideRect(mx, my, scrollbarX - pad, scrollbarY - pad, scrollbarW + pad * 2f, scrollbarH + pad * 2f); - } - - private void scrollToMouse(float my) { - if (!scrollbarVisible || scrollbarMaxScroll <= 0f) return; - float span = scrollbarH - scrollbarThumbH; - if (span <= 0.5f) return; - float thumbTop = AnimationUtility.clamp(my - scrollbarDragOffset, scrollbarY, scrollbarY + span); - float ratio = (thumbTop - scrollbarY) / span; - scroll = -scrollbarMaxScroll * AnimationUtility.clamp(ratio, 0f, 1f); - } - - private void drawVisibleLine(String text, float x, float y, float size, int color, float clipY, float clipH, float scale) { - if (y + 9f * scale < clipY || y > clipY + clipH) return; - drawLine(text, x, y, size, color); - } - - private void drawLine(String text, float x, float y, float size, int color) { - ClickGuiRenderer.drawText(ClickGuiRenderer.getInterRegular(), text, x, y, size, LayoutRender2D.alpha(color, openAnim), false); - } - public enum HeaderAction { NONE, SAVE, diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/config/diff/ConfigDiffBodyRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/config/diff/ConfigDiffBodyRenderer.java new file mode 100644 index 0000000..bee0bdc --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/config/diff/ConfigDiffBodyRenderer.java @@ -0,0 +1,107 @@ +package dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.implement.config.diff; + +import dev.loki.lovisual.config.profile.diff.ConfigProfileDiff; +import dev.loki.lovisual.config.profile.snapshot.ConfigProfileMeta; +import dev.loki.lovisual.features.gui.clickgui.render.ClickGuiRenderer; +import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.SettingsGuiPalette; +import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.render.LayoutRender2D; +import dev.loki.lovisual.features.gui.clickgui.util.ClickGuiI18n; + +/** + * Body of the diff panel: the empty-state messages and the per-setting change + * lines, plus the height math the scroller needs before painting. Stateless — + * the panel passes its fade value in on every call. + */ +public final class ConfigDiffBodyRenderer { + private ConfigDiffBodyRenderer() { + } + + public static void render(float x, + float y, + float w, + float clipY, + float clipH, + ConfigProfileMeta selected, + ConfigProfileDiff diff, + String error, + float scale, + SettingsGuiPalette palette, + float openAnim) { + if (error != null && !error.isBlank()) { + drawLine(ClickGuiI18n.tr("clickgui.settings.config.diff.error", "Diff error: %s", error), x, y, 7.2f * scale, palette.panelMuted(), openAnim); + return; + } + if (selected == null) { + drawLine(ClickGuiI18n.tr("clickgui.settings.config.diff.select_config", "Select a config and press diff."), x, y, 7.2f * scale, palette.panelMuted(), openAnim); + return; + } + if (diff == null) { + drawLine(selected.name(), x, y, 7.2f * scale, palette.panelText(), openAnim); + drawLine(ClickGuiI18n.tr("clickgui.settings.config.diff.not_loaded", "Diff is not loaded."), x, y + 10f * scale, 7.2f * scale, palette.panelMuted(), openAnim); + return; + } + if (diff.isEmpty()) { + drawLine(selected.name(), x, y, 7.2f * scale, palette.panelText(), openAnim); + drawLine(ClickGuiI18n.tr("clickgui.settings.config.diff.no_differences", "No differences."), x, y + 11f * scale, 7.2f * scale, palette.panelMuted(), openAnim); + return; + } + + float cy = y; + drawVisibleLine(ClickGuiRenderer.fitText(ClickGuiRenderer.getInterMedium(), selected.name(), 7.4f * scale, w), x, cy, 7.4f * scale, palette.panelText(), clipY, clipH, scale, openAnim); + cy += 11f * scale; + drawVisibleLine(ClickGuiI18n.tr("clickgui.settings.config.diff.changed", "Changes: %s", diff.valueChangeCount()), x, cy, 7f * scale, palette.panelMuted(), clipY, clipH, scale, openAnim); + cy += 13f * scale; + + for (ConfigProfileDiff.Entry entry : diff.entries()) { + String owner = ClickGuiRenderer.fitText(ClickGuiRenderer.getInterMedium(), entry.displayName(), 7.4f * scale, w); + drawVisibleLine(owner, x, cy, 7.4f * scale, palette.panelText(), clipY, clipH, scale, openAnim); + cy += 9.5f * scale; + + for (ConfigProfileDiff.Value value : entry.values()) { + String text = value.key() + ": " + stringify(value.currentValue()) + " -> " + stringify(value.selectedValue()); + text = ClickGuiRenderer.fitText(ClickGuiRenderer.getInterRegular(), text, 6.7f * scale, w - 5f * scale); + drawVisibleLine(text, x + 5f * scale, cy, 6.7f * scale, palette.panelMuted(), clipY, clipH, scale, openAnim); + cy += 8.5f * scale; + } + cy += 4f * scale; + } + } + + public static float contentHeight(ConfigProfileMeta selected, ConfigProfileDiff diff, String error, float scale) { + if (error != null && !error.isBlank()) return 11f * scale; + if (selected == null) return 11f * scale; + if (diff == null || diff.isEmpty()) return 23f * scale; + + float h = 24f * scale; + for (ConfigProfileDiff.Entry entry : diff.entries()) { + h += 9.5f * scale; + h += entry.values().size() * 8.5f * scale; + h += 4f * scale; + } + return h; + } + + private static String stringify(Object value) { + if (value == null) return "null"; + String text = String.valueOf(value); + if (text.length() > 34) text = text.substring(0, 31) + "..."; + return text; + } + + private static void drawVisibleLine(String text, + float x, + float y, + float size, + int color, + float clipY, + float clipH, + float scale, + float openAnim) { + if (y + 9f * scale < clipY || y > clipY + clipH) return; + drawLine(text, x, y, size, color, openAnim); + } + + private static void drawLine(String text, float x, float y, float size, int color, float openAnim) { + ClickGuiRenderer.drawText(ClickGuiRenderer.getInterRegular(), text, x, y, size, LayoutRender2D.alpha(color, openAnim), false); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/config/diff/ConfigDiffHeaderRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/config/diff/ConfigDiffHeaderRenderer.java new file mode 100644 index 0000000..5fd3e6b --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/config/diff/ConfigDiffHeaderRenderer.java @@ -0,0 +1,112 @@ +package dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.implement.config.diff; + +import dev.loki.lovisual.features.gui.clickgui.render.ClickGuiRenderer; +import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.SettingsGuiPalette; +import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.render.LayoutRender2D; +import dev.loki.lovisual.features.gui.clickgui.util.ClickGuiMath; +import dev.loki.lovisual.render.engine.animation.AnimationUtility; +import dev.loki.lovisual.render.engine.renderer.Renderer2D; +import dev.loki.lovisual.render.engine.svg.model.SvgRenderOptions; +import dev.loki.lovisual.render.helpers.util.SystemCursor; + +/** + * Header chrome of the diff panel: the save/close icon buttons — their rects, + * hover animation, painting and hit-testing. Owned by + * {@code ConfigDiffPanelComponent}, which forwards the panel's fade value here. + */ +public final class ConfigDiffHeaderRenderer { + private float saveX; + private float saveY; + private float saveW; + private float saveH; + private float closeX; + private float closeY; + private float closeW; + private float closeH; + private float saveHoverAnim; + private float closeHoverAnim; + + public void render(float x, + float y, + float w, + float mx, + float my, + float scale, + SettingsGuiPalette palette, + float openAnim) { + float btn = 16f * scale; + closeX = x + w - btn - 4f * scale; + closeY = y + 3f * scale; + closeW = btn; + closeH = btn; + saveX = closeX - btn - scale; + saveY = closeY; + saveW = btn; + saveH = btn; + + boolean saveHover = ClickGuiMath.insideRect(mx, my, saveX, saveY, saveW, saveH); + boolean closeHover = ClickGuiMath.insideRect(mx, my, closeX, closeY, closeW, closeH); + saveHoverAnim = AnimationUtility.approach(saveHoverAnim, saveHover ? 1f : 0f, AnimationUtility.deltaTime(), 10f); + closeHoverAnim = AnimationUtility.approach(closeHoverAnim, closeHover ? 1f : 0f, AnimationUtility.deltaTime(), 10f); + + drawIcon(saveX, saveY, btn, "save", saveHoverAnim, scale, palette, openAnim); + LayoutRender2D.rect(closeX - scale, y + 6f * scale, 0.5f * scale, 10f * scale, LayoutRender2D.alpha(palette.panelDivider(), openAnim)); + drawIcon(closeX, closeY, btn, "x", closeHoverAnim, scale, palette, openAnim); + if (saveHover || closeHover) SystemCursor.set(SystemCursor.CursorType.MOVE); + } + + public boolean saveHit(float mx, float my) { + return ClickGuiMath.insideRect(mx, my, saveX, saveY, saveW, saveH); + } + + public boolean closeHit(float mx, float my) { + return ClickGuiMath.insideRect(mx, my, closeX, closeY, closeW, closeH); + } + + private void drawIcon(float x, + float y, + float size, + String icon, + float hover, + float scale, + SettingsGuiPalette palette, + float openAnim) { + boolean save = "save".equals(icon); + int bgA = LayoutRender2D.alpha(SettingsGuiPalette.mix(palette.moduleCardTop(), save ? palette.menuCategorySelectedLeft() : palette.menuCategoryHoverLeft(), 0.14f + hover * 0.22f), openAnim); + int bgB = LayoutRender2D.alpha(SettingsGuiPalette.mix(palette.moduleCardTopStrong(), save ? palette.menuCategorySelectedRight() : palette.menuCategoryHoverRight(), 0.12f + hover * 0.18f), openAnim); + int bgC = LayoutRender2D.alpha(SettingsGuiPalette.mix(SettingsGuiPalette.darken(palette.moduleCardTopStrong(), 0.12f), save ? palette.menuCategorySelectedRight() : palette.menuCategoryHoverRight(), 0.08f + hover * 0.15f), openAnim); + LayoutRender2D.roundedSoftShadow( + x, + y + 0.4f * scale, + size, + size, + 3.5f * scale, + 4f * scale, + 0.015f + hover * 0.02f, + LayoutRender2D.alpha(palette.menuShadow(), openAnim * (0.50f + hover * 0.45f)) + ); + LayoutRender2D.roundedQuad(x, y, size, size, 3.5f * scale, bgA, bgB, bgC, bgA); + LayoutRender2D.roundedStrokeQuad( + x, + y, + size, + size, + 3.5f * scale, + (0.35f + hover * 0.18f) * scale, + LayoutRender2D.alpha(palette.moduleDividerStart(), openAnim * (0.54f + hover * 0.24f)), + LayoutRender2D.alpha(palette.moduleDividerEnd(), openAnim * (0.62f + hover * 0.24f)), + LayoutRender2D.alpha(palette.moduleDividerEnd(), openAnim * (0.48f + hover * 0.20f)), + LayoutRender2D.alpha(palette.moduleDividerStart(), openAnim * (0.54f + hover * 0.24f)) + ); + + int color = LayoutRender2D.alpha(SettingsGuiPalette.mix(palette.panelText(), palette.menuCategoryText(), 0.16f + hover * 0.38f), openAnim); + Renderer2D.COLOR.svg( + icon, + x + 4.6f * scale, + y + 4.6f * scale, + size - 9.2f * scale, + size - 9.2f * scale, + SvgRenderOptions.overrideColor(color) + ); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/config/diff/ConfigDiffScrollbar.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/config/diff/ConfigDiffScrollbar.java new file mode 100644 index 0000000..a1fa32c --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/config/diff/ConfigDiffScrollbar.java @@ -0,0 +1,137 @@ +package dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.implement.config.diff; + +import dev.loki.lovisual.features.gui.clickgui.render.ClickGuiRenderer; +import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.SettingsGuiPalette; +import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.render.LayoutRender2D; +import dev.loki.lovisual.features.gui.clickgui.util.ClickGuiMath; +import dev.loki.lovisual.render.engine.animation.AnimationUtility; +import dev.loki.lovisual.render.helpers.util.SystemCursor; + +/** + * Scroll state of the diff panel: wheel accumulation, clamping/smoothing of the + * offset, thumb geometry, drag handling and painting. The panel drives it in + * phases — {@link #begin} (geometry), {@link #dragToMouse} while dragging, + * {@link #smooth} (offset), then {@link #draw}. + */ +public final class ConfigDiffScrollbar { + private boolean visible; + private boolean dragging; + private float x; + private float y; + private float w; + private float h; + private float thumbY; + private float thumbH; + private float maxScroll; + private float dragOffset; + private float scroll; + private float smoothed; + + public void reset() { + scroll = 0f; + smoothed = 0f; + dragging = false; + } + + public void release() { + dragging = false; + } + + public boolean visible() { + return visible; + } + + public boolean dragging() { + return dragging; + } + + /** Smoothed offset the body is painted at. */ + public float offset() { + return smoothed; + } + + /** Wheel input: true when a scrollbar exists (a bare panel absorbs the event). */ + public boolean wheel(double amount) { + if (!visible && maxScroll <= 0.5f) return false; + scroll += (float) (amount * 20f); + return true; + } + + public boolean press(float mx, float my) { + if (!hovered(mx, my)) return false; + dragging = true; + dragOffset = ClickGuiMath.insideRect(mx, my, x, thumbY, w, thumbH) + ? my - thumbY + : thumbH * 0.5f; + dragToMouse(my); + return true; + } + + /** Thumb geometry for the current body rect and content height. */ + public void begin(float x, float y, float w, float h, float maxScroll, float scale) { + this.x = x; + this.y = y; + this.w = w; + this.h = h; + visible = maxScroll > 0.5f; + this.maxScroll = Math.max(0f, maxScroll); + if (!visible) { + dragging = false; + this.x = this.y = this.w = this.h = thumbY = thumbH = 0f; + return; + } + this.w = 3f * scale; + this.x = x + w - this.w - 1.5f * scale; + this.y = y + 2f * scale; + this.h = Math.max(1f, h - 4f * scale); + thumbH = Math.max(16f * scale, this.h * (this.h / (this.maxScroll + this.h))); + float ratio = this.maxScroll <= 0f ? 0f : (-smoothed / this.maxScroll); + thumbY = this.y + (this.h - thumbH) * AnimationUtility.clamp(ratio, 0f, 1f); + } + + /** Clamp + smooth the offset toward the target for this frame. */ + public void smooth() { + scroll = ClickGuiMath.clamp(scroll, -maxScroll, 0f); + smoothed = AnimationUtility.approach(smoothed, scroll, dragging ? 0.55f : 0.2f); + smoothed = AnimationUtility.snap(smoothed, scroll, dragging ? 0.01f : 0.05f); + } + + public void dragToMouse(float my) { + if (!dragging) return; + if (!visible || maxScroll <= 0f) return; + float span = h - thumbH; + if (span <= 0.5f) return; + float thumbTop = AnimationUtility.clamp(my - dragOffset, y, y + span); + float ratio = (thumbTop - y) / span; + scroll = -maxScroll * AnimationUtility.clamp(ratio, 0f, 1f); + } + + public void draw(float scale, SettingsGuiPalette palette, float openAnim) { + if (!visible) return; + float mx = ClickGuiRenderer.getMouseX(); + float my = ClickGuiRenderer.getMouseY(); + if (hovered(mx, my) || dragging) SystemCursor.set(SystemCursor.CursorType.SCROLL); + + LayoutRender2D.roundedQuad(x, y, w, h, 2f * scale, + LayoutRender2D.alpha(palette.moduleScrollTrackA(), openAnim), + LayoutRender2D.alpha(palette.moduleScrollTrackB(), openAnim), + LayoutRender2D.alpha(palette.moduleScrollTrackB(), openAnim), + LayoutRender2D.alpha(palette.moduleScrollTrackA(), openAnim)); + LayoutRender2D.roundedQuad(x, thumbY, w, thumbH, 2f * scale, + LayoutRender2D.alpha(palette.moduleScrollHandleA(), openAnim), + LayoutRender2D.alpha(palette.moduleScrollHandleB(), openAnim), + LayoutRender2D.alpha(palette.moduleScrollHandleB(), openAnim), + LayoutRender2D.alpha(palette.moduleScrollHandleA(), openAnim)); + } + + /** Body width lost to the scrollbar column, when there is one. */ + public float gutter(float scale) { + return visible ? 7f * scale : 0f; + } + + private boolean hovered(float mx, float my) { + if (!visible) return false; + float pad = 4f; + return ClickGuiMath.insideRect(mx, my, x - pad, y - pad, w + pad * 2f, h + pad * 2f); + } +}