From 6413392f1e332e8844d7f9fc7649101de3fe5223 Mon Sep 17 00:00:00 2001 From: yugecin Date: Sun, 28 May 2017 16:11:05 +0200 Subject: [PATCH] add navigation to optionmenu --- .../opsudance/core/DisplayContainer.java | 4 + .../opsudance/options/OptionGroups.java | 22 +- src/yugecin/opsudance/options/OptionTab.java | 10 + src/yugecin/opsudance/ui/OptionsOverlay.java | 208 ++++++++++++++++-- 4 files changed, 214 insertions(+), 30 deletions(-) diff --git a/src/yugecin/opsudance/core/DisplayContainer.java b/src/yugecin/opsudance/core/DisplayContainer.java index db5dc75c..0a25d960 100644 --- a/src/yugecin/opsudance/core/DisplayContainer.java +++ b/src/yugecin/opsudance/core/DisplayContainer.java @@ -465,6 +465,10 @@ public class DisplayContainer implements ErrorDumpable, ResolutionChangedListene return (Sys.getTime() * 1000) / Sys.getTimerResolution(); } + public boolean isWidescreen() { + return width * 1000 / height == 1777; // 16:9 + } + @Override public void writeErrorDump(StringWriter dump) { dump.append("> DisplayContainer dump\n"); diff --git a/src/yugecin/opsudance/options/OptionGroups.java b/src/yugecin/opsudance/options/OptionGroups.java index 089865e9..e8d40d00 100644 --- a/src/yugecin/opsudance/options/OptionGroups.java +++ b/src/yugecin/opsudance/options/OptionGroups.java @@ -18,12 +18,14 @@ package yugecin.opsudance.options; +import itdelatrisu.opsu.GameImage; + import static yugecin.opsudance.options.Options.*; public class OptionGroups { public static final OptionTab[] normalOptions = new OptionTab[] { - new OptionTab("GENERAL", null), + new OptionTab("General", GameImage.SEARCH), new OptionTab("GENERAL", new Option[]{ OPTION_DISABLE_UPDATER, OPTION_ENABLE_WATCH_SERVICE @@ -31,7 +33,7 @@ public class OptionGroups { new OptionTab("LANGUAGE", new Option[]{ OPTION_SHOW_UNICODE, }), - new OptionTab("GRAPHICS", null), + new OptionTab("Graphics", GameImage.SEARCH), new OptionTab("RENDERER", new Option[] { OPTION_SCREEN_RESOLUTION, OPTION_ALLOW_LARGER_RESOLUTIONS, @@ -54,7 +56,7 @@ public class OptionGroups { OPTION_DANCING_CIRCLES, OPTION_DANCING_CIRCLES_MULTIPLIER, }), - new OptionTab("SKIN", null), + new OptionTab("Skin", GameImage.SEARCH), new OptionTab("SKIN", new Option[]{ OPTION_SKIN, OPTION_IGNORE_BEATMAP_SKINS, @@ -69,7 +71,7 @@ public class OptionGroups { OPTION_DISABLE_CURSOR // TODO use combo colour as tint for slider ball option }), - new OptionTab("AUDIO", null), + new OptionTab("Audio", GameImage.SEARCH), new OptionTab("VOLUME", new Option[]{ OPTION_MASTER_VOLUME, OPTION_MUSIC_VOLUME, @@ -82,7 +84,7 @@ public class OptionGroups { OPTION_DISABLE_SOUNDS, OPTION_ENABLE_THEME_SONG }), - new OptionTab("GAMEPLAY", null), + new OptionTab("Gameplay", GameImage.SEARCH), new OptionTab("GENERAL", new Option[] { OPTION_BACKGROUND_DIM, OPTION_FORCE_DEFAULT_PLAYFIELD, @@ -96,7 +98,7 @@ public class OptionGroups { OPTION_MAP_END_DELAY, OPTION_EPILEPSY_WARNING, }), - new OptionTab("INPUT", null), + new OptionTab("Input", GameImage.SEARCH), new OptionTab("KEY MAPPING", new Option[]{ OPTION_KEY_LEFT, OPTION_KEY_RIGHT, @@ -105,7 +107,7 @@ public class OptionGroups { OPTION_DISABLE_MOUSE_WHEEL, OPTION_DISABLE_MOUSE_BUTTONS, }), - new OptionTab("CUSTOM", null), + new OptionTab("Custom", GameImage.SEARCH), new OptionTab("DIFFICULTY", new Option[]{ OPTION_FIXED_CS, OPTION_FIXED_HP, @@ -116,7 +118,7 @@ public class OptionGroups { OPTION_CHECKPOINT, OPTION_REPLAY_SEEKING, }), - new OptionTab("DANCE", null), + new OptionTab("Dance", GameImage.SEARCH), new OptionTab("MOVER", new Option[]{ OPTION_DANCE_MOVER, OPTION_DANCE_EXGON_DELAY, @@ -143,7 +145,7 @@ public class OptionGroups { new OptionTab("MIRROR", new Option[] { OPTION_DANCE_MIRROR, }), - new OptionTab("ADVANCED DISPLAY", null), + new OptionTab("Advanced Display", GameImage.SEARCH), new OptionTab("OBJECTS", new Option[]{ OPTION_DANCE_DRAW_APPROACH, OPTION_DANCE_OBJECT_COLOR_OVERRIDE, @@ -163,7 +165,7 @@ public class OptionGroups { OPTION_DANCE_REMOVE_BG, OPTION_DANCE_ENABLE_SB, }), - new OptionTab ("PIPPI", null), + new OptionTab ("Pippi", GameImage.SEARCH), new OptionTab ("GENERAL", new Option[]{ OPTION_PIPPI_ENABLE, OPTION_PIPPI_RADIUS_PERCENT, diff --git a/src/yugecin/opsudance/options/OptionTab.java b/src/yugecin/opsudance/options/OptionTab.java index 7db8dcd4..4c9b66c4 100644 --- a/src/yugecin/opsudance/options/OptionTab.java +++ b/src/yugecin/opsudance/options/OptionTab.java @@ -17,15 +17,25 @@ */ package yugecin.opsudance.options; +import itdelatrisu.opsu.GameImage; + public class OptionTab { public final String name; public final Option[] options; + public final GameImage icon; public boolean filtered; + public OptionTab(String name, GameImage icon) { + this.name = name; + this.icon = icon; + this.options = null; + } + public OptionTab(String name, Option[] options) { this.name = name; this.options = options; + this.icon = null; } } diff --git a/src/yugecin/opsudance/ui/OptionsOverlay.java b/src/yugecin/opsudance/ui/OptionsOverlay.java index 736284e3..87c8914e 100644 --- a/src/yugecin/opsudance/ui/OptionsOverlay.java +++ b/src/yugecin/opsudance/ui/OptionsOverlay.java @@ -49,6 +49,12 @@ public class OptionsOverlay extends OverlayOpsuState { private static final Color COL_GREY = new Color(55, 55, 57); private static final Color COL_BLUE = new Color(Colors.BLUE_BACKGROUND); private static final Color COL_COMBOBOX_HOVER = new Color(185, 19, 121); + private static final Color COL_NAV_BG = new Color(COL_BG); + private static final Color COL_NAV_INDICATOR = new Color(COL_PINK); + private static final Color COL_NAV_WHITE = new Color(COL_WHITE); + private static final Color COL_NAV_FILTERED = new Color(37, 37, 37); + private static final Color COL_NAV_INACTIVE = new Color(153, 153, 153); + private static final Color COL_NAV_FILTERED_HOVERED = new Color(58, 58, 58); private static final float INDICATOR_ALPHA = 0.8f; private static final Color COL_INDICATOR = new Color(Color.black); @@ -84,6 +90,8 @@ public class OptionsOverlay extends OverlayOpsuState { private Image searchImg; private OptionTab[] sections; + private OptionTab activeSection; + private OptionTab hoveredNavigationEntry; private Option hoverOption; private Option selectedOption; @@ -102,6 +110,13 @@ public class OptionsOverlay extends OverlayOpsuState { private int width; private int height; + private int navButtonSize; + private int navStartY; + private int navExpadedWidth; + private int navWidth; + private int navHoverTime; + private int navIndicatorSize; + private int optionWidth; private int optionStartX; private int optionStartY; @@ -166,8 +181,13 @@ public class OptionsOverlay extends OverlayOpsuState { height = displayContainer.height; // calculate positions + float navIconWidthRatio = displayContainer.isWidescreen() ? 0.046875f : 0.065f; + // non-widescreen ratio is not accurate + navButtonSize = (int) (displayContainer.width * navIconWidthRatio); + navIndicatorSize = navButtonSize / 10; + navExpadedWidth = (int) (finalWidth * 0.45f) - navButtonSize; paddingRight = (int) (displayContainer.width * 0.009375f); // not so accurate - paddingLeft = (int) (displayContainer.width * 0.0180f); // not so accurate + paddingLeft = navButtonSize + (int) (displayContainer.width * 0.0180f); // not so accurate paddingTextLeft = paddingLeft + LINEWIDTH + (int) (displayContainer.width * 0.00625f); // not so accurate optionStartX = paddingTextLeft; textOptionsY = Fonts.LARGE.getLineHeight() * 2; @@ -191,9 +211,11 @@ public class OptionsOverlay extends OverlayOpsuState { checkOnImg = GameImage.CONTROL_CHECK_ON.getImage().getScaledCopy(controlImageSize, controlImageSize); checkOffImg = GameImage.CONTROL_CHECK_OFF.getImage().getScaledCopy(controlImageSize, controlImageSize); + int navTotalHeight = 0; dropdownMenus.clear(); for (OptionTab section : sections) { if (section.options == null) { + navTotalHeight += navButtonSize; continue; } for (final Option option : section.options) { @@ -229,6 +251,7 @@ public class OptionsOverlay extends OverlayOpsuState { dropdownMenus.put(listOption, menu); } } + navStartY = (height - navTotalHeight) / 2; int searchImgSize = (int) (Fonts.LARGE.getLineHeight() * 0.75f); searchImg = GameImage.SEARCH.getImage().getScaledCopy(searchImgSize, searchImgSize); @@ -236,11 +259,11 @@ public class OptionsOverlay extends OverlayOpsuState { @Override public void onRender(Graphics g) { - g.setClip(0, 0, width, height); + g.setClip(navButtonSize, 0, width - navButtonSize, height); // bg g.setColor(COL_BG); - g.fillRect(0, 0, width, height); + g.fillRect(navButtonSize, 0, width, height); // title renderTitle(); @@ -263,6 +286,8 @@ public class OptionsOverlay extends OverlayOpsuState { g.fillRect(width - 5, scrollHandler.getPosition() / maxScrollOffset * (height - 45), 5, 45); g.clearClip(); + renderNavigation(g); + // UI UI.getBackButton().draw(g); @@ -275,6 +300,57 @@ public class OptionsOverlay extends OverlayOpsuState { } } + private void renderNavigation(Graphics g) { + navWidth = navButtonSize; + if (navHoverTime >= 600) { + navWidth += navExpadedWidth; + } else if (navHoverTime > 300) { + AnimationEquation anim = AnimationEquation.IN_EXPO; + if (displayContainer.mouseX < navWidth) { + anim = AnimationEquation.OUT_EXPO; + } + float progress = anim.calc((navHoverTime - 300f) / 300f); + navWidth += (int) (progress * navExpadedWidth); + } + + g.setClip(0, 0, navWidth, height); + g.setColor(COL_NAV_BG); + g.fillRect(0, 0, navWidth, displayContainer.height); + int y = navStartY; + float iconSize = navButtonSize / 2.5f; + float iconPadding = iconSize * 0.75f; + int fontOffsetX = navButtonSize + navIndicatorSize; + int fontOffsetY = (navButtonSize - Fonts.MEDIUM.getLineHeight()) / 2; + for (OptionTab section : sections) { + if (section.icon == null) { + continue; + } + Color iconCol = COL_NAV_INACTIVE; + Color fontCol = COL_NAV_WHITE; + if (section == activeSection) { + iconCol = COL_NAV_WHITE; + g.fillRect(0, y, navWidth, navButtonSize); + g.setColor(COL_NAV_INDICATOR); + g.fillRect(navWidth - navIndicatorSize, y, navIndicatorSize, navButtonSize); + } else if (section == hoveredNavigationEntry) { + iconCol = COL_NAV_WHITE; + } + if (section.filtered) { + iconCol = fontCol = COL_NAV_FILTERED; + if (section == hoveredNavigationEntry) { + iconCol = COL_NAV_FILTERED_HOVERED; + } + } + section.icon.getImage().draw(iconPadding, y + iconPadding, iconSize, iconSize, iconCol); + if (navHoverTime > 0) { + Fonts.MEDIUM.drawString(fontOffsetX, y + fontOffsetY, section.name, fontCol); + } + y += navButtonSize; + } + + g.clearClip(); + } + private void renderIndicator(Graphics g) { g.setColor(COL_INDICATOR); int indicatorPos = this.indicatorPos; @@ -289,7 +365,7 @@ public class OptionsOverlay extends OverlayOpsuState { indicatorPos += AnimationEquation.OUT_BACK.calc((float) indicatorMoveAnimationTime / INDICATORMOVEANIMATIONTIME) * indicatorOffsetToNextPos; } } - g.fillRect(0, indicatorPos - scrollHandler.getPosition(), width, optionHeight); + g.fillRect(navButtonSize, indicatorPos - scrollHandler.getPosition(), width, optionHeight); } private void renderKeyEntry(Graphics g) { @@ -324,12 +400,12 @@ public class OptionsOverlay extends OverlayOpsuState { continue; } int lineStartY = (int) (y + Fonts.LARGE.getLineHeight() * 0.6f); - if (render) { - if (section.options == null) { - FontUtil.drawRightAligned(Fonts.XLARGE, width, -paddingRight, (int) (y + Fonts.XLARGE.getLineHeight() * 0.3f), section.name, COL_CYAN); - } else { - Fonts.MEDIUMBOLD.drawString(paddingTextLeft, lineStartY, section.name, COL_WHITE); - } + if (section.options == null) { + FontUtil.drawRightAligned(Fonts.XLARGE, width, -paddingRight, + (int) (y + Fonts.XLARGE.getLineHeight() * 0.3f), section.name.toUpperCase(), + COL_CYAN); + } else { + Fonts.MEDIUMBOLD.drawString(paddingTextLeft, lineStartY, section.name, COL_WHITE); } y += sectionLineHeight; maxScrollOffset += sectionLineHeight; @@ -479,8 +555,11 @@ public class OptionsOverlay extends OverlayOpsuState { } private void renderTitle() { - FontUtil.drawCentered(Fonts.LARGE, width, 0, textOptionsY - scrollHandler.getIntPosition(), "Options", COL_WHITE); - FontUtil.drawCentered(Fonts.MEDIUM, width, 0, textChangeY - scrollHandler.getIntPosition(), "Change the way opsu! behaves", COL_PINK); + int textWidth = width - navButtonSize; + FontUtil.drawCentered(Fonts.LARGE, textWidth, navButtonSize, + textOptionsY - scrollHandler.getIntPosition(), "Options", COL_WHITE); + FontUtil.drawCentered(Fonts.MEDIUM, textWidth, navButtonSize, + textChangeY - scrollHandler.getIntPosition(), "Change the way opsu! behaves", COL_PINK); } private void renderSearch(Graphics g) { @@ -488,14 +567,14 @@ public class OptionsOverlay extends OverlayOpsuState { if (scrollHandler.getIntPosition() > posSearchY) { ypos = textSearchYOffset; g.setColor(COL_BG); - g.fillRect(0, 0, width, textSearchYOffset * 2 + Fonts.LARGE.getLineHeight()); + g.fillRect(navButtonSize, 0, width, textSearchYOffset * 2 + Fonts.LARGE.getLineHeight()); } String searchText = "Type to search!"; if (lastSearchText.length() > 0) { searchText = lastSearchText; } - FontUtil.drawCentered(Fonts.LARGE, width, 0, ypos, searchText, COL_WHITE); - int imgPosX = (width - Fonts.LARGE.getWidth(searchText)) / 2 - searchImg.getWidth() - 10; + FontUtil.drawCentered(Fonts.LARGE, width, navButtonSize, ypos, searchText, COL_WHITE); + int imgPosX = navButtonSize + (width - Fonts.LARGE.getWidth(searchText)) / 2 - searchImg.getWidth() - 10; searchImg.draw(imgPosX, ypos + Fonts.LARGE.getLineHeight() * 0.25f, COL_WHITE); } @@ -510,6 +589,7 @@ public class OptionsOverlay extends OverlayOpsuState { @Override public void show() { + navHoverTime = 0; indicatorPos = -optionHeight; indicatorOffsetToNextPos = 0; indicatorMoveAnimationTime = 0; @@ -546,10 +626,20 @@ public class OptionsOverlay extends OverlayOpsuState { sliderSoundDelay -= delta; } + if (mouseX < navWidth) { + if (navHoverTime < 600) { + navHoverTime += delta; + } + } else if (navHoverTime > 0) { + navHoverTime -= delta; + } + if (mouseX - prevMouseX == 0 && mouseY - prevMouseY == 0) { updateIndicatorAlpha(); return; } + updateActiveSection(); + updateHoverNavigation(mouseX, mouseY); prevMouseX = mouseX; prevMouseY = mouseY; updateHoverOption(mouseX, mouseY); @@ -565,6 +655,24 @@ public class OptionsOverlay extends OverlayOpsuState { } } + private void updateHoverNavigation(int mouseX, int mouseY) { + hoveredNavigationEntry = null; + if (mouseX >= navWidth) { + return; + } + int y = navStartY; + for (OptionTab section : sections) { + if (section.options != null) { + continue; + } + int nextY = y + navButtonSize; + if (y <= mouseY && mouseY < nextY) { + hoveredNavigationEntry = section; + } + y = nextY; + } + } + private void updateIndicatorAlpha() { if (hoverOption == null) { if (indicatorHideAnimationTime < INDICATORHIDEANIMATIONTIME) { @@ -594,20 +702,28 @@ public class OptionsOverlay extends OverlayOpsuState { // if acceptInput is false, it means that we're currently hiding ourselves float progress; + // navigation elemenst fade out with a different animation + float navProgress; if (acceptInput) { animationtime += delta; if (animationtime >= SHOWANIMATIONTIME) { animationtime = SHOWANIMATIONTIME; } - progress = AnimationEquation.OUT_EXPO.calc((float) animationtime / SHOWANIMATIONTIME); + progress = (float) animationtime / SHOWANIMATIONTIME; + navProgress = Utils.clamp(progress * 10f, 0f, 1f); + progress = AnimationEquation.OUT_EXPO.calc(progress); } else { animationtime -= delta; if (animationtime < 0) { animationtime = 0; } - progress = hideAnimationStartProgress * AnimationEquation.IN_EXPO.calc((float) animationtime / hideAnimationTime); + progress = (float) animationtime / hideAnimationTime; + navProgress = hideAnimationStartProgress * AnimationEquation.IN_CIRC.calc(progress); + progress = hideAnimationStartProgress * AnimationEquation.IN_EXPO.calc(progress); } - width = (int) (progress * finalWidth); + width = navButtonSize + (int) (progress * (finalWidth - navButtonSize)); + COL_NAV_FILTERED.a = COL_NAV_INACTIVE.a = COL_NAV_FILTERED_HOVERED.a = COL_NAV_INDICATOR.a = + COL_NAV_WHITE.a = COL_NAV_BG.a = navProgress; COL_BG.a = BG_ALPHA * progress; COL_WHITE.a = progress; COL_PINK.a = progress; @@ -692,6 +808,28 @@ public class OptionsOverlay extends OverlayOpsuState { } } + if (hoveredNavigationEntry != null && !hoveredNavigationEntry.filtered) { + int sectionPosition = 0; + for (OptionTab section : sections) { + if (section == hoveredNavigationEntry) { + break; + } + if (section.filtered) { + continue; + } + sectionPosition += sectionLineHeight; + if (section.options == null) { + continue; + } + for (Option option : section.options) { + if (!option.isFiltered() && option.showCondition()) { + sectionPosition += optionHeight; + } + } + } + scrollHandler.scrollToPosition(sectionPosition); + } + if (UI.getBackButton().contains(x, y)){ hide(); if (listener != null) { @@ -776,6 +914,33 @@ public class OptionsOverlay extends OverlayOpsuState { o.setValue(Utils.clamp(value, o.min, o.max)); } + private void updateActiveSection() { + // active section is the one that is visible in the top half of the screen + activeSection = sections[0]; + int virtualY = optionStartY; + for (int sectionIndex = 0; sectionIndex < sections.length; sectionIndex++) { + OptionTab section = sections[sectionIndex]; + if (section.filtered) { + continue; + } + virtualY += sectionLineHeight; + if (virtualY > scrollHandler.getPosition() + height / 2) { + return; + } + if (section.options == null) { + activeSection = section; + continue; + } + for (int optionIndex = 0; optionIndex < section.options.length; optionIndex++) { + Option option = section.options[optionIndex]; + if (option.isFiltered() || !option.showCondition()) { + continue; + } + virtualY += optionHeight; + } + } + } + private void updateHoverOption(int mouseX, int mouseY) { if (openDropdownMenu != null || keyEntryLeft || keyEntryRight) { return; @@ -805,7 +970,7 @@ public class OptionsOverlay extends OverlayOpsuState { continue; } if (mouseVirtualY <= optionHeight) { - if (mouseVirtualY >= 0) { + if (mouseX > navWidth && mouseVirtualY >= 0) { int indicatorPos = scrollHandler.getIntPosition() + mouseY - mouseVirtualY; if (indicatorPos != this.indicatorPos + indicatorOffsetToNextPos) { this.indicatorPos += indicatorOffsetToNextPos; // finish the current moving animation @@ -843,7 +1008,9 @@ public class OptionsOverlay extends OverlayOpsuState { if (section.options == null) { lastBigSectionMatches = sectionMatches; lastBigSection = section; - section.filtered = true; + if (!lastBigSectionMatches) { + section.filtered = true; + } continue; } section.filtered = true; @@ -855,6 +1022,7 @@ public class OptionsOverlay extends OverlayOpsuState { } if (!option.filter(lastSearchText)) { section.filtered = false; + //noinspection ConstantConditions lastBigSection.filtered = false; } }