Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
164 changes: 164 additions & 0 deletions common/src/main/java/dev/isxander/yacl3/api/MapOption.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
package dev.isxander.yacl3.api;

import com.google.common.collect.ImmutableList;
import dev.isxander.yacl3.api.controller.ControllerBuilder;
import dev.isxander.yacl3.impl.MapOptionImpl;
import net.minecraft.network.chat.Component;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;

import java.util.Collection;
import java.util.Map;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;

public interface MapOption<S, T> extends OptionGroup, Option<Map<S, T>> {
@Override
@NotNull ImmutableList<MapOptionEntry<S, T>> options();

@ApiStatus.Internal
int numberOfEntries();

@ApiStatus.Internal
int maximumNumberOfEntries();

@ApiStatus.Internal
int minimumNumberOfEntries();

@ApiStatus.Internal
MapOptionEntry<S, T> insertNewEntry();

@ApiStatus.Internal
void insertEntry(int index, MapOptionEntry<?, ?> entry);

@ApiStatus.Internal
int indexOf(MapOptionEntry<?, ?> entry);

@ApiStatus.Internal
void removeEntry(MapOptionEntry<?, ?> entry);

@ApiStatus.Internal
void addRefreshListener(Runnable changedListener);

static <S, T> Builder<S, T> createBuilder() {
return new MapOptionImpl.BuilderImpl<>();
}

interface Builder<S, T> {
/**
* Sets name of the list, for UX purposes, a name should always be given,
* but isn't enforced.
*
* @see ListOption#name()
*/
Builder<S, T> name(@NotNull Component name);

Builder<S, T> description(@NotNull OptionDescription description);

/**
* Sets the value that is used when creating new entries
*/
Builder<S, T> initial(@NotNull Supplier<Map.Entry<S, T>> initialValue);

/**
* Sets the value that is used when creating new entries
*/
Builder<S, T> initial(@NotNull Map.Entry<S, T> initialValue);

Builder<S, T> keyController(@NotNull Function<MapOptionEntry<S, T>, ControllerBuilder<S>> controller);

Builder<S, T> valueController(@NotNull Function<MapOptionEntry<S, T>, ControllerBuilder<T>> controller);

/**
* Sets the controller for the option.
* This is how you interact and change the options.
*
* @see dev.isxander.yacl3.gui.controllers
*/
Builder<S, T> customController(@NotNull Function<MapOptionEntry<S, T>, Controller<S>> keyControl, @NotNull Function<MapOptionEntry<S, T>, Controller<T>> valueControl);

/**
* Sets the binding for the option.
* Used for default, getter and setter.
*
* @see Binding
*/
Builder<S, T> binding(@NotNull Binding<Map<S, T>> binding);

/**
* Sets the binding for the option.
* Shorthand of {@link Binding#generic(Object, Supplier, Consumer)}
*
* @param def default value of the option, used to reset
* @param getter should return the current value of the option
* @param setter should set the option to the supplied value
* @see Binding
*/
Builder<S, T> binding(@NotNull Map<S, T> def, @NotNull Supplier<@NotNull Map<S, T>> getter, @NotNull Consumer<@NotNull Map<S, T>> setter);

/**
* Sets if the option can be configured
*
* @see Option#available()
*/
Builder<S, T> available(boolean available);

/**
* Sets a minimum size for the list. Once this size is reached,
* no further entries may be removed.
*/
Builder<S, T> minimumNumberOfEntries(int number);

/**
* Sets a maximum size for the list. Once this size is reached,
* no further entries may be added.
*/
Builder<S, T> maximumNumberOfEntries(int number);

/**
* Dictates if new entries should be added to the end of the list
* rather than the top.
*/
Builder<S, T> insertEntriesAtEnd(boolean insertAtEnd);

/**
* Adds a flag to the option.
* Upon applying changes, all flags are executed.
* {@link Option#flags()}
*/
Builder<S, T> flag(@NotNull OptionFlag... flag);

/**
* Adds a flag to the option.
* Upon applying changes, all flags are executed.
* {@link Option#flags()}
*/
Builder<S, T> flags(@NotNull Collection<OptionFlag> flags);

/**
* Dictates if the group should be collapsed by default.
* If not set, it will not be collapsed by default.
*
* @see OptionGroup#collapsed()
*/
Builder<S, T> collapsed(boolean collapsible);

/**
* Adds a listener to the option. Invoked upon changing any of the list's entries.
*
* @see Option#addListener(BiConsumer)
*/
Builder<S, T> listener(@NotNull BiConsumer<Option<Map<S, T>>, Map<S, T>> listener);

/**
* Adds multiple listeners to the option. Invoked upon changing of any of the list's entries.
*
* @see Option#addListener(BiConsumer)
*/
Builder<S, T> listeners(@NotNull Collection<BiConsumer<Option<Map<S, T>>, Map<S, T>>> listeners);

MapOption<S, T> build();
}
}
18 changes: 18 additions & 0 deletions common/src/main/java/dev/isxander/yacl3/api/MapOptionEntry.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package dev.isxander.yacl3.api;

import com.google.common.collect.ImmutableSet;
import org.jetbrains.annotations.NotNull;

public interface MapOptionEntry<S, T> extends Option<T> {
MapOption<S, T> parentGroup();

@Override
default @NotNull ImmutableSet<OptionFlag> flags() {
return parentGroup().flags();
}

@Override
default boolean available() {
return parentGroup().available();
}
}
154 changes: 151 additions & 3 deletions common/src/main/java/dev/isxander/yacl3/gui/OptionListWidget.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import net.minecraft.client.gui.narration.NarrationElementOutput;
import net.minecraft.client.gui.screens.Screen;
import net.minecraft.network.chat.Component;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.ArrayList;
Expand All @@ -41,6 +42,8 @@ public OptionListWidget(YACLScreen screen, ConfigCategory category, Minecraft cl
for (OptionGroup group : category.groups()) {
if (group instanceof ListOption<?> listOption) {
listOption.addRefreshListener(() -> refreshListEntries(listOption, category));
} else if (group instanceof MapOption<?, ?> mapOption) {
mapOption.addRefreshListener(() -> refreshMapEntries(mapOption, category));
}
}
}
Expand All @@ -51,9 +54,14 @@ public void refreshOptions() {
for (OptionGroup group : category.groups()) {
GroupSeparatorEntry groupSeparatorEntry;
if (!group.isRoot()) {
groupSeparatorEntry = group instanceof ListOption<?> listOption
? new ListGroupSeparatorEntry(listOption, yaclScreen)
: new GroupSeparatorEntry(group, yaclScreen);
if (group instanceof ListOption<?> listOption) {
groupSeparatorEntry = new ListGroupSeparatorEntry(listOption, yaclScreen);
} else if (group instanceof MapOption<?, ?> mapOption) {
groupSeparatorEntry = new MapGroupSeparatorEntry(mapOption, yaclScreen);
} else {
groupSeparatorEntry = new GroupSeparatorEntry(group, yaclScreen);
}

addEntry(groupSeparatorEntry);
} else {
groupSeparatorEntry = null;
Expand Down Expand Up @@ -116,6 +124,44 @@ private void refreshListEntries(ListOption<?> listOption, ConfigCategory categor
}
}

private void refreshMapEntries(MapOption<?, ?> mapOption, ConfigCategory category) {
// Find group separator for group
OptionListWidget.MapGroupSeparatorEntry groupSeparator = super.children().stream().filter(
entry -> entry instanceof OptionListWidget.MapGroupSeparatorEntry mapGroupSeparatorEntry && mapGroupSeparatorEntry.group == mapOption).map(
OptionListWidget.MapGroupSeparatorEntry.class::cast).findAny().orElse(null);

if (groupSeparator == null) {
YACLConstants.LOGGER.warn("Can't find group separator to refresh map option entries for map option " + mapOption.name());
return;
}

var groupSeparatorChildEntries = groupSeparator.childEntries;
for (OptionListWidget.Entry entry : groupSeparatorChildEntries)
super.removeEntry(entry);
groupSeparatorChildEntries.clear();

// If no entries, below loop won't run where addEntryBelow() reaches viewable children
if (mapOption.options().isEmpty()) {
OptionListWidget.EmptyMapLabel emptyMapLabel = new EmptyMapLabel(groupSeparator, category);

addEntryBelow(groupSeparator, emptyMapLabel);
groupSeparatorChildEntries.add(emptyMapLabel);

return;
}

OptionListWidget.Entry lastEntry = groupSeparator;
for (MapOptionEntry<?, ?> mapOptionEntry : mapOption.options()) {
OptionListWidget.OptionEntry optionEntry = new OptionEntry(mapOptionEntry, category, mapOption, groupSeparator,
mapOptionEntry.controller().provideWidget(yaclScreen, getDefaultEntryDimension())
);

addEntryBelow(lastEntry, optionEntry);
groupSeparatorChildEntries.add(optionEntry);
lastEntry = optionEntry;
}
}

public Dimension<Integer> getDefaultEntryDimension() {
return Dimension.ofInt(getRowLeft(), 0, getRowWidth(), 20);
}
Expand Down Expand Up @@ -533,6 +579,71 @@ public List<? extends GuiEventListener> children() {
}
}

public class MapGroupSeparatorEntry extends GroupSeparatorEntry {
private final MapOption<?, ?> mapOption;
private final TextScaledButtonWidget resetListButton;
private final TooltipButtonWidget addListButton;

private MapGroupSeparatorEntry(MapOption<?, ?> group, Screen screen) {
super(group, screen);
this.mapOption = group;

this.resetListButton = new TextScaledButtonWidget(screen, getRowRight() - 20, -50, 20, 20, 2f, Component.literal("\u21BB"), button -> {
group.requestSetDefault();
});
group.addListener((opt, val) -> this.resetListButton.active = !opt.isPendingValueDefault() && opt.available());
this.resetListButton.active = !group.isPendingValueDefault() && group.available();


this.addListButton = new TooltipButtonWidget(yaclScreen, resetListButton.getX() - 20, -50, 20, 20, Component.literal("+"), Component.translatable("yacl.list.add_top"), btn -> {
group.insertNewEntry();
setExpanded(true);
});

updateExpandMinimizeText();
minimizeIfUnavailable();
}

@Override
public void render(GuiGraphics graphics, int index, int y, int x, int entryWidth, int entryHeight, int mouseX, int mouseY, boolean hovered, float tickDelta) {
updateExpandMinimizeText(); // update every render because option could become available/unavailable at any time

super.render(graphics, index, y, x, entryWidth, entryHeight, mouseX, mouseY, hovered, tickDelta);

int buttonY = expandMinimizeButton.getY();

resetListButton.setY(buttonY);
addListButton.setY(buttonY);

resetListButton.render(graphics, mouseX, mouseY, tickDelta);
addListButton.render(graphics, mouseX, mouseY, tickDelta);
}

private void minimizeIfUnavailable() {
if (!mapOption.available() && isExpanded()) {
setExpanded(false);
}
}

@Override
protected void updateExpandMinimizeText() {
super.updateExpandMinimizeText();
expandMinimizeButton.active = mapOption == null || mapOption.available();
if (addListButton != null)
addListButton.active = expandMinimizeButton.active && mapOption.numberOfEntries() < mapOption.maximumNumberOfEntries();
}

@Override
public void setExpanded(boolean expanded) {
super.setExpanded(mapOption.available() && expanded);
}

@Override
public List<? extends GuiEventListener> children() {
return ImmutableList.of(expandMinimizeButton, addListButton, resetListButton);
}
}

public class EmptyListLabel extends Entry {
private final ListGroupSeparatorEntry parent;
private final String groupName;
Expand Down Expand Up @@ -569,4 +680,41 @@ public List<? extends NarratableEntry> narratables() {
return ImmutableList.of();
}
}

public class EmptyMapLabel extends Entry {
private final MapGroupSeparatorEntry parent;
private final String groupName;
private final String categoryName;

public EmptyMapLabel(MapGroupSeparatorEntry parent, ConfigCategory category) {
this.parent = parent;
this.groupName = parent.group.name().getString().toLowerCase();
this.categoryName = category.name().getString().toLowerCase();
}

@Override
public void render(GuiGraphics graphics, int index, int y, int x, int entryWidth, int entryHeight, int mouseX, int mouseY, boolean hovered, float tickDelta) {
graphics.drawCenteredString(Minecraft.getInstance().font, Component.translatable("yacl.list.empty").withStyle(ChatFormatting.DARK_GRAY, ChatFormatting.ITALIC), x + entryWidth / 2, y, -1);
}

@Override
public boolean isViewable() {
return parent.isExpanded() && (searchQuery.isEmpty() || groupName.contains(searchQuery));
}

@Override
public int getItemHeight() {
return 11;
}

@Override
public @NotNull List<? extends GuiEventListener> children() {
return ImmutableList.of();
}

@Override
public @NotNull List<? extends NarratableEntry> narratables() {
return ImmutableList.of();
}
}
}
Loading