diff --git a/src/main/java/com/cleanroommc/modularui/ClientProxy.java b/src/main/java/com/cleanroommc/modularui/ClientProxy.java
index e211f19d4..fa175f380 100644
--- a/src/main/java/com/cleanroommc/modularui/ClientProxy.java
+++ b/src/main/java/com/cleanroommc/modularui/ClientProxy.java
@@ -8,6 +8,7 @@
import com.cleanroommc.modularui.factory.inventory.InventoryTypes;
import com.cleanroommc.modularui.holoui.HoloScreenEntity;
import com.cleanroommc.modularui.holoui.ScreenEntityRender;
+import com.cleanroommc.modularui.hud.HudManager;
import com.cleanroommc.modularui.network.ModularNetwork;
import com.cleanroommc.modularui.screen.ClientScreenHandler;
import com.cleanroommc.modularui.test.TestItem;
@@ -66,6 +67,8 @@ void preInit(FMLPreInitializationEvent event) {
// registered to both buses since handled events are not bound to a single bus
FMLCommonHandler.instance().bus().register(clientScreenHandler);
MinecraftForge.EVENT_BUS.register(clientScreenHandler);
+ // register the HUD manager on the Forge event bus
+ HudManager.init();
AnimatorManager.init();
if (ModularUIConfig.enableTestGuis) {
diff --git a/src/main/java/com/cleanroommc/modularui/hud/HudContext.java b/src/main/java/com/cleanroommc/modularui/hud/HudContext.java
new file mode 100644
index 000000000..412312b4a
--- /dev/null
+++ b/src/main/java/com/cleanroommc/modularui/hud/HudContext.java
@@ -0,0 +1,27 @@
+package com.cleanroommc.modularui.hud;
+
+import com.cleanroommc.modularui.screen.ModularScreen;
+import com.cleanroommc.modularui.screen.viewport.ModularGuiContext;
+
+import cpw.mods.fml.relauncher.Side;
+import cpw.mods.fml.relauncher.SideOnly;
+
+import org.jetbrains.annotations.ApiStatus;
+
+/**
+ * A read-only context for HUD elements: disables the hover/focus/drag subsystem, since HUD
+ * elements are display-only.
+ */
+@ApiStatus.Internal
+@SideOnly(Side.CLIENT)
+public class HudContext extends ModularGuiContext {
+
+ public HudContext(ModularScreen screen) {
+ super(screen);
+ }
+
+ @Override
+ public void onFrameUpdate() {
+ // no-op: display-only, no hover/focus/drag
+ }
+}
diff --git a/src/main/java/com/cleanroommc/modularui/hud/HudElement.java b/src/main/java/com/cleanroommc/modularui/hud/HudElement.java
new file mode 100644
index 000000000..0dcdcb3fe
--- /dev/null
+++ b/src/main/java/com/cleanroommc/modularui/hud/HudElement.java
@@ -0,0 +1,161 @@
+package com.cleanroommc.modularui.hud;
+
+import com.cleanroommc.modularui.screen.ModularPanel;
+import com.cleanroommc.modularui.screen.ModularScreen;
+import com.cleanroommc.modularui.screen.viewport.ModularGuiContext;
+
+import cpw.mods.fml.relauncher.Side;
+import cpw.mods.fml.relauncher.SideOnly;
+
+import lombok.Getter;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Objects;
+import java.util.function.Function;
+
+/**
+ * A single HUD element rendered on top of the game world.
+ *
panelCreator) {
+ Objects.requireNonNull(owner, "The owner must not be null!");
+ Objects.requireNonNull(panelCreator, "The panel creator must not be null!");
+ this.screen = new HudScreen(owner, panelCreator);
+ this.wrapper = new HudWrapper(this.screen);
+ }
+
+ /**
+ * Sets whether this element is rendered when no screen is open (in-game).
+ *
+ * @param visibleInWorld true to render in-world
+ * @return this
+ */
+ public HudElement visibleInWorld(boolean visibleInWorld) {
+ this.visibleInWorld = visibleInWorld;
+ return this;
+ }
+
+ /**
+ * Sets whether and where this element is drawn while a {@code GuiScreen} (inventory, etc.) is
+ * open. Visible underneath open screens by default, not over them: most HUD elements (persistent
+ * overlays, ambient status) should not cover an open screen's own content.
+ *
+ * @param visibleInGui true to render at all while a screen is open
+ * @param isOverGui if visible, true to draw on top of the screen, false to draw underneath it
+ * @return this
+ */
+ public HudElement visibleInGui(boolean visibleInGui, boolean isOverGui) {
+ this.visibleInGui = visibleInGui;
+ this.overGui = isOverGui;
+ return this;
+ }
+
+ /**
+ * Sets the render priority. Higher values are drawn on top of lower values.
+ *
+ * @param renderPriority priority value
+ * @return this
+ */
+ public HudElement renderPriority(int renderPriority) {
+ this.renderPriority = renderPriority;
+ return this;
+ }
+
+ /**
+ * Enables or disables this element. Disabled elements are not rendered or ticked.
+ *
+ * @param enabled true to enable
+ * @return this
+ */
+ public HudElement enabled(boolean enabled) {
+ this.enabled = enabled;
+ return this;
+ }
+
+ public ModularPanel getPanel() {
+ return screen.getMainPanel();
+ }
+
+ /**
+ * @return true if this element should be rendered in-world (no screen open)
+ */
+ public boolean isVisibleInWorld() {
+ return enabled && visibleInWorld;
+ }
+
+ /**
+ * @return true if this element should be rendered on top of an open {@code GuiScreen}
+ */
+ public boolean isVisibleOverGui() {
+ return enabled && visibleInGui && overGui;
+ }
+
+ /**
+ * @return true if this element should be rendered underneath an open {@code GuiScreen}
+ */
+ public boolean isVisibleUnderGui() {
+ return enabled && visibleInGui && !overGui;
+ }
+
+ /**
+ * @return true if this element can be ticked (enabled)
+ */
+ boolean canDraw() {
+ return enabled;
+ }
+
+ @Nullable
+ public String getOwner() {
+ return screen.getOwner();
+ }
+
+ @Override
+ public String toString() {
+ return "HudElement[" + screen.getOwner() + ":" + screen.getName() + "]";
+ }
+}
diff --git a/src/main/java/com/cleanroommc/modularui/hud/HudManager.java b/src/main/java/com/cleanroommc/modularui/hud/HudManager.java
new file mode 100644
index 000000000..941917312
--- /dev/null
+++ b/src/main/java/com/cleanroommc/modularui/hud/HudManager.java
@@ -0,0 +1,175 @@
+package com.cleanroommc.modularui.hud;
+
+import com.cleanroommc.modularui.utils.GlStateManager;
+
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.gui.GuiScreen;
+import net.minecraftforge.client.event.GuiScreenEvent;
+import net.minecraftforge.client.event.RenderGameOverlayEvent;
+import net.minecraftforge.client.event.RenderGameOverlayEvent.ElementType;
+import cpw.mods.fml.common.eventhandler.EventPriority;
+import cpw.mods.fml.common.eventhandler.SubscribeEvent;
+import cpw.mods.fml.common.gameevent.TickEvent;
+import cpw.mods.fml.relauncher.Side;
+import cpw.mods.fml.relauncher.SideOnly;
+
+import org.jetbrains.annotations.ApiStatus;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.function.Predicate;
+
+/**
+ * Manages all registered {@link HudElement HUD elements}.
+ *
+ * Subscribes to Forge render/tick events and draws each visible element. HUD elements are
+ * display-only (no input capture). See {@link HudElement} for details.
+ */
+@ApiStatus.Internal
+@SideOnly(Side.CLIENT)
+public class HudManager {
+
+ private static final List elements = new ArrayList<>();
+ private static int lastWidth = -1;
+ private static int lastHeight = -1;
+ private static boolean initialized = false;
+ private static final HudManager INSTANCE = new HudManager();
+
+ private HudManager() {}
+
+ /**
+ * Registers the HUD manager on the Forge event bus. Must be called once during client init,
+ * typically from {@code ClientProxy.preInit()}.
+ */
+ public static void init() {
+ if (initialized) return;
+ initialized = true;
+ // must be an instance method - Forge's ASMEventHandler crashes on a static @SubscribeEvent
+ net.minecraftforge.common.MinecraftForge.EVENT_BUS.register(INSTANCE);
+ }
+
+ /**
+ * Registers a HUD element so it is rendered and ticked.
+ *
+ * @param element the element to register
+ */
+ public static void register(HudElement element) {
+ if (element == null) throw new NullPointerException("Element must not be null!");
+ if (!elements.contains(element)) {
+ elements.add(element);
+ }
+ }
+
+ /**
+ * Unregisters a HUD element.
+ *
+ * @param element the element to unregister
+ * @return true if the element was registered and is now removed
+ */
+ public static boolean unregister(HudElement element) {
+ return elements.remove(element);
+ }
+
+ /**
+ * Removes all registered HUD elements.
+ */
+ public static void clear() {
+ elements.clear();
+ }
+
+ /**
+ * Renders HUD elements over the game world (no screen open), after the vanilla HUD.
+ */
+ @SubscribeEvent(priority = EventPriority.LOW)
+ public void onRenderGameOverlay(RenderGameOverlayEvent.Post event) {
+ if (event.type != ElementType.ALL) return;
+ Minecraft mc = Minecraft.getMinecraft();
+ if (mc.currentScreen != null) return;
+ drawVisible(event.mouseX, event.mouseY, event.partialTicks, HudElement::isVisibleInWorld);
+ }
+
+ /**
+ * Renders HUD elements underneath an open {@link GuiScreen} (before the screen draws).
+ */
+ @SubscribeEvent(priority = EventPriority.LOW)
+ public void onGuiDrawPre(GuiScreenEvent.DrawScreenEvent.Pre event) {
+ drawVisible(event.mouseX, event.mouseY, event.renderPartialTicks, HudElement::isVisibleUnderGui);
+ }
+
+ /**
+ * Renders HUD elements over an open {@link GuiScreen} (after the screen draws).
+ */
+ @SubscribeEvent(priority = EventPriority.HIGH)
+ public void onGuiDrawPost(GuiScreenEvent.DrawScreenEvent.Post event) {
+ drawVisible(event.mouseX, event.mouseY, event.renderPartialTicks, HudElement::isVisibleOverGui);
+ }
+
+ private static void drawVisible(int mouseX, int mouseY, float partialTicks, Predicate visibility) {
+ if (elements.isEmpty()) return;
+ checkResize();
+ boolean anyVisible = false;
+ for (HudElement e : elements) {
+ if (visibility.test(e)) {
+ anyVisible = true;
+ e.getScreen()
+ .getContext()
+ .updateState(mouseX, mouseY, partialTicks);
+ }
+ }
+ if (!anyVisible) return;
+
+ // Render from lowest to highest priority so higher priority draws on top.
+ elements.stream()
+ .filter(visibility)
+ .sorted(Comparator.comparingInt(HudElement::getRenderPriority))
+ .forEach(e -> {
+ GlStateManager.enableBlend();
+ GlStateManager.color(1f, 1f, 1f, 1f);
+ e.getScreen()
+ .drawScreen();
+ e.getScreen()
+ .drawForeground();
+ });
+ }
+
+ /**
+ * Ticks all enabled, drawable HUD elements at 20 Hz (client tick).
+ */
+ @SubscribeEvent
+ public void onClientTick(TickEvent.ClientTickEvent event) {
+ if (event.phase != TickEvent.Phase.END) return;
+ if (elements.isEmpty()) return;
+ for (HudElement e : elements) {
+ if (e.canDraw()) {
+ e.getScreen()
+ .onUpdate();
+ }
+ }
+ }
+
+ private static void checkResize() {
+ Minecraft mc = Minecraft.getMinecraft();
+ int scaledWidth;
+ int scaledHeight;
+ if (mc.currentScreen != null) {
+ scaledWidth = mc.currentScreen.width;
+ scaledHeight = mc.currentScreen.height;
+ } else {
+ net.minecraft.client.gui.ScaledResolution res = new net.minecraft.client.gui.ScaledResolution(mc, mc.displayWidth, mc.displayHeight);
+ scaledWidth = res.getScaledWidth();
+ scaledHeight = res.getScaledHeight();
+ }
+
+ boolean resized = scaledWidth != lastWidth || scaledHeight != lastHeight;
+ if (resized) {
+ lastWidth = scaledWidth;
+ lastHeight = scaledHeight;
+ }
+ for (HudElement e : elements) {
+ if (resized || !e.getScreen().getPanelManager().isOpen()) {
+ e.getScreen().onResize(scaledWidth, scaledHeight);
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/cleanroommc/modularui/hud/HudScreen.java b/src/main/java/com/cleanroommc/modularui/hud/HudScreen.java
new file mode 100644
index 000000000..967edf277
--- /dev/null
+++ b/src/main/java/com/cleanroommc/modularui/hud/HudScreen.java
@@ -0,0 +1,25 @@
+package com.cleanroommc.modularui.hud;
+
+import com.cleanroommc.modularui.screen.ModularPanel;
+import com.cleanroommc.modularui.screen.ModularScreen;
+import com.cleanroommc.modularui.screen.viewport.ModularGuiContext;
+
+import cpw.mods.fml.relauncher.Side;
+import cpw.mods.fml.relauncher.SideOnly;
+
+import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.function.Function;
+
+/**
+ * Internal subclass of {@link ModularScreen} used by {@link HudElement}.
+ */
+@ApiStatus.Internal
+@SideOnly(Side.CLIENT)
+class HudScreen extends ModularScreen {
+
+ HudScreen(@NotNull String owner, @NotNull Function panelCreator) {
+ super(owner, panelCreator, HudContext::new);
+ }
+}
diff --git a/src/main/java/com/cleanroommc/modularui/hud/HudWrapper.java b/src/main/java/com/cleanroommc/modularui/hud/HudWrapper.java
new file mode 100644
index 000000000..eee59f4bd
--- /dev/null
+++ b/src/main/java/com/cleanroommc/modularui/hud/HudWrapper.java
@@ -0,0 +1,37 @@
+package com.cleanroommc.modularui.hud;
+
+import com.cleanroommc.modularui.screen.ModularScreen;
+
+import net.minecraft.client.gui.GuiScreen;
+import cpw.mods.fml.relauncher.Side;
+import cpw.mods.fml.relauncher.SideOnly;
+
+import org.jetbrains.annotations.ApiStatus;
+
+/**
+ * A phantom {@link GuiScreen}, never set as {@code Minecraft.currentScreen} - it only exists to
+ * satisfy {@link ModularScreen#constructOverlay(GuiScreen)}, reusing the normal render pipeline
+ * for HUD elements. Input methods are inherited as no-ops, which is correct since HUD elements
+ * are display-only.
+ */
+@ApiStatus.Internal
+@SideOnly(Side.CLIENT)
+class HudWrapper extends GuiScreen {
+
+ private final ModularScreen screen;
+
+ HudWrapper(ModularScreen screen) {
+ this.screen = screen;
+ this.screen.constructOverlay(this);
+ }
+
+ @Override
+ public boolean doesGuiPauseGame() {
+ return false;
+ }
+
+ @Override
+ public String toString() {
+ return "HudWrapper(" + this.screen + ")";
+ }
+}
diff --git a/src/main/java/com/cleanroommc/modularui/screen/ModularScreen.java b/src/main/java/com/cleanroommc/modularui/screen/ModularScreen.java
index b8dc16b38..0f53769d4 100644
--- a/src/main/java/com/cleanroommc/modularui/screen/ModularScreen.java
+++ b/src/main/java/com/cleanroommc/modularui/screen/ModularScreen.java
@@ -80,7 +80,7 @@ public static ModularScreen getCurrent() {
private final String owner;
private final String name;
private final PanelManager panelManager;
- private final ModularGuiContext context = new ModularGuiContext(this);
+ private final ModularGuiContext context;
private final Map, List> guiActionListeners = new Object2ObjectOpenHashMap<>();
private final Object2ObjectArrayMap frameUpdates = new Object2ObjectArrayMap<>();
private final ScreenResizeNode resizeNode = new ScreenResizeNode(this);
@@ -122,12 +122,15 @@ public ModularScreen(@NotNull String owner, @NotNull ModularPanel mainPanel) {
* @param mainPanelCreator function which creates the main panel of this screen
*/
public ModularScreen(@NotNull String owner, @NotNull Function mainPanelCreator) {
- this(owner, Objects.requireNonNull(mainPanelCreator, "The main panel function must not be null!"), false);
+ this(owner, Objects.requireNonNull(mainPanelCreator, "The main panel function must not be null!"), ModularGuiContext::new);
}
- private ModularScreen(@NotNull String owner, @Nullable Function mainPanelCreator, boolean ignored) {
+ protected ModularScreen(@NotNull String owner, @Nullable Function mainPanelCreator,
+ @NotNull Function contextFactory) {
Objects.requireNonNull(owner, "The owner must not be null!");
+ Objects.requireNonNull(contextFactory, "The context factory must not be null!");
this.owner = owner;
+ this.context = contextFactory.apply(this);
ModularPanel mainPanel = mainPanelCreator != null ? mainPanelCreator.apply(this.context) : buildUI(this.context);
Objects.requireNonNull(mainPanel, "The main panel must not be null!");
this.name = mainPanel.getName();
@@ -138,7 +141,7 @@ private ModularScreen(@NotNull String owner, @Nullable Function element.accept(this.context));
GlStateManager.enableRescaleNormal();
- GlStateManager.enableLighting();
- RenderHelper.enableStandardItemLighting();
+ if (!isOverlay()) {
+ GlStateManager.enableLighting();
+ RenderHelper.enableStandardItemLighting();
+ }
+ GlStateManager.enableDepth();
GlStateManager.enableAlpha();
}
@@ -329,8 +340,10 @@ public void drawScreen() {
*/
public void drawForeground() {
GlStateManager.disableRescaleNormal();
- RenderHelper.disableStandardItemLighting();
- GlStateManager.disableLighting();
+ if (!isOverlay()) {
+ RenderHelper.disableStandardItemLighting();
+ GlStateManager.disableLighting();
+ }
GlStateManager.disableDepth();
GlStateManager.disableAlpha();
@@ -346,8 +359,11 @@ public void drawForeground() {
this.context.popViewport(null);
GlStateManager.enableRescaleNormal();
- GlStateManager.enableLighting();
- RenderHelper.enableStandardItemLighting();
+ if (!isOverlay()) {
+ GlStateManager.enableLighting();
+ RenderHelper.enableStandardItemLighting();
+ }
+ GlStateManager.enableDepth();
GlStateManager.enableAlpha();
}
@@ -526,7 +542,9 @@ public boolean onMouseDrag(int mouseButton, long timeSinceClick) {
return false;
}
- /** Lwjgl3 key press event */
+ /**
+ * Lwjgl3 key press event
+ */
@Optional.Method(modid = ModularUI.ModIds.LWJGL3IFY)
public void onKeyEvent(InputEvents.KeyEvent event) {
for (ModularPanel panel : this.panelManager.getOpenPanels()) {
@@ -539,7 +557,9 @@ public void onKeyEvent(InputEvents.KeyEvent event) {
}
}
- /** Lwjgl3 text input event */
+ /**
+ * Lwjgl3 text input event
+ */
@Optional.Method(modid = ModularUI.ModIds.LWJGL3IFY)
public void onTextEvent(InputEvents.TextEvent event) {
for (ModularPanel panel : this.panelManager.getOpenPanels()) {
diff --git a/src/main/java/com/cleanroommc/modularui/utils/Platform.java b/src/main/java/com/cleanroommc/modularui/utils/Platform.java
index 7a47cde2a..53adb21fc 100644
--- a/src/main/java/com/cleanroommc/modularui/utils/Platform.java
+++ b/src/main/java/com/cleanroommc/modularui/utils/Platform.java
@@ -135,7 +135,12 @@ public static void endDrawItem() {
}
public static void setupDrawFont() {
- setupDrawTex();
+ // blend must be on (with alpha blend func) or a text color's alpha channel - e.g. a fade-out
+ // effect - has no visual effect: FontRenderer still writes fully opaque pixels with GL_BLEND off.
+ setupDrawTex(true);
+ GlStateManager.tryBlendFuncSeparate(
+ GlStateManager.SourceFactor.SRC_ALPHA, GlStateManager.DestFactor.ONE_MINUS_SRC_ALPHA,
+ GlStateManager.SourceFactor.ONE, GlStateManager.DestFactor.ZERO);
}
/**
diff --git a/src/main/java/com/cleanroommc/modularui/widget/InternalWidgetTree.java b/src/main/java/com/cleanroommc/modularui/widget/InternalWidgetTree.java
index 01d8c39e3..f099c05be 100644
--- a/src/main/java/com/cleanroommc/modularui/widget/InternalWidgetTree.java
+++ b/src/main/java/com/cleanroommc/modularui/widget/InternalWidgetTree.java
@@ -166,6 +166,11 @@ static void drawBackground(IWidget parent, ModularGuiContext context, boolean ig
}
static void drawTreeForeground(IWidget parent, ModularGuiContext context) {
+ // Mirror drawTree: a disabled widget (and its whole subtree) must not draw its foreground
+ // either. This matches the documented contract of IWidget#drawForeground ("If a parent of
+ // this widget is disabled, this widget will not be drawn.") which the recursion below
+ // otherwise violated, leaving tooltips/foreground of disabled widgets rendering.
+ if (!parent.isEnabled()) return;
IViewport viewport = parent instanceof IViewport viewport1 ? viewport1 : null;
context.pushMatrix();
parent.transform(context);
diff --git a/src/main/java/com/cleanroommc/modularui/widgets/textfield/BaseTextFieldWidget.java b/src/main/java/com/cleanroommc/modularui/widgets/textfield/BaseTextFieldWidget.java
index 6a3e5856b..369d8cd9c 100644
--- a/src/main/java/com/cleanroommc/modularui/widgets/textfield/BaseTextFieldWidget.java
+++ b/src/main/java/com/cleanroommc/modularui/widgets/textfield/BaseTextFieldWidget.java
@@ -137,7 +137,14 @@ protected void drawText(ModularGuiContext context, TextFieldTheme widgetTheme) {
} else {
this.renderer.draw(this.handler.getText());
}
- getScrollArea().getScrollX().setScrollSize(Math.max(0, (int) (this.renderer.getLastActualWidth() + 0.5f)));
+ // + renderer.getX(): scrollSize has to be measured in the same coordinate space as
+ // TextFieldHandler's cursor positions (getPosOf, which include the field's left-padding offset
+ // via getStartX) - otherwise ScrollData.clamp's "scrollSize - visibleSize" cap silently pulls the
+ // scroll back short of the cursor on every single frame, undoing whatever the handler just set.
+ // + getScrollEdgeMargin(): matches the same margin setMainCursor bakes into its own scrollSize,
+ // so this per-frame recompute doesn't clamp that margin back down to zero on the right/end edge.
+ getScrollArea().getScrollX().setScrollSize(this.renderer.getX()
+ + Math.max(0, (int) (this.renderer.getLastActualWidth() + 0.5f)) + this.handler.getScrollEdgeMargin());
}
@Override
@@ -391,6 +398,17 @@ public W setFocusOnGuiOpen(boolean focusOnGuiOpen) {
return getThis();
}
+ /**
+ * Safety margin, in pixels, kept between the cursor/revealed text and the field's real clip edge
+ * when scrolling horizontally (see {@link TextFieldHandler#setScrollEdgeMargin}). Defaults to
+ * this field's own default horizontal padding, since that's already the breathing room the field
+ * visually reserves; override this if a different padding is configured.
+ */
+ public W scrollEdgeMargin(int margin) {
+ this.handler.setScrollEdgeMargin(margin);
+ return getThis();
+ }
+
/**
* Sets a constant hint text. The hint is displayed in a less noticeable color when the field is empty.
* The color is by default obtained from the current them, but can be overriden with {@link #hintColor(int)}.
diff --git a/src/main/java/com/cleanroommc/modularui/widgets/textfield/TextFieldHandler.java b/src/main/java/com/cleanroommc/modularui/widgets/textfield/TextFieldHandler.java
index 3f28b4f28..6c98eef83 100644
--- a/src/main/java/com/cleanroommc/modularui/widgets/textfield/TextFieldHandler.java
+++ b/src/main/java/com/cleanroommc/modularui/widgets/textfield/TextFieldHandler.java
@@ -3,6 +3,7 @@
import com.cleanroommc.modularui.screen.viewport.GuiContext;
import com.cleanroommc.modularui.utils.MathUtils;
import com.cleanroommc.modularui.widget.scroll.ScrollArea;
+import com.cleanroommc.modularui.widget.scroll.ScrollData;
import com.google.common.base.Joiner;
import org.jetbrains.annotations.Nullable;
@@ -33,6 +34,7 @@ public class TextFieldHandler {
private Pattern pattern;
private int maxCharacters = -1;
private GuiContext guiContext;
+ private int scrollEdgeMargin = 4;
public TextFieldHandler(BaseTextFieldWidget> textFieldWidget) {
this.textFieldWidget = textFieldWidget;
@@ -46,6 +48,14 @@ public void setMaxCharacters(int maxCharacters) {
this.maxCharacters = maxCharacters;
}
+ public int getScrollEdgeMargin() {
+ return this.scrollEdgeMargin;
+ }
+
+ public void setScrollEdgeMargin(int scrollEdgeMargin) {
+ this.scrollEdgeMargin = scrollEdgeMargin;
+ }
+
public void setScrollArea(@Nullable ScrollArea scrollArea) {
this.scrollArea = scrollArea;
}
@@ -93,19 +103,27 @@ public void setMainCursor(int linePos, int charPos, boolean animate) {
if (main.x != charPos || main.y != linePos) {
main.setLocation(charPos, linePos);
if (!this.text.isEmpty() && this.renderer != null && this.scrollArea != null) {
- // update actual width
- this.renderer.setSimulate(true);
- this.renderer.draw(this.text);
- this.renderer.setSimulate(false);
- this.scrollArea.getScrollX().setScrollSize((int) this.renderer.getLastActualWidth());
- if (this.scrollArea.getScrollX().isScrollBarActive(this.scrollArea)) {
- String line = this.text.get(main.y);
- int scrollTo = (int) this.renderer.getPosOf(this.renderer.measureLines(Collections.singletonList(line)), new Point(main.x, 0)).x;
- scrollTo -= this.scrollArea.getScrollX().getFullVisibleSize(this.scrollArea) / 2;
- if (animate) {
- this.scrollArea.getScrollX().animateTo(this.scrollArea, scrollTo);
- } else {
- this.scrollArea.getScrollX().scrollTo(this.scrollArea, scrollTo);
+ ScrollData scrollX = this.scrollArea.getScrollX();
+ String line = this.text.get(main.y);
+ var measuredLine = this.renderer.measureLines(Collections.singletonList(line));
+ int lineEndX = (int) this.renderer.getPosOf(measuredLine, new Point(line.length(), 0)).x;
+ scrollX.setScrollSize(lineEndX + this.scrollEdgeMargin);
+ if (scrollX.isScrollBarActive(this.scrollArea)) {
+ int cursorX = (int) this.renderer.getPosOf(measuredLine, new Point(main.x, 0)).x;
+ int visible = scrollX.getFullVisibleSize(this.scrollArea) - this.scrollEdgeMargin;
+ int current = scrollX.getScroll();
+ int scrollTo = current;
+ if (cursorX < current + this.scrollEdgeMargin) {
+ scrollTo = Math.max(0, cursorX - this.scrollEdgeMargin);
+ } else if (cursorX > current + visible) {
+ scrollTo = cursorX - visible;
+ }
+ if (scrollTo != current) {
+ if (animate) {
+ scrollX.animateTo(this.scrollArea, scrollTo);
+ } else {
+ scrollX.scrollTo(this.scrollArea, scrollTo);
+ }
}
}
}
@@ -308,7 +326,9 @@ public void insert(List text, boolean hasHorizontalScrolling) {
if (point == null || copy.size() > this.maxLines || !this.renderer.wouldFit(copy, !hasHorizontalScrolling)) return;
this.text.clear();
this.text.addAll(copy);
- setCursor(point, true);
+ // no animation here: typing fires this on every keystroke, and an animated scroll can't catch up
+ // to a target that keeps moving forward each character - see setMainCursor's comment.
+ setCursor(point, false);
onChanged();
}
diff --git a/src/main/java/com/cleanroommc/modularui/widgets/textfield/TextFieldRenderer.java b/src/main/java/com/cleanroommc/modularui/widgets/textfield/TextFieldRenderer.java
index 8f6ebc96c..7a4186f4e 100644
--- a/src/main/java/com/cleanroommc/modularui/widgets/textfield/TextFieldRenderer.java
+++ b/src/main/java/com/cleanroommc/modularui/widgets/textfield/TextFieldRenderer.java
@@ -235,7 +235,10 @@ public void drawMarked(float y0, float x0, float x1) {
private void drawCursor(float x0, float y0) {
x0 = (x0 - 0.8f) / this.scale;
y0 = (y0 - 1) / this.scale;
- float x1 = x0 + 0.6f;
+ // 1.0 rather than the previous 0.6: a sub-pixel-wide quad can fail to rasterize as a visible
+ // pixel at all once it lands right at a scrolled/clipped edge (see TextFieldHandler's scroll
+ // margin), which is exactly where the cursor sits while typing past a field's visible width.
+ float x1 = x0 + 1.0f;
float y1 = y0 + 9;
float red = Color.getRedF(this.cursorColor);
float green = Color.getGreenF(this.cursorColor);