Skip to content

Commit cab74fa

Browse files
IMS212EnnuiLcontariaahaykam821
authored andcommitted
Squashed implementation of Config API and Options Screen redesign with initial work on UX/UI refactor
Curently broken: DonationButtonWidget and ScrollableTooltip need updating to the latest state of Minecraft GUI rendering fix: correct condition and use setAllowedValuesProvider fix: don't show fabulous on unsupported systems Tackle page headers and Apply/Done on bottom don't open vanilla options when doing text input in search mode move scroll code into separate file fix option list scroll bars not working because of wrong dimensions that fail the hit test reset options to binding value on initial config screen open to fetch changes from changed vanilla options improve top margin in scissor of search results add new vanilla option to options screen fix focus behavior on search bar fix compact donation button functionality fix merge issues fix some focus issues with the search box add search results with page list and clickable page list items use abstract option list instead of concrete option list where possible use option page instead of just page as the metadata on text sources from options make the search results actually ordered make page list highlight more visible remove search result class and move score into text source, this simplifies a number of operations around the search result processing. also added more context to the text source objects add search widget that turns search button into a text box with a close button, other refactoring around page and option list layouts move search button to top of page list add search index interface and bigram search index implementation add page list width constant move builder impls into separate package update todos add arrow to toolbar at hovered element, moved tooltip logic into own class, moved donation button logic into own class, refactored layout misc cleanup and refactor scrollbar widget to extend abstract widget consolidate more colors into the color class new: make tooltips scrollable allow changing fullscreen resolution on macOS too fixes #2831 Fix cycling controls not playing a sound when activated using the keyboard fixes #2309 add annotation-based entrypoint loading on neoforge, refactored how mod metadata is retrieved, updated documentation implement dynamic value constraints in control elements, cleanup control elements cleanup add ability for mods to override other mods' options with new options allow gui scale to be changed by holding down control and scrolling, refactor option applying, move flag processing into Config, add api methods to add button that redirects to an external page add usage documentation, update todos truncate mod header subtitles if too long add default implementation to early register entrypoint so users don't need to implement both entrypoints even if they only need the late ("main") one improve config failure crash message fix tooltip after rebase constrain tooltip to screen move layout constants into separate class, fix gradient behind sidebar, change layout of buttons and options panes, fix overlapping ui elements, fix scissor areas, make spacings and arrangement consistent fix crash not working correctly add api method for formatting version instead of setting a new one fix: move sliders and tickboxes when sccrolling Co-authored-by: contaria <[email protected]> new: scrollable option list Co-authored-by: contaria <[email protected]> refactor: store dimensions in AbstractWidget Co-authored-by: contaria <[email protected]> move important members of option builder subinterfaces more useful add api to add custom screens, use a color theme builder instead of a direct method allow users to set their own theme colors, fix some issues when text is too long scrollabe page list authored by @KingContaria Add WIP config api with IMS' redesign of the options page and contaria's forge mod entrypoint loader code New game GUI experiment Co-authored-by: Ennui Langeweile <[email protected]> Co-authored-by: contaria <[email protected]> Co-authored-by: haykam821 <[email protected]> Co-authored-by: IMS212 <[email protected]>
1 parent 60d40b3 commit cab74fa

File tree

116 files changed

+6284
-1724
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

116 files changed

+6284
-1724
lines changed
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package net.caffeinemc.mods.sodium.api.config;
2+
3+
import net.caffeinemc.mods.sodium.api.config.structure.ConfigBuilder;
4+
5+
public interface ConfigEntryPoint {
6+
default void registerConfigEarly(ConfigBuilder builder) {
7+
};
8+
9+
void registerConfigLate(ConfigBuilder builder);
10+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package net.caffeinemc.mods.sodium.api.config;
2+
3+
import java.lang.annotation.ElementType;
4+
import java.lang.annotation.Retention;
5+
import java.lang.annotation.RetentionPolicy;
6+
import java.lang.annotation.Target;
7+
8+
@Retention(RetentionPolicy.RUNTIME)
9+
@Target(ElementType.TYPE)
10+
public @interface ConfigEntryPointForge {
11+
/**
12+
* The mod id to associate this config entrypoint's "owner" with.
13+
*
14+
* @return the mod id
15+
*/
16+
String value();
17+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package net.caffeinemc.mods.sodium.api.config;
2+
3+
import net.minecraft.resources.ResourceLocation;
4+
5+
public interface ConfigState {
6+
boolean readBooleanOption(ResourceLocation id);
7+
8+
int readIntOption(ResourceLocation id);
9+
10+
<E extends Enum<E>> E readEnumOption(ResourceLocation id, Class<E> enumClass);
11+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package net.caffeinemc.mods.sodium.api.config;
2+
3+
@FunctionalInterface
4+
public interface StorageEventHandler {
5+
void afterSave();
6+
}
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
TODO: finalize dependency declaration to only use the API package, variant declaration?
2+
3+
# Usage of the Sodium Config API
4+
5+
The Sodium Config API lets mods add their own pages to the Video Settings screen, which Sodium replaces with its own screen.
6+
7+
## Scope
8+
9+
The Sodium Config API is intended for mods that add video settings, not as a general purpose config API. For general purpose configuration, use the platform's appropriate mod list and a config library.
10+
11+
As a presentation API, it does not handle loading, parsing, or saving configuration data to files. It is up to your mod to handle that on its own.
12+
13+
## Overview
14+
15+
Sodium redirects Minecraft's "Video Settings" screen to its own screen. Historically, third-party mods have mixed into Sodium to add buttons to their own settings pages or additional options to Sodium's pages.
16+
17+
With this API, these mods will not need to touch Sodium's internals anymore and should be able to operate independently of the GUI's implementation details. The API may not be able to cover all use cases where mods mixed into Sodium's options code, but it should cover most of the common ones.
18+
19+
Registration of options happens in two stages: Early and late. Early registration happens when Sodium initializes its own early options before the window is created. Late registration happens after the game launched. Most mods will only need to use late registration. These stages are independent and only options that are registered in the late stage will show up in the GUI.
20+
21+
## Getting Started
22+
23+
### Dependency on Sodium's API
24+
25+
Sodium publishes its api package on a maven repository that you can depend on in your buildscript.
26+
27+
Fabric:
28+
29+
```groovy
30+
dependencies {
31+
// ... other dependencies
32+
33+
modImplementation "net.caffeinemc.mods:sodium-fabric:0.6.0+mc1.21.3"
34+
}
35+
```
36+
37+
NeoForge:
38+
39+
```groovy
40+
dependencies {
41+
// ... other dependencies
42+
43+
implementation "net.caffeinemc.mods:sodium-neoforge:0.6.0+mc1.21.3"
44+
}
45+
```
46+
47+
### Creating an Entrypoint
48+
49+
Entrypoint classes that Sodium calls to run your options registration code can be declared either in your mod's metadata file, or on NeoForge with a special annotation.
50+
51+
#### With a Metadata Entry
52+
53+
Metadata-based entrypoints use the key `sodium:config_api_user` and the value is the full reference to a class that implements the `net.caffeinemc.mods.sodium.api.config.ConfigEntryPoint` interface.
54+
55+
Fabric `fabric.mod.json`:
56+
57+
```json5
58+
{
59+
"entrypoints": {
60+
// ... other entrypoints
61+
62+
"sodium:config_api_user": [
63+
"com.example.examplemod.ExampleModConfigBuilder"
64+
]
65+
}
66+
}
67+
```
68+
69+
NeoForge `neoforge.mods.toml`:
70+
```toml
71+
[modproperties.examplemod]
72+
"sodium:config_api_user" = "com.example.examplemod.ExampleModConfigBuilder"
73+
```
74+
75+
The implementation of the entrypoint can look something like this:
76+
77+
```java
78+
package com.example.examplemod;
79+
80+
import net.caffeinemc.mods.sodium.api.config.ConfigEntryPoint;
81+
import net.caffeinemc.mods.sodium.api.config.structure.ConfigBuilder;
82+
import net.minecraft.network.chat.Component;
83+
import net.minecraft.resources.ResourceLocation;
84+
85+
public class ExampleConfigUser implements ConfigEntryPoint {
86+
// Store your options in a separate class!
87+
private final class OptionStorage {
88+
private boolean exampleOption = true;
89+
90+
public boolean getExampleOption() {
91+
return this.exampleOption;
92+
}
93+
94+
public void setExampleOption(boolean value) {
95+
this.exampleOption = value;
96+
}
97+
98+
public void flush() {
99+
// flush options to config file
100+
}
101+
}
102+
103+
private final OptionStorage storage = new OptionStorage();
104+
private final Runnable handler = this.storage::flush;
105+
106+
@Override
107+
public void registerConfigLate(ConfigBuilder builder) {
108+
builder.registerOwnModOptions()
109+
.addPage(builder.createOptionPage()
110+
.setName(Component.literal("Example Page"))
111+
.addOptionGroup(builder.createOptionGroup()
112+
.setName(Component.literal("Example Group"))
113+
.addOption(builder.createBooleanOption(ResourceLocation.parse("examplemod:example_option"))
114+
.setName(Component.literal("Example Option")) // use translation keys here
115+
.setTooltip(Component.literal("Example tooltip"))
116+
.setStorageHandler(this.handler)
117+
.setBinding(this.storage::setExampleOption, this.storage::getExampleOption)
118+
.setDefaultValue(true)
119+
)
120+
)
121+
);
122+
}
123+
}
124+
```
125+
126+
#### NeoForge: With an Annotation
127+
128+
Since NeoForge has the convention of using annotations for entrypoints, this option is provided as an alternative. Any classes annotated with `@ConfigEntryPointForge("examplemod")` will be loaded as config entrypoints too. Note that the annotation must be given the mod id that should be associated as the default mod for which a config is registered with `ConfigBuilder.registerOwnModOptions`. This is necessary as it's otherwise impossible to uniquely determine which mod a class is associated with on NeoForge.
129+
130+
```java
131+
import net.caffeinemc.mods.sodium.api.config.ConfigEntryPoint;
132+
import net.caffeinemc.mods.sodium.api.config.ConfigEntryPointForge;
133+
134+
@ConfigEntryPointForge("examplemod")
135+
public class ExampleConfigUser implements ConfigEntryPoint {
136+
// class body identical to the above
137+
}
138+
```
139+
140+
### Registering Your Options
141+
142+
Each mod adds a page for its options, within each page there are groups of options, and each group contains a list of options. Each option has an id, a name, a tooltip, a storage handler, a binding, and a default value. There are three types of options: boolean (tickbox), integer (slider), and enum. Optionally, all types of options can be disabled, while integer and enum options can have their allowed values restricted. Those two types also require you to set a function that assigns a label to each selected value.
143+
144+
Some attributes of an option can be provided dynamically, meaning the returned value can depend on the state of another option. The default value, option enablement, and the allowed values can be computed dynamically. The methods for setting a dynamic value provider also require you to specify a list of dependencies, which are the resource locations of the options that the dynamic value provider reads from. Since dynamically evaluated attributes may change the state of an option, cyclic dependencies will lead to option registration failing and the game crashing.
145+
146+
Sodium constructs one instance of the entrypoint class, and then calls the early and late registration methods at the right time.
147+
148+
## API Notes
149+
150+
The API is largely self-explanatory and an example is provided above. Also see Sodium's own options registration for a more in-depth example of the API's usage.
151+
152+
### Using `ConfigBuilder` and `ModOptions`
153+
154+
The `ConfigBuilder` instance passed to the registration method allows quick and easy registration of a mod's own options using `ConfigBuilder.registerOwnModOptions`. The mod's id, name, version or a formatter for the existing version, and the color theme can be configured on the returned `ModOptionsBuilder`. It's also possible to register options for additional mods using `ConfigBuilder.registerModOptions`. Which mod is the "own" mod for `registerOwnModOptions` is determined by the mod that owns the metadata-based entrypoint or the mod id passed to the `@ConfigEntryPointForge("examplemod")` annotation.
155+
156+
Each registered mod gets its own header in the page list. The color of the header and the corresponding entries is randomly selected from a predefined list by default, but can be customized using `ModOptionsBuilder.setColorTheme`. A color theme is created either by specifying three RGB colors or a single base color with the lighter and darker colors getting derived automatically.
157+
158+
To simply switch to a new `Screen` when an entry in the video settings screen's page list is clicked, use `ConfigBuilder.createExternalPage` and add the returned page normally after configuring it with a name and a `Consumer<Screen>` that receives the current screen and switches to your custom screen.
159+
160+
### Using `OptionBuilder`
161+
162+
The storage handler set with `OptionBuilder.setStorageHandler` is called after changes have been made to the options through the bindings. This lets you flush the changes to the config file once, instead of every time an option is changed.
163+
164+
The tooltip set with `OptionBuilder.setTooltip` can optionally be a function that generates a tooltip depending on the option's current value. This is useful for enum options for which the description would be too long otherwise.
165+
166+
Optionally a performance impact can be specified with `OptionBuilder.setImpact` where the impact ranges from low to high (or "varies").
167+
168+
Flags set with `OptionBuilder.setFlags` control what things are reset when this option is applied. They include reloading chunks or reloading resource packs. See `OptionFlag` for the available values.
169+
170+
The default value set with `OptionBuilder.setDefaultValue`, or dynamically with `OptionBuilder.setDefaultProvider`, is used if the value returned by the binding does not fulfill the option's value constraint (in the case of a integer or enum option).
171+
172+
Disabling an option with `OptionBuilder.setEnabled(false)` shows the option as strikethrough and makes it non-interactive. Otherwise, especially with regard to value constraints, it will behave as usual.
173+
174+
The binding configured with `OptionBuilder.setBinding` is called when changes to the options have been made and are applied, or when the value no longer fulfills the option's constraints and is reset to the default value. It's also used to initially load a value during initialization.
175+
176+
### Using `? extends OptionBuilder`
177+
178+
Some of the attributes of an option are required and you must set them, or registration will fail. The concrete extensions of `OptionBuilder` for each of the option types have some additional methods for configuring type-specific things, some of which are also required. Notably, `EnumOptionBuilder.setElementNameProvider` and `IntegerOptionBuilder.setValueFormatter` are required in order to display these types of options. The method `setValueFormatter` for integer options takes a `ControlValueFormatter`, which simply formats a number as a `Component`. Many standard value formatters are provided in `ControlValueFormatterImpls` (not part of the API package).
179+
180+
Integer and enum options can have value constraints that restrict the set of allowed values the user can select. For integer options, a `Range` must be configured with `IntegerOptionBuilder.setRange` so that the slider's start, end, and step positions are well-defined. Enum options may be configured to only allow the selection of certain elements with `EnumOptionBuilder.setAllowedValues`.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package net.caffeinemc.mods.sodium.api.config.option;
2+
3+
import net.minecraft.network.chat.Component;
4+
5+
public interface ControlValueFormatter {
6+
Component format(int value);
7+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package net.caffeinemc.mods.sodium.api.config.option;
2+
3+
import net.minecraft.network.chat.Component;
4+
5+
public interface NameProvider {
6+
Component getName();
7+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package net.caffeinemc.mods.sodium.api.config.option;
2+
3+
public interface OptionBinding<V> {
4+
void save(V value);
5+
6+
V load();
7+
8+
// TODO: add shortcuts to generate for vanilla option bindings
9+
}

common/src/main/java/net/caffeinemc/mods/sodium/client/gui/options/OptionFlag.java renamed to common/src/api/java/net/caffeinemc/mods/sodium/api/config/option/OptionFlag.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package net.caffeinemc.mods.sodium.client.gui.options;
1+
package net.caffeinemc.mods.sodium.api.config.option;
22

33
public enum OptionFlag {
44
REQUIRES_RENDERER_RELOAD,

common/src/main/java/net/caffeinemc/mods/sodium/client/gui/options/OptionImpact.java renamed to common/src/api/java/net/caffeinemc/mods/sodium/api/config/option/OptionImpact.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
package net.caffeinemc.mods.sodium.client.gui.options;
1+
package net.caffeinemc.mods.sodium.api.config.option;
22

33
import net.minecraft.ChatFormatting;
44
import net.minecraft.network.chat.Component;
55

6-
public enum OptionImpact implements TextProvider {
6+
public enum OptionImpact implements NameProvider {
77
LOW(ChatFormatting.GREEN, "sodium.option_impact.low"),
88
MEDIUM(ChatFormatting.YELLOW, "sodium.option_impact.medium"),
99
HIGH(ChatFormatting.GOLD, "sodium.option_impact.high"),
@@ -17,7 +17,7 @@ public enum OptionImpact implements TextProvider {
1717
}
1818

1919
@Override
20-
public Component getLocalizedName() {
20+
public Component getName() {
2121
return this.text;
2222
}
2323
}

0 commit comments

Comments
 (0)