aboutsummaryrefslogtreecommitdiff
path: root/src/main/java
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java')
-rw-r--r--src/main/java/io/github/cottonmc/cotton/gui/client/CottonClientScreen.java22
-rw-r--r--src/main/java/io/github/cottonmc/cotton/gui/client/CottonInventoryScreen.java26
-rw-r--r--src/main/java/io/github/cottonmc/cotton/gui/impl/client/CottonScreenImpl.java3
-rw-r--r--src/main/java/io/github/cottonmc/cotton/gui/impl/client/MouseInputHandler.java38
-rw-r--r--src/main/java/io/github/cottonmc/cotton/gui/impl/client/NarrationHelper.java49
-rw-r--r--src/main/java/io/github/cottonmc/cotton/gui/impl/client/NarrationMessages.java29
-rw-r--r--src/main/java/io/github/cottonmc/cotton/gui/widget/WAbstractSlider.java11
-rw-r--r--src/main/java/io/github/cottonmc/cotton/gui/widget/WButton.java17
-rw-r--r--src/main/java/io/github/cottonmc/cotton/gui/widget/WItemSlot.java59
-rw-r--r--src/main/java/io/github/cottonmc/cotton/gui/widget/WLabel.java8
-rw-r--r--src/main/java/io/github/cottonmc/cotton/gui/widget/WLabeledSlider.java15
-rw-r--r--src/main/java/io/github/cottonmc/cotton/gui/widget/WPanel.java12
-rw-r--r--src/main/java/io/github/cottonmc/cotton/gui/widget/WPlayerInvPanel.java9
-rw-r--r--src/main/java/io/github/cottonmc/cotton/gui/widget/WScrollBar.java10
-rw-r--r--src/main/java/io/github/cottonmc/cotton/gui/widget/WTabPanel.java17
-rw-r--r--src/main/java/io/github/cottonmc/cotton/gui/widget/WText.java8
-rw-r--r--src/main/java/io/github/cottonmc/cotton/gui/widget/WTextField.java15
-rw-r--r--src/main/java/io/github/cottonmc/cotton/gui/widget/WToggleButton.java25
-rw-r--r--src/main/java/io/github/cottonmc/cotton/gui/widget/WWidget.java69
-rw-r--r--src/main/java/io/github/cottonmc/cotton/gui/widget/data/ObservableProperty.java162
-rw-r--r--src/main/java/io/github/cottonmc/cotton/gui/widget/data/ObservableView.java75
21 files changed, 652 insertions, 27 deletions
diff --git a/src/main/java/io/github/cottonmc/cotton/gui/client/CottonClientScreen.java b/src/main/java/io/github/cottonmc/cotton/gui/client/CottonClientScreen.java
index cb80707..331f68b 100644
--- a/src/main/java/io/github/cottonmc/cotton/gui/client/CottonClientScreen.java
+++ b/src/main/java/io/github/cottonmc/cotton/gui/client/CottonClientScreen.java
@@ -1,6 +1,7 @@
package io.github.cottonmc.cotton.gui.client;
import net.minecraft.client.gui.screen.Screen;
+import net.minecraft.client.gui.screen.narration.NarrationMessageBuilder;
import net.minecraft.client.util.math.MatrixStack;
import net.minecraft.text.LiteralText;
import net.minecraft.text.Style;
@@ -10,6 +11,7 @@ import io.github.cottonmc.cotton.gui.GuiDescription;
import io.github.cottonmc.cotton.gui.impl.VisualLogger;
import io.github.cottonmc.cotton.gui.impl.client.CottonScreenImpl;
import io.github.cottonmc.cotton.gui.impl.client.MouseInputHandler;
+import io.github.cottonmc.cotton.gui.impl.client.NarrationHelper;
import io.github.cottonmc.cotton.gui.widget.WPanel;
import io.github.cottonmc.cotton.gui.widget.WWidget;
import org.jetbrains.annotations.Nullable;
@@ -36,6 +38,8 @@ public class CottonClientScreen extends Screen implements CottonScreenImpl {
@Nullable
protected WWidget lastResponder = null;
+
+ private final MouseInputHandler<CottonClientScreen> mouseInputHandler = new MouseInputHandler<>(this);
public CottonClientScreen(GuiDescription description) {
this(new LiteralText(""), description);
@@ -46,7 +50,8 @@ public class CottonClientScreen extends Screen implements CottonScreenImpl {
this.description = description;
description.getRootPanel().validate(description);
}
-
+
+ @Override
public GuiDescription getDescription() {
return description;
}
@@ -176,7 +181,7 @@ public class CottonClientScreen extends Screen implements CottonScreenImpl {
int containerX = (int)mouseX-left;
int containerY = (int)mouseY-top;
if (containerX<0 || containerY<0 || containerX>=width || containerY>=height) return true;
- MouseInputHandler.onMouseDown(description, this, containerX, containerY, mouseButton);
+ mouseInputHandler.onMouseDown(containerX, containerY, mouseButton);
return true;
}
@@ -187,7 +192,7 @@ public class CottonClientScreen extends Screen implements CottonScreenImpl {
super.mouseReleased(mouseX, mouseY, mouseButton);
int containerX = (int)mouseX-left;
int containerY = (int)mouseY-top;
- MouseInputHandler.onMouseUp(description, this, containerX, containerY, mouseButton);
+ mouseInputHandler.onMouseUp(containerX, containerY, mouseButton);
return true;
}
@@ -199,7 +204,7 @@ public class CottonClientScreen extends Screen implements CottonScreenImpl {
int containerX = (int)mouseX-left;
int containerY = (int)mouseY-top;
- MouseInputHandler.onMouseDrag(description, this, containerX, containerY, mouseButton, deltaX, deltaY);
+ mouseInputHandler.onMouseDrag(containerX, containerY, mouseButton, deltaX, deltaY);
return true;
}
@@ -210,7 +215,7 @@ public class CottonClientScreen extends Screen implements CottonScreenImpl {
int containerX = (int)mouseX-left;
int containerY = (int)mouseY-top;
- MouseInputHandler.onMouseScroll(description, containerX, containerY, amount);
+ mouseInputHandler.onMouseScroll(containerX, containerY, amount);
return true;
}
@@ -221,7 +226,7 @@ public class CottonClientScreen extends Screen implements CottonScreenImpl {
int containerX = (int)mouseX-left;
int containerY = (int)mouseY-top;
- MouseInputHandler.onMouseMove(description, containerX, containerY);
+ mouseInputHandler.onMouseMove(containerX, containerY);
}
@Override
@@ -264,4 +269,9 @@ public class CottonClientScreen extends Screen implements CottonScreenImpl {
return true;
}
+
+ @Override
+ protected void addElementNarrations(NarrationMessageBuilder builder) {
+ if (description != null) NarrationHelper.addNarrations(description.getRootPanel(), builder);
+ }
}
diff --git a/src/main/java/io/github/cottonmc/cotton/gui/client/CottonInventoryScreen.java b/src/main/java/io/github/cottonmc/cotton/gui/client/CottonInventoryScreen.java
index 87d9453..953563c 100644
--- a/src/main/java/io/github/cottonmc/cotton/gui/client/CottonInventoryScreen.java
+++ b/src/main/java/io/github/cottonmc/cotton/gui/client/CottonInventoryScreen.java
@@ -1,6 +1,7 @@
package io.github.cottonmc.cotton.gui.client;
import net.minecraft.client.gui.screen.ingame.HandledScreen;
+import net.minecraft.client.gui.screen.narration.NarrationMessageBuilder;
import net.minecraft.client.render.DiffuseLighting;
import net.minecraft.client.util.math.MatrixStack;
import net.minecraft.entity.player.PlayerEntity;
@@ -8,12 +9,15 @@ import net.minecraft.text.LiteralText;
import net.minecraft.text.Style;
import net.minecraft.text.Text;
+import io.github.cottonmc.cotton.gui.GuiDescription;
import io.github.cottonmc.cotton.gui.SyncedGuiDescription;
import io.github.cottonmc.cotton.gui.impl.VisualLogger;
import io.github.cottonmc.cotton.gui.impl.client.CottonScreenImpl;
import io.github.cottonmc.cotton.gui.impl.client.MouseInputHandler;
+import io.github.cottonmc.cotton.gui.impl.client.NarrationHelper;
import io.github.cottonmc.cotton.gui.widget.WPanel;
import io.github.cottonmc.cotton.gui.widget.WWidget;
+import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Nullable;
import org.lwjgl.glfw.GLFW;
import org.lwjgl.opengl.GL11;
@@ -26,6 +30,7 @@ import org.lwjgl.opengl.GL11;
public class CottonInventoryScreen<T extends SyncedGuiDescription> extends HandledScreen<T> implements CottonScreenImpl {
protected SyncedGuiDescription description;
@Nullable protected WWidget lastResponder = null;
+ private final MouseInputHandler<CottonInventoryScreen<T>> mouseInputHandler = new MouseInputHandler<>(this);
/**
* Constructs a new screen without a title.
@@ -83,6 +88,12 @@ public class CottonInventoryScreen<T extends SyncedGuiDescription> extends Handl
VisualLogger.reset();
}
+ @ApiStatus.Internal
+ @Override
+ public GuiDescription getDescription() {
+ return description;
+ }
+
@Nullable
@Override
public WWidget getLastResponder() {
@@ -187,7 +198,7 @@ public class CottonInventoryScreen<T extends SyncedGuiDescription> extends Handl
int containerX = (int)mouseX-x;
int containerY = (int)mouseY-y;
if (containerX<0 || containerY<0 || containerX>=width || containerY>=height) return result;
- MouseInputHandler.onMouseDown(description, this, containerX, containerY, mouseButton);
+ mouseInputHandler.onMouseDown(containerX, containerY, mouseButton);
return true;
}
@@ -197,7 +208,7 @@ public class CottonInventoryScreen<T extends SyncedGuiDescription> extends Handl
super.mouseReleased(mouseX, mouseY, mouseButton);
int containerX = (int)mouseX-x;
int containerY = (int)mouseY-y;
- MouseInputHandler.onMouseUp(description, this, containerX, containerY, mouseButton);
+ mouseInputHandler.onMouseUp(containerX, containerY, mouseButton);
return true;
}
@@ -208,7 +219,7 @@ public class CottonInventoryScreen<T extends SyncedGuiDescription> extends Handl
int containerX = (int)mouseX-x;
int containerY = (int)mouseY-y;
- MouseInputHandler.onMouseDrag(description, this, containerX, containerY, mouseButton, deltaX, deltaY);
+ mouseInputHandler.onMouseDrag(containerX, containerY, mouseButton, deltaX, deltaY);
return true;
}
@@ -219,7 +230,7 @@ public class CottonInventoryScreen<T extends SyncedGuiDescription> extends Handl
int containerX = (int)mouseX-x;
int containerY = (int)mouseY-y;
- MouseInputHandler.onMouseScroll(description, containerX, containerY, amount);
+ mouseInputHandler.onMouseScroll(containerX, containerY, amount);
return true;
}
@@ -230,7 +241,7 @@ public class CottonInventoryScreen<T extends SyncedGuiDescription> extends Handl
int containerX = (int)mouseX-x;
int containerY = (int)mouseY-y;
- MouseInputHandler.onMouseMove(description, containerX, containerY);
+ mouseInputHandler.onMouseMove(containerX, containerY);
}
@Override
@@ -304,4 +315,9 @@ public class CottonInventoryScreen<T extends SyncedGuiDescription> extends Handl
return true;
}
+
+ @Override
+ protected void addElementNarrations(NarrationMessageBuilder builder) {
+ if (description != null) NarrationHelper.addNarrations(description.getRootPanel(), builder);
+ }
}
diff --git a/src/main/java/io/github/cottonmc/cotton/gui/impl/client/CottonScreenImpl.java b/src/main/java/io/github/cottonmc/cotton/gui/impl/client/CottonScreenImpl.java
index 2ef9632..cd215b3 100644
--- a/src/main/java/io/github/cottonmc/cotton/gui/impl/client/CottonScreenImpl.java
+++ b/src/main/java/io/github/cottonmc/cotton/gui/impl/client/CottonScreenImpl.java
@@ -5,11 +5,14 @@ import net.fabricmc.api.Environment;
import net.minecraft.client.util.math.MatrixStack;
import net.minecraft.text.Style;
+import io.github.cottonmc.cotton.gui.GuiDescription;
import io.github.cottonmc.cotton.gui.widget.WWidget;
import org.jetbrains.annotations.Nullable;
@Environment(EnvType.CLIENT)
public interface CottonScreenImpl {
+ GuiDescription getDescription();
+
@Nullable
WWidget getLastResponder();
diff --git a/src/main/java/io/github/cottonmc/cotton/gui/impl/client/MouseInputHandler.java b/src/main/java/io/github/cottonmc/cotton/gui/impl/client/MouseInputHandler.java
index 45fc17f..055df75 100644
--- a/src/main/java/io/github/cottonmc/cotton/gui/impl/client/MouseInputHandler.java
+++ b/src/main/java/io/github/cottonmc/cotton/gui/impl/client/MouseInputHandler.java
@@ -2,9 +2,9 @@ package io.github.cottonmc.cotton.gui.impl.client;
import net.minecraft.client.gui.screen.Screen;
-import io.github.cottonmc.cotton.gui.GuiDescription;
import io.github.cottonmc.cotton.gui.widget.WWidget;
import io.github.cottonmc.cotton.gui.widget.data.InputResult;
+import io.github.cottonmc.cotton.gui.widget.data.ObservableProperty;
import org.jetbrains.annotations.Nullable;
import java.util.function.Function;
@@ -12,10 +12,21 @@ import java.util.function.Function;
/**
* The implementation for mouse inputs.
*/
-public final class MouseInputHandler {
- public static void onMouseDown(GuiDescription description, CottonScreenImpl screen, int containerX, int containerY, int mouseButton) {
+public final class MouseInputHandler<S extends Screen & CottonScreenImpl> {
+ private final S screen;
+ private final ObservableProperty<WWidget> hovered = ObservableProperty.of(null);
+
+ public MouseInputHandler(S screen) {
+ this.screen = screen;
+ hovered.addListener((property, from, to) -> {
+ if (from != null) from.setHovered(false);
+ if (to != null) to.setHovered(true);
+ });
+ }
+
+ public void onMouseDown(int containerX, int containerY, int mouseButton) {
if (screen.getLastResponder() == null) {
- WWidget lastResponder = description.getRootPanel().hit(containerX, containerY);
+ WWidget lastResponder = screen.getDescription().getRootPanel().hit(containerX, containerY);
screen.setLastResponder(lastResponder);
if (lastResponder != null) {
runTree(
@@ -28,7 +39,7 @@ public final class MouseInputHandler {
}
}
- public static <S extends Screen & CottonScreenImpl> void onMouseUp(GuiDescription description, S screen, int containerX, int containerY, int mouseButton) {
+ public void onMouseUp(int containerX, int containerY, int mouseButton) {
WWidget lastResponder = screen.getLastResponder();
if (lastResponder != null) {
@@ -48,7 +59,7 @@ public final class MouseInputHandler {
}
} else {
runTree(
- description.getRootPanel().hit(containerX, containerY),
+ screen.getDescription().getRootPanel().hit(containerX, containerY),
widget -> widget.onMouseUp(containerX - widget.getAbsoluteX(), containerY - widget.getAbsoluteY(), mouseButton)
);
}
@@ -56,7 +67,7 @@ public final class MouseInputHandler {
screen.setLastResponder(null);
}
- public static <S extends Screen & CottonScreenImpl> void onMouseDrag(GuiDescription description, S screen, int containerX, int containerY, int mouseButton, double deltaX, double deltaY) {
+ public void onMouseDrag(int containerX, int containerY, int mouseButton, double deltaX, double deltaY) {
WWidget lastResponder = screen.getLastResponder();
if (lastResponder != null) {
@@ -68,22 +79,25 @@ public final class MouseInputHandler {
if (containerX < 0 || containerY < 0 || containerX >= width || containerY >= height) return;
runTree(
- description.getRootPanel().hit(containerX, containerY),
+ screen.getDescription().getRootPanel().hit(containerX, containerY),
widget -> widget.onMouseDrag(containerX - widget.getAbsoluteX(), containerY - widget.getAbsoluteY(), mouseButton, deltaX, deltaY)
);
}
}
- public static void onMouseScroll(GuiDescription description, int containerX, int containerY, double amount) {
+ public void onMouseScroll(int containerX, int containerY, double amount) {
runTree(
- description.getRootPanel().hit(containerX, containerY),
+ screen.getDescription().getRootPanel().hit(containerX, containerY),
widget -> widget.onMouseScroll(containerX - widget.getAbsoluteX(), containerY - widget.getAbsoluteY(), amount)
);
}
- public static void onMouseMove(GuiDescription description, int containerX, int containerY) {
+ public void onMouseMove(int containerX, int containerY) {
+ WWidget hit = screen.getDescription().getRootPanel().hit(containerX, containerY);
+ hovered.set(hit);
+
runTree(
- description.getRootPanel().hit(containerX, containerY),
+ hit,
widget -> widget.onMouseMove(containerX - widget.getAbsoluteX(), containerY - widget.getAbsoluteY())
);
}
diff --git a/src/main/java/io/github/cottonmc/cotton/gui/impl/client/NarrationHelper.java b/src/main/java/io/github/cottonmc/cotton/gui/impl/client/NarrationHelper.java
new file mode 100644
index 0000000..54567be
--- /dev/null
+++ b/src/main/java/io/github/cottonmc/cotton/gui/impl/client/NarrationHelper.java
@@ -0,0 +1,49 @@
+package io.github.cottonmc.cotton.gui.impl.client;
+
+import net.fabricmc.api.EnvType;
+import net.fabricmc.api.Environment;
+import net.minecraft.client.gui.screen.narration.NarrationMessageBuilder;
+import net.minecraft.client.gui.screen.narration.NarrationPart;
+import net.minecraft.text.TranslatableText;
+
+import io.github.cottonmc.cotton.gui.widget.WPanel;
+import io.github.cottonmc.cotton.gui.widget.WWidget;
+
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+@Environment(EnvType.CLIENT)
+public final class NarrationHelper {
+ public static void addNarrations(WPanel rootPanel, NarrationMessageBuilder builder) {
+ List<WWidget> narratableWidgets = getAllWidgets(rootPanel)
+ .filter(WWidget::isNarratable)
+ .collect(Collectors.toList());
+
+ for (int i = 0, childCount = narratableWidgets.size(); i < childCount; i++) {
+ WWidget child = narratableWidgets.get(i);
+ if (!child.isFocused() && !child.isHovered()) continue;
+
+ // replicates Screen.addElementNarrations
+ if (narratableWidgets.size() > 1) {
+ builder.put(NarrationPart.POSITION, new TranslatableText(NarrationMessages.Vanilla.SCREEN_POSITION_KEY, i + 1, childCount));
+
+ if (child.isFocused()) {
+ builder.put(NarrationPart.USAGE, NarrationMessages.Vanilla.COMPONENT_LIST_USAGE);
+ }
+ }
+
+ child.addNarrations(builder.nextMessage());
+ }
+ }
+
+ private static Stream<WWidget> getAllWidgets(WPanel panel) {
+ return Stream.concat(Stream.of(panel), panel.streamChildren().flatMap(widget -> {
+ if (widget instanceof WPanel nested) {
+ return getAllWidgets(nested);
+ }
+
+ return Stream.of(widget);
+ }));
+ }
+}
diff --git a/src/main/java/io/github/cottonmc/cotton/gui/impl/client/NarrationMessages.java b/src/main/java/io/github/cottonmc/cotton/gui/impl/client/NarrationMessages.java
new file mode 100644
index 0000000..bc9ee48
--- /dev/null
+++ b/src/main/java/io/github/cottonmc/cotton/gui/impl/client/NarrationMessages.java
@@ -0,0 +1,29 @@
+package io.github.cottonmc.cotton.gui.impl.client;
+
+import net.minecraft.text.Text;
+import net.minecraft.text.TranslatableText;
+
+public final class NarrationMessages {
+ public static final String ITEM_SLOT_TITLE_KEY = "widget.libgui.item_slot.narration.title";
+ public static final String LABELED_SLIDER_TITLE_KEY = "widget.libgui.labeled_slider.narration.title";
+ public static final Text PLAYER_INVENTORY_HOTBAR = new TranslatableText("widget.libgui.player_inventory.narration.hotbar");
+ public static final Text SCROLL_BAR_TITLE = new TranslatableText("widget.libgui.scroll_bar.narration.title");
+ public static final String SLIDER_MESSAGE_KEY = "widget.libgui.slider.narration.title";
+ public static final Text SLIDER_USAGE = new TranslatableText("widget.libgui.slider.narration.usage");
+ public static final String TAB_TITLE_KEY = "widget.libgui.tab.narration.title";
+ public static final String TAB_POSITION_KEY = "widget.libgui.tab.narration.position";
+ public static final String TEXT_FIELD_TITLE_KEY = "widget.libgui.text_field.narration.title";
+ public static final String TEXT_FIELD_SUGGESTION_KEY = "widget.libgui.text_field.narration.suggestion";
+ public static final String TOGGLE_BUTTON_NAMED_KEY = "widget.libgui.toggle_button.narration.named";
+ public static final Text TOGGLE_BUTTON_OFF = new TranslatableText("widget.libgui.toggle_button.narration.off");
+ public static final Text TOGGLE_BUTTON_ON = new TranslatableText("widget.libgui.toggle_button.narration.on");
+ public static final String TOGGLE_BUTTON_UNNAMED_KEY = "widget.libgui.toggle_button.narration.unnamed";
+
+ public static final class Vanilla {
+ public static final Text BUTTON_USAGE_FOCUSED = new TranslatableText("narration.button.usage.focused");
+ public static final Text BUTTON_USAGE_HOVERED = new TranslatableText("narration.button.usage.hovered");
+ public static final Text COMPONENT_LIST_USAGE = new TranslatableText("narration.component_list.usage");
+ public static final Text INVENTORY = new TranslatableText("container.inventory");
+ public static final String SCREEN_POSITION_KEY = "narrator.position.screen";
+ }
+}
diff --git a/src/main/java/io/github/cottonmc/cotton/gui/widget/WAbstractSlider.java b/src/main/java/io/github/cottonmc/cotton/gui/widget/WAbstractSlider.java
index 7fda1d9..2af6092 100644
--- a/src/main/java/io/github/cottonmc/cotton/gui/widget/WAbstractSlider.java
+++ b/src/main/java/io/github/cottonmc/cotton/gui/widget/WAbstractSlider.java
@@ -2,8 +2,12 @@ package io.github.cottonmc.cotton.gui.widget;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
+import net.minecraft.client.gui.screen.narration.NarrationMessageBuilder;
+import net.minecraft.client.gui.screen.narration.NarrationPart;
+import net.minecraft.text.TranslatableText;
import net.minecraft.util.math.MathHelper;
+import io.github.cottonmc.cotton.gui.impl.client.NarrationMessages;
import io.github.cottonmc.cotton.gui.widget.data.Axis;
import io.github.cottonmc.cotton.gui.widget.data.InputResult;
import org.jetbrains.annotations.Nullable;
@@ -342,6 +346,13 @@ public abstract class WAbstractSlider extends WWidget {
return dragging;
}
+ @Environment(EnvType.CLIENT)
+ @Override
+ public void addNarrations(NarrationMessageBuilder builder) {
+ builder.put(NarrationPart.TITLE, new TranslatableText(NarrationMessages.SLIDER_MESSAGE_KEY, value, min, max));
+ builder.put(NarrationPart.USAGE, NarrationMessages.SLIDER_USAGE);
+ }
+
/**
* Tests if the key should decrease sliders with the specified direction.
*
diff --git a/src/main/java/io/github/cottonmc/cotton/gui/widget/WButton.java b/src/main/java/io/github/cottonmc/cotton/gui/widget/WButton.java
index 15cd6bb..533e436 100644
--- a/src/main/java/io/github/cottonmc/cotton/gui/widget/WButton.java
+++ b/src/main/java/io/github/cottonmc/cotton/gui/widget/WButton.java
@@ -3,6 +3,8 @@ package io.github.cottonmc.cotton.gui.widget;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.gui.screen.narration.NarrationMessageBuilder;
+import net.minecraft.client.gui.screen.narration.NarrationPart;
import net.minecraft.client.gui.widget.ClickableWidget;
import net.minecraft.client.sound.PositionedSoundInstance;
import net.minecraft.client.util.math.MatrixStack;
@@ -12,6 +14,7 @@ import net.minecraft.util.Identifier;
import io.github.cottonmc.cotton.gui.client.LibGui;
import io.github.cottonmc.cotton.gui.client.ScreenDrawing;
+import io.github.cottonmc.cotton.gui.impl.client.NarrationMessages;
import io.github.cottonmc.cotton.gui.widget.data.HorizontalAlignment;
import io.github.cottonmc.cotton.gui.widget.data.InputResult;
import io.github.cottonmc.cotton.gui.widget.icon.Icon;
@@ -220,6 +223,20 @@ public class WButton extends WWidget {
}
@Environment(EnvType.CLIENT)
+ @Override
+ public void addNarrations(NarrationMessageBuilder builder) {
+ builder.put(NarrationPart.TITLE, ClickableWidget.getNarrationMessage(getLabel()));
+
+ if (isEnabled()) {
+ if (isFocused()) {
+ builder.put(NarrationPart.USAGE, NarrationMessages.Vanilla.BUTTON_USAGE_FOCUSED);
+ } else if (isHovered()) {
+ builder.put(NarrationPart.USAGE, NarrationMessages.Vanilla.BUTTON_USAGE_HOVERED);
+ }
+ }
+ }
+
+ @Environment(EnvType.CLIENT)
static Identifier getTexture() {
return LibGui.isDarkMode() ? DARK_WIDGETS_LOCATION : ClickableWidget.WIDGETS_TEXTURE;
}
diff --git a/src/main/java/io/github/cottonmc/cotton/gui/widget/WItemSlot.java b/src/main/java/io/github/cottonmc/cotton/gui/widget/WItemSlot.java
index a99028c..a05b364 100644
--- a/src/main/java/io/github/cottonmc/cotton/gui/widget/WItemSlot.java
+++ b/src/main/java/io/github/cottonmc/cotton/gui/widget/WItemSlot.java
@@ -3,16 +3,23 @@ package io.github.cottonmc.cotton.gui.widget;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.minecraft.client.MinecraftClient;
+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.entity.player.PlayerInventory;
import net.minecraft.inventory.Inventory;
import net.minecraft.item.ItemStack;
import net.minecraft.screen.ScreenHandler;
import net.minecraft.screen.slot.SlotActionType;
+import net.minecraft.text.Text;
+import net.minecraft.text.TranslatableText;
import io.github.cottonmc.cotton.gui.GuiDescription;
import io.github.cottonmc.cotton.gui.ValidatedSlot;
import io.github.cottonmc.cotton.gui.client.BackgroundPainter;
import io.github.cottonmc.cotton.gui.impl.VisualLogger;
+import io.github.cottonmc.cotton.gui.impl.client.NarrationMessages;
+import io.github.cottonmc.cotton.gui.widget.data.InputResult;
import io.github.cottonmc.cotton.gui.widget.icon.Icon;
import org.jetbrains.annotations.Nullable;
@@ -75,10 +82,12 @@ public class WItemSlot extends WWidget {
private boolean insertingAllowed = true;
private boolean takingAllowed = true;
private int focusedSlot = -1;
+ private int hoveredSlot = -1;
private Predicate<ItemStack> filter = DEFAULT_FILTER;
private final Set<ChangeListener> listeners = new HashSet<>();
public WItemSlot(Inventory inventory, int startIndex, int slotsWide, int slotsHigh, boolean big) {
+ this();
this.inventory = inventory;
this.startIndex = startIndex;
this.slotsWide = slotsWide;
@@ -87,7 +96,12 @@ public class WItemSlot extends WWidget {
//this.ltr = ltr;
}
- private WItemSlot() {}
+ private WItemSlot() {
+ hoveredProperty().addListener((property, from, to) -> {
+ assert to != null;
+ if (!to) hoveredSlot = -1;
+ });
+ }
public static WItemSlot of(Inventory inventory, int index) {
WItemSlot w = new WItemSlot();
@@ -124,7 +138,12 @@ public class WItemSlot extends WWidget {
* @see WPlayerInvPanel
*/
public static WItemSlot ofPlayerStorage(Inventory inventory) {
- WItemSlot w = new WItemSlot();
+ WItemSlot w = new WItemSlot() {
+ @Override
+ protected Text getNarrationName() {
+ return inventory instanceof PlayerInventory inv ? inv.getDisplayName() : NarrationMessages.Vanilla.INVENTORY;
+ }
+ };
w.inventory = inventory;
w.startIndex = 9;
w.slotsWide = 9;
@@ -415,6 +434,14 @@ public class WItemSlot extends WWidget {
}
@Override
+ public InputResult onMouseMove(int x, int y) {
+ int slotX = x / 18;
+ int slotY = y / 18;
+ hoveredSlot = slotX + slotY * slotsWide;
+ return InputResult.PROCESSED;
+ }
+
+ @Override
public void onHidden() {
super.onHidden();
@@ -429,6 +456,34 @@ public class WItemSlot extends WWidget {
backgroundPainter = BackgroundPainter.SLOT;
}
+ @Environment(EnvType.CLIENT)
+ @Override
+ public void addNarrations(NarrationMessageBuilder builder) {
+ List<Text> parts = new ArrayList<>();
+ Text name = getNarrationName();
+ if (name != null) parts.add(name);
+
+ if (focusedSlot >= 0) {
+ parts.add(new TranslatableText(NarrationMessages.ITEM_SLOT_TITLE_KEY, focusedSlot + 1, slotsWide * slotsHigh));
+ } else if (hoveredSlot >= 0) {
+ parts.add(new TranslatableText(NarrationMessages.ITEM_SLOT_TITLE_KEY, hoveredSlot + 1, slotsWide * slotsHigh));
+ }
+
+ builder.put(NarrationPart.TITLE, parts.toArray(new Text[0]));
+ }
+
+ /**
+ * Returns a "narration name" for this slot.
+ * It's narrated before the slot index. One example of a narration name would be "hotbar" for the player's hotbar.
+ *
+ * @return the narration name, or null if there's none for this slot
+ * @since 4.2.0
+ */
+ @Nullable
+ protected Text getNarrationName() {
+ return null;
+ }
+
/**
* A listener for changes in an item slot.
*
diff --git a/src/main/java/io/github/cottonmc/cotton/gui/widget/WLabel.java b/src/main/java/io/github/cottonmc/cotton/gui/widget/WLabel.java
index 5c6b2da..83f0700 100644
--- a/src/main/java/io/github/cottonmc/cotton/gui/widget/WLabel.java
+++ b/src/main/java/io/github/cottonmc/cotton/gui/widget/WLabel.java
@@ -5,6 +5,8 @@ import net.fabricmc.api.Environment;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.font.TextRenderer;
import net.minecraft.client.gui.screen.Screen;
+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.LiteralText;
import net.minecraft.text.Style;
@@ -261,4 +263,10 @@ public class WLabel extends WWidget {
this.verticalAlignment = align;
return this;
}
+
+ @Environment(EnvType.CLIENT)
+ @Override
+ public void addNarrations(NarrationMessageBuilder builder) {
+ builder.put(NarrationPart.TITLE, text);
+ }
}
diff --git a/src/main/java/io/github/cottonmc/cotton/gui/widget/WLabeledSlider.java b/src/main/java/io/github/cottonmc/cotton/gui/widget/WLabeledSlider.java
index 911d547..2f972ff 100644
--- a/src/main/java/io/github/cottonmc/cotton/gui/widget/WLabeledSlider.java
+++ b/src/main/java/io/github/cottonmc/cotton/gui/widget/WLabeledSlider.java
@@ -2,12 +2,16 @@ package io.github.cottonmc.cotton.gui.widget;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
+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.text.TranslatableText;
import net.minecraft.util.Identifier;
import net.minecraft.util.math.Vec3f;
import io.github.cottonmc.cotton.gui.client.ScreenDrawing;
+import io.github.cottonmc.cotton.gui.impl.client.NarrationMessages;
import io.github.cottonmc.cotton.gui.widget.data.Axis;
import io.github.cottonmc.cotton.gui.widget.data.HorizontalAlignment;
import org.jetbrains.annotations.Nullable;
@@ -212,6 +216,17 @@ public class WLabeledSlider extends WAbstractSlider {
ScreenDrawing.texturedRect(matrices, x + halfWidth, y, halfWidth, 20, texture, buttonEndLeft, buttonTop, 200 * px, buttonTop + buttonHeight, 0xFFFFFFFF);
}
+ @Environment(EnvType.CLIENT)
+ @Override
+ public void addNarrations(NarrationMessageBuilder builder) {
+ if (getLabel() != null) {
+ builder.put(NarrationPart.TITLE, new TranslatableText(NarrationMessages.LABELED_SLIDER_TITLE_KEY, getLabel(), value, min, max));
+ builder.put(NarrationPart.USAGE, NarrationMessages.SLIDER_USAGE);
+ } else {
+ super.addNarrations(builder);
+ }
+ }
+
/**
* A label updater updates the label of a slider based on the current value.
*
diff --git a/src/main/java/io/github/cottonmc/cotton/gui/widget/WPanel.java b/src/main/java/io/github/cottonmc/cotton/gui/widget/WPanel.java
index 912c1c0..204ea93 100644
--- a/src/main/java/io/github/cottonmc/cotton/gui/widget/WPanel.java
+++ b/src/main/java/io/github/cottonmc/cotton/gui/widget/WPanel.java
@@ -7,6 +7,7 @@ import net.minecraft.client.util.math.MatrixStack;
import io.github.cottonmc.cotton.gui.GuiDescription;
import io.github.cottonmc.cotton.gui.client.BackgroundPainter;
import io.github.cottonmc.cotton.gui.widget.data.Insets;
+import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Nullable;
import java.util.AbstractList;
@@ -247,6 +248,17 @@ public abstract class WPanel extends WWidget {
}
}
+ /**
+ * {@return a stream of all visible widgets in this panel}
+ *
+ * @experimental
+ * @since 4.2.0
+ */
+ @ApiStatus.Experimental
+ public final Stream<WWidget> streamChildren() {
+ return children.stream();
+ }
+
@Override
public String toString() {
return getClass().getSimpleName() + " {\n" + children.stream().map(Object::toString).map(x -> x + ",").flatMap(x -> Stream.of(x.split("\n")).filter(y -> !y.isEmpty()).map(y -> "\t" + y)).collect(Collectors.joining("\n")) + "\n}";
diff --git a/src/main/java/io/github/cottonmc/cotton/gui/widget/WPlayerInvPanel.java b/src/main/java/io/github/cottonmc/cotton/gui/widget/WPlayerInvPanel.java
index c105924..af4a159 100644
--- a/src/main/java/io/github/cottonmc/cotton/gui/widget/WPlayerInvPanel.java
+++ b/src/main/java/io/github/cottonmc/cotton/gui/widget/WPlayerInvPanel.java
@@ -3,9 +3,11 @@ package io.github.cottonmc.cotton.gui.widget;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.minecraft.entity.player.PlayerInventory;
+import net.minecraft.text.Text;
import io.github.cottonmc.cotton.gui.GuiDescription;
import io.github.cottonmc.cotton.gui.client.BackgroundPainter;
+import io.github.cottonmc.cotton.gui.impl.client.NarrationMessages;
import org.jetbrains.annotations.Nullable;
/**
@@ -54,7 +56,12 @@ public class WPlayerInvPanel extends WPlainPanel {
}
inv = WItemSlot.ofPlayerStorage(playerInventory);
- hotbar = WItemSlot.of(playerInventory, 0, 9, 1);
+ hotbar = new WItemSlot(playerInventory, 0, 9, 1, false) {
+ @Override
+ protected Text getNarrationName() {
+ return NarrationMessages.PLAYER_INVENTORY_HOTBAR;
+ }
+ };
this.add(inv, 0, y);
this.add(hotbar, 0, y + 58);
}
diff --git a/src/main/java/io/github/cottonmc/cotton/gui/widget/WScrollBar.java b/src/main/java/io/github/cottonmc/cotton/gui/widget/WScrollBar.java
index 0eb7818..2882dbd 100644
--- a/src/main/java/io/github/cottonmc/cotton/gui/widget/WScrollBar.java
+++ b/src/main/java/io/github/cottonmc/cotton/gui/widget/WScrollBar.java
@@ -2,10 +2,13 @@ package io.github.cottonmc.cotton.gui.widget;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
+import net.minecraft.client.gui.screen.narration.NarrationMessageBuilder;
+import net.minecraft.client.gui.screen.narration.NarrationPart;
import net.minecraft.client.util.math.MatrixStack;
import io.github.cottonmc.cotton.gui.client.LibGui;
import io.github.cottonmc.cotton.gui.client.ScreenDrawing;
+import io.github.cottonmc.cotton.gui.impl.client.NarrationMessages;
import io.github.cottonmc.cotton.gui.widget.data.Axis;
import io.github.cottonmc.cotton.gui.widget.data.InputResult;
@@ -264,4 +267,11 @@ public class WScrollBar extends WWidget {
}
if (this.value<0) this.value = 0;
}
+
+ @Environment(EnvType.CLIENT)
+ @Override
+ public void addNarrations(NarrationMessageBuilder builder) {
+ builder.put(NarrationPart.TITLE, NarrationMessages.SCROLL_BAR_TITLE);
+ builder.put(NarrationPart.USAGE, NarrationMessages.SLIDER_USAGE);
+ }
}
diff --git a/src/main/java/io/github/cottonmc/cotton/gui/widget/WTabPanel.java b/src/main/java/io/github/cottonmc/cotton/gui/widget/WTabPanel.java
index 2ee7e92..718e44b 100644
--- a/src/main/java/io/github/cottonmc/cotton/gui/widget/WTabPanel.java
+++ b/src/main/java/io/github/cottonmc/cotton/gui/widget/WTabPanel.java
@@ -4,16 +4,20 @@ import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.font.TextRenderer;
+import net.minecraft.client.gui.screen.narration.NarrationMessageBuilder;
+import net.minecraft.client.gui.screen.narration.NarrationPart;
import net.minecraft.client.sound.PositionedSoundInstance;
import net.minecraft.client.util.math.MatrixStack;
import net.minecraft.sound.SoundEvents;
import net.minecraft.text.Text;
+import net.minecraft.text.TranslatableText;
import net.minecraft.util.Identifier;
import io.github.cottonmc.cotton.gui.client.BackgroundPainter;
import io.github.cottonmc.cotton.gui.client.LibGui;
import io.github.cottonmc.cotton.gui.client.ScreenDrawing;
import io.github.cottonmc.cotton.gui.impl.LibGuiCommon;
+import io.github.cottonmc.cotton.gui.impl.client.NarrationMessages;
import io.github.cottonmc.cotton.gui.widget.data.Axis;
import io.github.cottonmc.cotton.gui.widget.data.HorizontalAlignment;
import io.github.cottonmc.cotton.gui.widget.data.InputResult;
@@ -354,10 +358,23 @@ public class WTabPanel extends WPanel {
}
}
+ @Environment(EnvType.CLIENT)
@Override
public void addTooltip(TooltipBuilder tooltip) {
data.addTooltip(tooltip);
}
+
+ @Environment(EnvType.CLIENT)
+ @Override
+ public void addNarrations(NarrationMessageBuilder builder) {
+ Text label = data.getTitle();
+
+ if (label != null) {
+ builder.put(NarrationPart.TITLE, new TranslatableText(NarrationMessages.TAB_TITLE_KEY, label));
+ }
+
+ builder.put(NarrationPart.POSITION, new TranslatableText(NarrationMessages.TAB_POSITION_KEY, tabWidgets.indexOf(this) + 1, tabWidgets.size()));
+ }
}
/**
diff --git a/src/main/java/io/github/cottonmc/cotton/gui/widget/WText.java b/src/main/java/io/github/cottonmc/cotton/gui/widget/WText.java
index 96f78ed..8a4f03e 100644
--- a/src/main/java/io/github/cottonmc/cotton/gui/widget/WText.java
+++ b/src/main/java/io/github/cottonmc/cotton/gui/widget/WText.java
@@ -4,6 +4,8 @@ import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.font.TextRenderer;
+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.OrderedText;
import net.minecraft.text.Style;
@@ -253,4 +255,10 @@ public class WText extends WWidget {
this.verticalAlignment = verticalAlignment;
return this;
}
+
+ @Environment(EnvType.CLIENT)
+ @Override
+ public void addNarrations(NarrationMessageBuilder builder) {
+ builder.put(NarrationPart.TITLE, text);
+ }
}
diff --git a/src/main/java/io/github/cottonmc/cotton/gui/widget/WTextField.java b/src/main/java/io/github/cottonmc/cotton/gui/widget/WTextField.java
index 60ae5d4..e9e2692 100644
--- a/src/main/java/io/github/cottonmc/cotton/gui/widget/WTextField.java
+++ b/src/main/java/io/github/cottonmc/cotton/gui/widget/WTextField.java
@@ -7,6 +7,8 @@ import net.fabricmc.api.Environment;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.font.TextRenderer;
import net.minecraft.client.gui.screen.Screen;
+import net.minecraft.client.gui.screen.narration.NarrationMessageBuilder;
+import net.minecraft.client.gui.screen.narration.NarrationPart;
import net.minecraft.client.render.BufferBuilder;
import net.minecraft.client.render.BufferRenderer;
import net.minecraft.client.render.GameRenderer;
@@ -16,11 +18,13 @@ import net.minecraft.client.render.VertexFormats;
import net.minecraft.client.util.math.MatrixStack;
import net.minecraft.text.LiteralText;
import net.minecraft.text.Text;
+import net.minecraft.text.TranslatableText;
import net.minecraft.util.math.MathHelper;
import net.minecraft.util.math.Matrix4f;
import io.github.cottonmc.cotton.gui.client.BackgroundPainter;
import io.github.cottonmc.cotton.gui.client.ScreenDrawing;
+import io.github.cottonmc.cotton.gui.impl.client.NarrationMessages;
import io.github.cottonmc.cotton.gui.widget.data.InputResult;
import org.jetbrains.annotations.Nullable;
import org.lwjgl.glfw.GLFW;
@@ -431,7 +435,16 @@ public class WTextField extends WWidget {
}
}
}
-
+
+ @Override
+ public void addNarrations(NarrationMessageBuilder builder) {
+ builder.put(NarrationPart.TITLE, new TranslatableText(NarrationMessages.TEXT_FIELD_TITLE_KEY, text));
+
+ if (suggestion != null) {
+ builder.put(NarrationPart.HINT, new TranslatableText(NarrationMessages.TEXT_FIELD_SUGGESTION_KEY, suggestion));
+ }
+ }
+
/**
* From an X offset past the left edge of a TextRenderer.draw, finds out what the closest caret
* position (division between letters) is.
diff --git a/src/main/java/io/github/cottonmc/cotton/gui/widget/WToggleButton.java b/src/main/java/io/github/cottonmc/cotton/gui/widget/WToggleButton.java
index 8953f4d..ca0285f 100644
--- a/src/main/java/io/github/cottonmc/cotton/gui/widget/WToggleButton.java
+++ b/src/main/java/io/github/cottonmc/cotton/gui/widget/WToggleButton.java
@@ -3,15 +3,19 @@ package io.github.cottonmc.cotton.gui.widget;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.gui.screen.narration.NarrationMessageBuilder;
+import net.minecraft.client.gui.screen.narration.NarrationPart;
import net.minecraft.client.sound.PositionedSoundInstance;
import net.minecraft.client.util.math.MatrixStack;
import net.minecraft.sound.SoundEvents;
import net.minecraft.text.Text;
+import net.minecraft.text.TranslatableText;
import net.minecraft.util.Identifier;
import io.github.cottonmc.cotton.gui.client.LibGui;
import io.github.cottonmc.cotton.gui.client.ScreenDrawing;
import io.github.cottonmc.cotton.gui.impl.LibGuiCommon;
+import io.github.cottonmc.cotton.gui.impl.client.NarrationMessages;
import io.github.cottonmc.cotton.gui.widget.data.InputResult;
import io.github.cottonmc.cotton.gui.widget.data.Texture;
import org.jetbrains.annotations.Nullable;
@@ -202,4 +206,25 @@ public class WToggleButton extends WWidget {
this.focusImage = focusImage;
return this;
}
+
+ @Environment(EnvType.CLIENT)
+ @Override
+ public void addNarrations(NarrationMessageBuilder builder) {
+ Text onOff = isOn ? NarrationMessages.TOGGLE_BUTTON_ON : NarrationMessages.TOGGLE_BUTTON_OFF;
+ Text title;
+
+ if (label != null) {
+ title = new TranslatableText(NarrationMessages.TOGGLE_BUTTON_NAMED_KEY, label, onOff);
+ } else {
+ title = new TranslatableText(NarrationMessages.TOGGLE_BUTTON_UNNAMED_KEY, onOff);
+ }
+
+ builder.put(NarrationPart.TITLE, title);
+
+ if (isFocused()) {
+ builder.put(NarrationPart.USAGE, NarrationMessages.Vanilla.BUTTON_USAGE_FOCUSED);
+ } else if (isHovered()) {
+ builder.put(NarrationPart.USAGE, NarrationMessages.Vanilla.BUTTON_USAGE_HOVERED);
+ }
+ }
}
diff --git a/src/main/java/io/github/cottonmc/cotton/gui/widget/WWidget.java b/src/main/java/io/github/cottonmc/cotton/gui/widget/WWidget.java
index 2a6ae5e..27e62f8 100644
--- a/src/main/java/io/github/cottonmc/cotton/gui/widget/WWidget.java
+++ b/src/main/java/io/github/cottonmc/cotton/gui/widget/WWidget.java
@@ -4,16 +4,21 @@ import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.gui.screen.Screen;
+import net.minecraft.client.gui.screen.narration.NarrationMessageBuilder;
import net.minecraft.client.util.math.MatrixStack;
import io.github.cottonmc.cotton.gui.GuiDescription;
import io.github.cottonmc.cotton.gui.impl.VisualLogger;
import io.github.cottonmc.cotton.gui.widget.data.InputResult;
+import io.github.cottonmc.cotton.gui.widget.data.ObservableProperty;
+import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Nullable;
import org.lwjgl.glfw.GLFW;
/**
* The base class for all widgets.
+ *
+ * @properties
*/
public class WWidget {
private static final VisualLogger LOGGER = new VisualLogger(WWidget.class);
@@ -41,6 +46,8 @@ public class WWidget {
@Nullable
protected GuiDescription host;
+ private final ObservableProperty<Boolean> hovered = ObservableProperty.of(false).nonnullValues().setName("WWidget.hovered");
+
/**
* Sets the location of this widget relative to its parent.
*
@@ -457,6 +464,68 @@ public class WWidget {
}
/**
+ * Returns whether the user is hovering over this widget.
+ * The result is an <i>observable property</i> that can be modified and listened to.
+ *
+ * @experimental
+ * @return the {@code hovered} property
+ * @since 4.2.0
+ */
+ @ApiStatus.Experimental
+ public ObservableProperty<Boolean> hoveredProperty() {
+ return hovered;
+ }
+
+ /**
+ * Returns whether the user is hovering over this widget.
+ * This is equivalent to calling <code>{@link #hoveredProperty()}.get()</code>.
+ *
+ * @experimental
+ * @return true if this widget is hovered, false otherwise
+ * @since 4.2.0
+ */
+ @ApiStatus.Experimental
+ public final boolean isHovered() {
+ return hoveredProperty().get();
+ }
+
+ /**
+ * Sets the {@link #hoveredProperty() hovered} property.
+ *
+ * @experimental
+ * @param hovered the new value; true if hovered, false otherwise
+ * @since 4.2.0
+ */
+ @ApiStatus.Experimental
+ public final void setHovered(boolean hovered) {
+ hoveredProperty().set(hovered);
+ }
+
+ /**
+ * {@return whether this widget can be narrated}
+ *
+ * @see #addNarrations(NarrationMessageBuilder)
+ * @since 4.2.0
+ */
+ public boolean isNarratable() {
+ return true;
+ }
+
+ /**
+ * Adds the narrations of this widget to a narration builder.
+ * Narrations will only apply if this widget {@linkplain #isNarratable() is narratable}.
+ *
+ * <p>As of LibGui 4.2.0, the widget also needs to be {@linkplain #canFocus() focusable}, but that is
+ * planned to be changed in the future to include "hoverable" widgets.
+ *
+ * @param builder the narration builder, cannot be null
+ * @since 4.2.0
+ */
+ @Environment(EnvType.CLIENT)
+ public void addNarrations(NarrationMessageBuilder builder) {
+ }
+
+ /**
* Tests if the provided key code is an activation key for widgets.
*
* <p>The activation keys are Enter, keypad Enter, and Space.
diff --git a/src/main/java/io/github/cottonmc/cotton/gui/widget/data/ObservableProperty.java b/src/main/java/io/github/cottonmc/cotton/gui/widget/data/ObservableProperty.java
new file mode 100644
index 0000000..eb92387
--- /dev/null
+++ b/src/main/java/io/github/cottonmc/cotton/gui/widget/data/ObservableProperty.java
@@ -0,0 +1,162 @@
+package io.github.cottonmc.cotton.gui.widget.data;
+
+import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * An observable mutable property. Observable properties are containers for values
+ * that can be modified and listened to.
+ *
+ * <p>The naming convention for {@code ObservableProperty} getters follows the convention
+ * {@code <property name>Property}. For example, the {@code WWidget.hovered} property can be retrieved with
+ * {@link io.github.cottonmc.cotton.gui.widget.WWidget#hoveredProperty() hoveredProperty()}.
+ *
+ * @experimental
+ * @param <T> the contained value type
+ * @since 4.2.0
+ */
+@ApiStatus.Experimental
+public final class ObservableProperty<T> implements ObservableView<T> {
+ private boolean hasValue;
+ private T value;
+ private final List<ChangeListener<? super T>> listeners = new ArrayList<>();
+ private boolean allowNull = true;
+ private String name = "<unnamed>";
+
+ private ObservableProperty(@Nullable T value, boolean hasValue) {
+ this.value = value;
+ this.hasValue = hasValue;
+ }
+
+ public static <T> ObservableProperty<T> lateinit() {
+ return new ObservableProperty<>(null, false);
+ }
+
+ public static <T> ObservableProperty<T> of(T initialValue) {
+ return new ObservableProperty<>(initialValue, true);
+ }
+
+ @Override
+ public boolean hasValue() {
+ return hasValue;
+ }
+
+ @Override
+ public T get() {
+ if (!hasValue) {
+ throw new IllegalStateException("Property " + name + " not initialized!");
+ }
+
+ return value;
+ }
+
+ /**
+ * Sets this property to a constant value.
+ *
+ * @param value the new value
+ * @throws NullPointerException if the value is null and nulls aren't allowed
+ */
+ public void set(T value) {
+ if (value == null && !allowNull) throw new NullPointerException("Trying to set null value for nonnull property " + name);
+ T oldValue = this.value;
+ this.value = value;
+ hasValue = true;
+
+ if (oldValue != value) {
+ for (ChangeListener<? super T> listener : listeners) {
+ listener.onPropertyChange(this, oldValue, value);
+ }
+ }
+ }
+
+ /**
+ * Clears the current value, if any, from this property.
+ */
+ public void clear() {
+ T oldValue = value;
+ value = null;
+ hasValue = false;
+
+ if (oldValue != null) {
+ for (ChangeListener<? super T> listener : listeners) {
+ listener.onPropertyChange(this, oldValue, null);
+ }
+ }
+ }
+
+ /**
+ * Prevents this property from accepting null values.
+ *
+ * @return this property
+ */
+ public ObservableProperty<T> nonnullValues() {
+ allowNull = false;
+ return this;
+ }
+
+ /**
+ * Returns a read-only view of this property.
+ * The result is not an instance of {@link ObservableProperty},
+ * and thus can't be mutated.
+ *
+ * @return an observable view of this property
+ */
+ public ObservableView<T> readOnly() {
+ // Missing delegates from Kotlin... :(
+ return new ObservableView<>() {
+ @Override
+ public boolean hasValue() {
+ return ObservableProperty.this.hasValue();
+ }
+
+ @Override
+ public T get() {
+ return ObservableProperty.this.get();
+ }
+
+ @Override
+ public void addListener(ChangeListener<? super T> listener) {
+ ObservableProperty.this.addListener(listener);
+ }
+
+ @Override
+ public void removeListener(ChangeListener<? super T> listener) {
+ ObservableProperty.this.removeListener(listener);
+ }
+ };
+ }
+
+ /**
+ * {@return the name of this property}
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Sets the name of this property, which is used in debug messages.
+ *
+ * @param name the new name
+ * @return this property
+ */
+ public ObservableProperty<T> setName(String name) {
+ this.name = Objects.requireNonNull(name, "name");
+ return this;
+ }
+
+ @Override
+ public void addListener(ChangeListener<? super T> listener) {
+ Objects.requireNonNull(listener);
+ listeners.add(listener);
+ }
+
+ @Override
+ public void removeListener(ChangeListener<? super T> listener) {
+ Objects.requireNonNull(listener);
+ listeners.remove(listener);
+ }
+}
diff --git a/src/main/java/io/github/cottonmc/cotton/gui/widget/data/ObservableView.java b/src/main/java/io/github/cottonmc/cotton/gui/widget/data/ObservableView.java
new file mode 100644
index 0000000..5f6a33c
--- /dev/null
+++ b/src/main/java/io/github/cottonmc/cotton/gui/widget/data/ObservableView.java
@@ -0,0 +1,75 @@
+package io.github.cottonmc.cotton.gui.widget.data;
+
+import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Optional;
+import java.util.function.Supplier;
+
+/**
+ * A read-only {@linkplain ObservableProperty observable property}.
+ *
+ * @experimental
+ * @param <T> the contained value type
+ * @since 4.2.0
+ */
+@ApiStatus.Experimental
+public interface ObservableView<T> extends Supplier<T> {
+ /**
+ * {@return whether this property has been set to a value}
+ */
+ boolean hasValue();
+
+ /**
+ * {@return the value of this property}
+ * @throws IllegalStateException if not initialized
+ * @see #hasValue()
+ */
+ @Override
+ T get();
+
+ /**
+ * {@return the value of this property, or null if not initialized}
+ */
+ default @Nullable T getOrNull() {
+ return hasValue() ? get() : null;
+ }
+
+ /**
+ * {@return the nonnull value of this property, or {@link Optional#empty()} if null or not initialized}
+ */
+ default Optional<T> find() {
+ return Optional.ofNullable(getOrNull());
+ }
+
+ /**
+ * Adds a change listener to this property view.
+ *
+ * @param listener the added listener
+ */
+ void addListener(ChangeListener<? super T> listener);
+
+ /**
+ * Removes a change listener from this property view if present.
+ *
+ * @param listener the removed listener
+ */
+ void removeListener(ChangeListener<? super T> listener);
+
+ /**
+ * A listener for changes in observable views and properties.
+ *
+ * @param <T> the value type listened to
+ */
+ @FunctionalInterface
+ interface ChangeListener<T> {
+ /**
+ * Handles a change in an observable property.
+ *
+ * @param property the changed property or view
+ * @param from the previous value, or null if not set before
+ * @param to the new value, or null if cleared
+ */
+ void onPropertyChange(ObservableView<? extends T> property, @Nullable T from, @Nullable T to);
+ }
+}