diff options
8 files changed, 239 insertions, 113 deletions
diff --git a/src/client/java/dev/isxander/yacl/api/LabelOption.java b/src/client/java/dev/isxander/yacl/api/LabelOption.java new file mode 100644 index 0000000..a1bbe28 --- /dev/null +++ b/src/client/java/dev/isxander/yacl/api/LabelOption.java @@ -0,0 +1,20 @@ +package dev.isxander.yacl.api; + +import dev.isxander.yacl.impl.LabelOptionImpl; +import net.minecraft.text.Text; + +/** + * A label option is an easier way of creating a label with a {@link dev.isxander.yacl.gui.controllers.LabelController}. + * This option is immutable and cannot be disabled. Tooltips are supported through + * {@link Text} styling. + */ +public interface LabelOption extends Option<Text> { + Text label(); + + /** + * Creates a new label option with the given label. + */ + static LabelOption create(Text label) { + return new LabelOptionImpl(label); + } +} diff --git a/src/client/java/dev/isxander/yacl/api/Option.java b/src/client/java/dev/isxander/yacl/api/Option.java index 406931f..9b4ff7b 100644 --- a/src/client/java/dev/isxander/yacl/api/Option.java +++ b/src/client/java/dev/isxander/yacl/api/Option.java @@ -136,6 +136,7 @@ public interface Option<T> { * * @param tooltipGetter function to get tooltip depending on value {@link Builder#build()}. */ + @SuppressWarnings("unchecked") Builder<T> tooltip(@NotNull Function<T, Text>... tooltipGetter); /** diff --git a/src/client/java/dev/isxander/yacl/gui/OptionListWidget.java b/src/client/java/dev/isxander/yacl/gui/OptionListWidget.java index c18597f..674fc56 100644 --- a/src/client/java/dev/isxander/yacl/gui/OptionListWidget.java +++ b/src/client/java/dev/isxander/yacl/gui/OptionListWidget.java @@ -3,8 +3,6 @@ package dev.isxander.yacl.gui; import com.google.common.collect.ImmutableList; import dev.isxander.yacl.api.*; import dev.isxander.yacl.api.utils.Dimension; -import dev.isxander.yacl.gui.controllers.ListEntryWidget; -import dev.isxander.yacl.impl.ListOptionEntryImpl; import dev.isxander.yacl.impl.utils.YACLConstants; import net.minecraft.client.MinecraftClient; import net.minecraft.client.font.MultilineText; @@ -16,6 +14,7 @@ import net.minecraft.client.gui.screen.narration.NarrationMessageBuilder; import net.minecraft.client.gui.screen.narration.NarrationPart; import net.minecraft.client.util.math.MatrixStack; import net.minecraft.text.Text; +import net.minecraft.util.Formatting; import org.jetbrains.annotations.Nullable; import java.util.*; @@ -27,7 +26,7 @@ public class OptionListWidget extends ElementListWidgetExt<OptionListWidget.Entr private ImmutableList<Entry> viewableChildren; public OptionListWidget(YACLScreen screen, MinecraftClient client, int width, int height) { - super(client, width / 3, 0, width / 3 * 2, height, true); + super(client, width / 3, 0, width / 3 * 2 + 1, height, true); this.yaclScreen = screen; refreshOptions(); @@ -65,7 +64,17 @@ public class OptionListWidget extends ElementListWidgetExt<OptionListWidget.Entr groupSeparatorEntry = null; } - List<OptionEntry> optionEntries = new ArrayList<>(); + List<Entry> optionEntries = new ArrayList<>(); + + // add empty entry to make sure users know it's empty not just bugging out + if (groupSeparatorEntry instanceof ListGroupSeparatorEntry listGroupSeparatorEntry) { + if (listGroupSeparatorEntry.listOption.options().isEmpty()) { + EmptyListLabel emptyListLabel = new EmptyListLabel(listGroupSeparatorEntry, category); + addEntry(emptyListLabel); + optionEntries.add(emptyListLabel); + } + } + for (Option<?> option : group.options()) { OptionEntry entry = new OptionEntry(option, category, group, groupSeparatorEntry, option.controller().provideWidget(yaclScreen, getDefaultEntryDimension())); addEntry(entry); @@ -73,7 +82,7 @@ public class OptionListWidget extends ElementListWidgetExt<OptionListWidget.Entr } if (groupSeparatorEntry != null) { - groupSeparatorEntry.setOptionEntries(optionEntries); + groupSeparatorEntry.setChildEntries(optionEntries); } } } @@ -85,20 +94,22 @@ public class OptionListWidget extends ElementListWidgetExt<OptionListWidget.Entr private void refreshListEntries(ListOption<?> listOption, ConfigCategory category) { // find group separator for group - GroupSeparatorEntry groupSeparator = super.children().stream().filter(e -> e instanceof GroupSeparatorEntry gs && gs.group == listOption).map(GroupSeparatorEntry.class::cast).findAny().orElse(null); + ListGroupSeparatorEntry groupSeparator = super.children().stream().filter(e -> e instanceof ListGroupSeparatorEntry gs && gs.group == listOption).map(ListGroupSeparatorEntry.class::cast).findAny().orElse(null); if (groupSeparator == null) { YACLConstants.LOGGER.warn("Can't find group seperator to refresh list option entries for list option " + listOption.name()); return; } - for (OptionEntry entry : groupSeparator.optionEntries) + for (Entry entry : groupSeparator.childEntries) super.removeEntry(entry); - groupSeparator.optionEntries.clear(); + groupSeparator.childEntries.clear(); // if no entries, below loop won't run where addEntryBelow() recaches viewable children if (listOption.options().isEmpty()) { - recacheViewableChildren(); + EmptyListLabel emptyListLabel; + addEntryBelow(groupSeparator, emptyListLabel = new EmptyListLabel(groupSeparator, category)); + groupSeparator.childEntries.add(emptyListLabel); return; } @@ -106,7 +117,7 @@ public class OptionListWidget extends ElementListWidgetExt<OptionListWidget.Entr for (ListOptionEntry<?> listOptionEntry : listOption.options()) { OptionEntry optionEntry = new OptionEntry(listOptionEntry, category, listOption, groupSeparator, listOptionEntry.controller().provideWidget(yaclScreen, getDefaultEntryDimension())); addEntryBelow(lastEntry, optionEntry); - groupSeparator.optionEntries.add(optionEntry); + groupSeparator.childEntries.add(optionEntry); lastEntry = optionEntry; } } @@ -304,7 +315,7 @@ public class OptionListWidget extends ElementListWidgetExt<OptionListWidget.Entr @Override public boolean isViewable() { - String query = yaclScreen.searchFieldWidget.getText(); + String query = yaclScreen.searchFieldWidget.getQuery(); return (groupSeparatorEntry == null || groupSeparatorEntry.isExpanded()) && (yaclScreen.searchFieldWidget.isEmpty() || (!singleCategory && categoryName.contains(query)) @@ -346,7 +357,7 @@ public class OptionListWidget extends ElementListWidgetExt<OptionListWidget.Entr protected boolean groupExpanded; - protected List<OptionEntry> optionEntries; + protected List<Entry> childEntries = new ArrayList<>(); private int y; @@ -401,13 +412,14 @@ public class OptionListWidget extends ElementListWidgetExt<OptionListWidget.Entr expandMinimizeButton.setMessage(Text.of(isExpanded() ? "▼" : "▶")); } - public void setOptionEntries(List<OptionEntry> optionEntries) { - this.optionEntries = optionEntries; + public void setChildEntries(List<? extends Entry> childEntries) { + this.childEntries.clear(); + this.childEntries.addAll(childEntries); } @Override public boolean isViewable() { - return yaclScreen.searchFieldWidget.isEmpty() || optionEntries.stream().anyMatch(OptionEntry::isViewable); + return yaclScreen.searchFieldWidget.isEmpty() || childEntries.stream().anyMatch(Entry::isViewable); } @Override @@ -506,4 +518,44 @@ public class OptionListWidget extends ElementListWidgetExt<OptionListWidget.Entr return ImmutableList.of(expandMinimizeButton, addListButton, resetListButton); } } + + public class EmptyListLabel extends Entry { + private final ListGroupSeparatorEntry parent; + private final String groupName; + private final String categoryName; + + public EmptyListLabel(ListGroupSeparatorEntry parent, ConfigCategory category) { + this.parent = parent; + this.groupName = parent.group.name().getString().toLowerCase(); + this.categoryName = category.name().getString().toLowerCase(); + } + + @Override + public void render(MatrixStack matrices, int index, int y, int x, int entryWidth, int entryHeight, int mouseX, int mouseY, boolean hovered, float tickDelta) { + drawCenteredText(matrices, MinecraftClient.getInstance().textRenderer, Text.translatable("yacl.list.empty").formatted(Formatting.DARK_GRAY, Formatting.ITALIC), x + entryWidth / 2, y, -1); + } + + @Override + public boolean isViewable() { + String query = yaclScreen.searchFieldWidget.getQuery(); + return parent.isExpanded() && (yaclScreen.searchFieldWidget.isEmpty() + || (!singleCategory && categoryName.contains(query)) + || groupName.contains(query)); + } + + @Override + public int getItemHeight() { + return 11; + } + + @Override + public List<? extends Element> children() { + return ImmutableList.of(); + } + + @Override + public List<? extends Selectable> selectableChildren() { + return ImmutableList.of(); + } + } } diff --git a/src/client/java/dev/isxander/yacl/gui/SearchFieldWidget.java b/src/client/java/dev/isxander/yacl/gui/SearchFieldWidget.java index 5b7c9dc..3cfe75e 100644 --- a/src/client/java/dev/isxander/yacl/gui/SearchFieldWidget.java +++ b/src/client/java/dev/isxander/yacl/gui/SearchFieldWidget.java @@ -48,6 +48,10 @@ public class SearchFieldWidget extends TextFieldWidget { yaclScreen.categoryList.setScrollAmount(0); } + public String getQuery() { + return getText().toLowerCase(); + } + public boolean isEmpty() { return isEmpty; } diff --git a/src/client/java/dev/isxander/yacl/impl/LabelOptionImpl.java b/src/client/java/dev/isxander/yacl/impl/LabelOptionImpl.java new file mode 100644 index 0000000..314c2ad --- /dev/null +++ b/src/client/java/dev/isxander/yacl/impl/LabelOptionImpl.java @@ -0,0 +1,113 @@ +package dev.isxander.yacl.impl; + +import com.google.common.collect.ImmutableSet; +import dev.isxander.yacl.api.*; +import dev.isxander.yacl.gui.controllers.LabelController; +import net.minecraft.text.Text; +import org.jetbrains.annotations.NotNull; + +import java.util.function.BiConsumer; + +public class LabelOptionImpl implements LabelOption { + private final Text label; + private final Text name = Text.literal("Label Option"); + private final Text tooltip = Text.empty(); + private final LabelController labelController; + private final Binding<Text> binding; + + public LabelOptionImpl(Text label) { + this.label = label; + this.labelController = new LabelController(this); + this.binding = Binding.immutable(label); + } + + @Override + public Text label() { + return label; + } + + @Override + public @NotNull Text name() { + return name; + } + + @Override + public @NotNull Text tooltip() { + return tooltip; + } + + @Override + public @NotNull Controller<Text> controller() { + return labelController; + } + + @Override + public @NotNull Binding<Text> binding() { + return binding; + } + + @Override + public boolean available() { + return true; + } + + @Override + public void setAvailable(boolean available) { + throw new UnsupportedOperationException("Label options cannot be disabled."); + } + + @Override + public @NotNull Class<Text> typeClass() { + return Text.class; + } + + @Override + public @NotNull ImmutableSet<OptionFlag> flags() { + return ImmutableSet.of(); + } + + @Override + public boolean changed() { + return false; + } + + @Override + public @NotNull Text pendingValue() { + return label; + } + + @Override + public void requestSet(Text value) { + + } + + @Override + public boolean applyValue() { + return false; + } + + @Override + public void forgetPendingValue() { + + } + + @Override + public void requestSetDefault() { + + } + + @Override + public boolean isPendingValueDefault() { + return true; + } + + @Override + public boolean canResetToDefault() { + return false; + } + + @Override + public void addListener(BiConsumer<Option<Text>, Text> changedListener) { + + } +} diff --git a/src/client/java/dev/isxander/yacl/impl/ListOptionImpl.java b/src/client/java/dev/isxander/yacl/impl/ListOptionImpl.java index 1924205..fb74601 100644 --- a/src/client/java/dev/isxander/yacl/impl/ListOptionImpl.java +++ b/src/client/java/dev/isxander/yacl/impl/ListOptionImpl.java @@ -93,7 +93,7 @@ public final class ListOptionImpl<T> implements ListOption<T> { } @Override - public ImmutableList<T> pendingValue() { + public @NotNull ImmutableList<T> pendingValue() { return ImmutableList.copyOf(entries.stream().map(Option::pendingValue).toList()); } diff --git a/src/main/resources/assets/yet-another-config-lib/lang/en_us.json b/src/main/resources/assets/yet-another-config-lib/lang/en_us.json index 292864f..32621e9 100644 --- a/src/main/resources/assets/yet-another-config-lib/lang/en_us.json +++ b/src/main/resources/assets/yet-another-config-lib/lang/en_us.json @@ -20,6 +20,7 @@ "yacl.list.move_down": "Move down", "yacl.list.remove": "Remove", "yacl.list.add_top": "New entry", + "yacl.list.empty": "List is empty", "yacl.restart.title": "Config requires restart!", "yacl.restart.message": "One or more options needs you to restart the game to apply the changes.", diff --git a/src/testmod/java/dev/isxander/yacl/test/config/GuiTest.java b/src/testmod/java/dev/isxander/yacl/test/config/GuiTest.java index 4965150..1881e3c 100644 --- a/src/testmod/java/dev/isxander/yacl/test/config/GuiTest.java +++ b/src/testmod/java/dev/isxander/yacl/test/config/GuiTest.java @@ -36,20 +36,13 @@ public class GuiTest { .controller(ActionController::new) .action((screen, opt) -> MinecraftClient.getInstance().setScreen(getFullTestSuite(screen))) .build()) - .option(ButtonOption.createBuilder() - .name(Text.of("Basic Wiki Suite")) - .controller(ActionController::new) - .action((screen, opt) -> MinecraftClient.getInstance().setScreen(getWikiBasic(screen))) - .build()) - .option(ButtonOption.createBuilder() - .name(Text.of("Group Wiki Suite")) - .controller(ActionController::new) - .action((screen, opt) -> MinecraftClient.getInstance().setScreen(getWikiGroups(screen))) - .build()) - .option(ButtonOption.createBuilder() - .name(Text.of("Unavailable Test Suite")) - .controller(ActionController::new) - .action((screen, opt) -> MinecraftClient.getInstance().setScreen(getDisabledTest(screen))) + .group(OptionGroup.createBuilder() + .name(Text.of("Wiki")) + .option(ButtonOption.createBuilder() + .name(Text.of("Get Started")) + .controller(ActionController::new) + .action((screen, opt) -> MinecraftClient.getInstance().setScreen(getWikiGetStarted(screen))) + .build()) .build()) .build()) ) @@ -76,7 +69,6 @@ public class GuiTest { ) .controller(BooleanController::new) .flag(OptionFlag.GAME_RESTART) - .available(false) .build()) .option(Option.createBuilder(boolean.class) .name(Text.of("Custom Boolean Toggle")) @@ -89,7 +81,7 @@ public class GuiTest { .controller(opt -> new BooleanController(opt, state -> state ? Text.of("Amazing") : Text.of("Not Amazing"), true)) .build()) .option(Option.createBuilder(boolean.class) - .name(Text.of("Tick Box aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")) + .name(Text.of("Tick Box")) .tooltip(Text.of("There are even alternate methods of displaying the same data type!")) .binding( defaults.tickbox, @@ -102,7 +94,7 @@ public class GuiTest { .group(OptionGroup.createBuilder() .name(Text.of("Slider Controllers")) .option(Option.createBuilder(int.class) - .name(Text.of("Int Slider that is cut off because the slider")) + .name(Text.of("Int Slider")) .instant(true) .binding( defaults.intSlider, @@ -219,16 +211,14 @@ public class GuiTest { .action((screen, opt) -> SystemToast.add(MinecraftClient.getInstance().getToastManager(), SystemToast.Type.TUTORIAL_HINT, Text.of("Button Pressed"), Text.of("Button option was invoked!"))) .controller(ActionController::new) .build()) - .option(Option.createBuilder(Text.class) - .binding(Binding.immutable(Text.empty() + .option(LabelOption.create( + Text.empty() .append(Text.literal("a").styled(style -> style.withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, Text.of("a"))))) .append(Text.literal("b").styled(style -> style.withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, Text.of("b"))))) .append(Text.literal("c c c c c c c c c c c c c c c c c c c c c c c c c c c c c c c c c c c c c c").styled(style -> style.withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, Text.of("c"))))) .append(Text.literal("e").styled(style -> style.withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, Text.of("e"))))) - .styled(style -> style.withClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, "https://isxander.dev"))) - )) - .controller(LabelController::new) - .build()) + .styled(style -> style.withClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, "https://isxander.dev")))) + ) .build()) .group(OptionGroup.createBuilder() .name(Text.of("Minecraft Bindings")) @@ -410,81 +400,26 @@ public class GuiTest { .generateScreen(parent); } - private static Screen getDisabledTest(Screen parent) { - return YetAnotherConfigLib.create(ExampleConfig.INSTANCE, (defaults, config, builder) -> builder - .title(Text.empty()) - .category(ConfigCategory.createBuilder() - .name(Text.of("Disabled Test")) - .option(Option.createBuilder(int.class) - .name(Text.of("Slider")) - .binding(Binding.immutable(0)) - .controller(opt -> new IntegerSliderController(opt, 0, 5, 1)) - .available(false) - .build()) - .option(Option.createBuilder(boolean.class) - .name(Text.of("Tick Box")) - .binding(Binding.immutable(true)) - .controller(TickBoxController::new) - .available(false) - .build()) - .option(Option.createBuilder(boolean.class) - .name(Text.of("Tick Box (Enabled)")) - .binding(Binding.immutable(true)) - .controller(TickBoxController::new) - .build()) - .option(Option.createBuilder(String.class) - .name(Text.of("Text Field")) - .binding(Binding.immutable("hi")) - .controller(StringController::new) - .available(false) - .build()) - .build()) - ) - .generateScreen(parent); - } + private static boolean myBooleanOption = true; - private static Screen getWikiBasic(Screen parent) { - return YetAnotherConfigLib.create(ExampleConfig.INSTANCE, (defaults, config, builder) -> builder - .title(Text.of("Mod Name")) - .category(ConfigCategory.createBuilder() - .name(Text.of("My Category")) - .tooltip(Text.of("This displays when you hover over a category button")) // optional - .option(Option.createBuilder(boolean.class) - .name(Text.of("My Boolean Option")) - .tooltip(Text.of("This option displays the basic capabilities of YetAnotherConfigLib")) // optional - .binding( - defaults.booleanToggle, // default - () -> config.booleanToggle, // getter - newValue -> config.booleanToggle = newValue // setter - ) - .controller(BooleanController::new) - .build()) - .build()) - ) - .generateScreen(parent); - } - - private static Screen getWikiGroups(Screen parent) { - return YetAnotherConfigLib.create(ExampleConfig.INSTANCE, (defaults, config, builder) -> builder - .title(Text.of("Mod Name")) - .category(ConfigCategory.createBuilder() - .name(Text.of("My Category")) - .tooltip(Text.of("This displays when you hover over a category button")) // optional - .group(OptionGroup.createBuilder() - .name(Text.of("Option Group")) - .option(Option.createBuilder(boolean.class) - .name(Text.of("My Boolean Option")) - .tooltip(Text.of("This option displays the basic capabilities of YetAnotherConfigLib")) // optional - .binding( - defaults.booleanToggle, // default - () -> config.booleanToggle, // getter - newValue -> config.booleanToggle = newValue // setter - ) - .controller(BooleanController::new) - .build()) - .build()) - .build()) - ) + private static Screen getWikiGetStarted(Screen parent) { + return YetAnotherConfigLib.createBuilder() + .title(Text.literal("Used for narration. Could be used to render a title in the future.")) + .category(ConfigCategory.createBuilder() + .name(Text.literal("Name of the category")) + .tooltip(Text.literal("This text will appear as a tooltip when you hover or focus the button with Tab. There is no need to add \n to wrap as YACL will do it for you.")) + .group(OptionGroup.createBuilder() + .name(Text.literal("Name of the group")) + .tooltip(Text.literal("This text will appear when you hover over the name or focus on the collapse button with Tab.")) + .option(Option.createBuilder(boolean.class) + .name(Text.literal("Boolean Option")) + .tooltip(Text.literal("This text will appear as a tooltip when you hover over the option.")) + .binding(true, () -> myBooleanOption, newVal -> myBooleanOption = newVal) + .controller(TickBoxController::new) + .build()) + .build()) + .build()) + .build() .generateScreen(parent); } } |