From b9dee91d9cbe8daab513e647ddf539bc7394f1d5 Mon Sep 17 00:00:00 2001 From: david Date: Mon, 27 Jul 2026 20:24:33 +0200 Subject: [PATCH 1/8] Revert "Remove onboarding screen" This reverts commit 810fd7edffcfc1901cbc7effff0271eebc60b03f. --- .../com/example/mixin/OnboardingScreen.java | 465 ++++++++++++++++++ .../main/resources/example-mod.mixins.json | 11 + .../src/main/resources/fabric.mod.json | 3 + 3 files changed, 479 insertions(+) create mode 100644 fabric/example-mod/src/main/java/com/example/mixin/OnboardingScreen.java create mode 100644 fabric/example-mod/src/main/resources/example-mod.mixins.json diff --git a/fabric/example-mod/src/main/java/com/example/mixin/OnboardingScreen.java b/fabric/example-mod/src/main/java/com/example/mixin/OnboardingScreen.java new file mode 100644 index 00000000..1f9874d1 --- /dev/null +++ b/fabric/example-mod/src/main/java/com/example/mixin/OnboardingScreen.java @@ -0,0 +1,465 @@ +package com.example.mixin; + +import net.minecraft.ChatFormatting; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.components.Checkbox; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.gui.screens.TitleScreen; +import net.minecraft.client.input.MouseButtonEvent; +import net.minecraft.network.chat.ClickEvent; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.FormattedText; +import net.minecraft.network.chat.MutableComponent; +import net.minecraft.network.chat.Style; +import net.minecraft.network.chat.TextColor; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.net.URI; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +@NullMarked +@Mixin(TitleScreen.class) +public abstract class OnboardingScreen { + @Unique + private static boolean dismissed; + + @Inject(method = "init", at = @At(value = "TAIL")) + private void showFastStatsOnboardingScreen(final CallbackInfo cir) { + if (!dismissed) { + Minecraft.getInstance().setScreenAndShow(new Onboarding()); + } + } + + // todo: add a button to open the onboarding screen somewhere in the settings + + private static final class Onboarding extends Screen { + private static final String MODS_URL = "https://faststats.dev/mods"; + private static final String ABUSE_URL = "https://faststats.dev/abuse"; + private static final String INFO_URL = "https://faststats.dev/info"; + private static final Component title = Component.literal("FastStats Metrics"); + private static final Component description = Component.literal("") + .append(link("FastStats", MODS_URL)) + .append(" collects anonymous usage statistics and errors.\n") + .append("Keeping Metrics and Error tracking enabled helps developers to improve their mods.\n\n") + .append("If you suspect a developer is collecting personal data or bypassing any opt-out option,\n") + .append("please report it at: ") + .append(link(ABUSE_URL, ABUSE_URL)); + + private static final Component submitMetrics = Component.literal("Submit Metrics"); + private static final Component submitAdditionalMetrics = Component.literal("Submit Additional Metrics (provided by the developer)"); + private static final Component errorTracking = Component.literal("Submit Errors"); + + private static final String[] alwaysCollectedData = new String[]{ + "Mod version", + "Fabric version", + "..." + }; + + private static final Set expandedDetails = new HashSet<>(); + + private static final Set mods = Set.of( + new Mod("TreeHugger69", new String[]{"Default Metrics", "Error Tracking"}, new String[]{"client_age", "language"}), + new Mod("Sucker123", new String[]{"Default Metrics"}, new String[]{}) + ); + + private record Mod(String name, String[] metrics, String[] additionalMetrics) { + + } + + private boolean collectedDataExpanded; + private boolean installedModsExpanded; + private boolean submitMetricsSelected = true; + private boolean submitAdditionalMetricsSelected = true; + private boolean errorTrackingSelected = true; + private int scrollOffset; + + private Checkbox submitMetricsWidget; + private Checkbox submitAdditionalMetricsWidget; + private Checkbox errorTrackingWidget; + private Button declineButton; + private Button acceptButton; + + private Onboarding() { + super(title); + } + + @Override + protected void init() { + this.scrollOffset = Math.min(this.scrollOffset, this.maxScrollOffset()); + + this.submitMetricsWidget = this.addRenderableWidget(Checkbox.builder(Onboarding.submitMetrics, this.font) + .pos(contentLeft(), checkboxY()) + .onValueChange((checkbox, selected) -> { + this.submitMetricsSelected = selected; + this.rebuildWidgets(); + }) + .selected(this.submitMetricsSelected) + .build()); + this.submitAdditionalMetricsWidget = this.addRenderableWidget(Checkbox.builder(this.additionalMetricsLabel(), this.font) + .pos(contentLeft(), checkboxY() + CHECKBOX_GAP) + .onValueChange((checkbox, selected) -> this.submitAdditionalMetricsSelected = selected) + .selected(this.submitAdditionalMetricsSelected && this.submitMetricsSelected) + .build()); + this.submitAdditionalMetricsWidget.active = this.submitMetricsSelected; + this.submitAdditionalMetricsWidget.setAlpha(this.submitMetricsSelected ? 1.0F : 0.45F); + this.errorTrackingWidget = this.addRenderableWidget(Checkbox.builder(Onboarding.errorTracking, this.font) + .pos(contentLeft(), checkboxY() + CHECKBOX_GAP * 2) + .onValueChange((checkbox, selected) -> this.errorTrackingSelected = selected) + .selected(this.errorTrackingSelected) + .build()); + + final var buttonGap = 2; + final var buttonWidth = (contentWidth() - buttonGap) / 2; + this.declineButton = this.addRenderableWidget(Button.builder(Component.literal("Decline All").withColor(TextColor.RED), button -> { + // todo: disable all… close immediately or let the user also click "confirm"? + this.onClose(); + }).bounds(contentLeft(), buttonY(), buttonWidth, 20).build()); + this.acceptButton = this.addRenderableWidget(Button.builder(Component.literal("Confirm Selection").withColor(TextColor.GREEN), button -> { + // todo: save selection + this.onClose(); + }).bounds(contentLeft() + buttonWidth + buttonGap, buttonY(), buttonWidth, 20).build()); + this.updateWidgetPositions(); + } + + @Override + public void onClose() { + dismissed = true; + Minecraft.getInstance().setScreenAndShow(new TitleScreen()); + } + + // fixme: this is hellish code, i bet i will forget what it does until tomorrow :) + @Override + public void extractRenderState(final GuiGraphicsExtractor graphics, final int mouseX, final int mouseY, final float a) { + super.extractRenderState(graphics, mouseX, mouseY, a); + + final int panelWidth = Math.min(this.width - 32, 1186); + final int panelHeight = this.height - 54; + final int panelX = (this.width - panelWidth) / 2; + final int panelY = 27; + final int border = 0xFFFFFFFF; + final int text = 0xFFE0E0E0; + + graphics.outline(panelX, panelY, panelWidth, panelHeight, border); + graphics.centeredText(this.font, title, this.width / 2, panelY + 45, text); + + final int contentInset = contentInset(panelWidth); + final int descriptionX = panelX + contentInset; + final int descriptionY = descriptionY(); + final int descriptionWidth = panelWidth - contentInset * 2; + final int descriptionHeight = descriptionHeight(); + graphics.enableScissor(panelX + 1, scrollTop(), panelX + panelWidth - 1, scrollBottom()); + graphics.fill(descriptionX + 1, descriptionY + 1, descriptionX + descriptionWidth - 1, descriptionY + descriptionHeight - 1, 0xFF000000); + graphics.outline(descriptionX, descriptionY, descriptionWidth, descriptionHeight, border); + graphics.textWithWordWrap(this.font, description, descriptionX + 24, descriptionY + 16, descriptionWidth - 48, text, false); + + final int infoY = descriptionY + descriptionHeight + 12; + final int infoHeight = infoHeight(); + graphics.fill(descriptionX + 1, infoY + 1, descriptionX + descriptionWidth - 1, infoY + infoHeight - 1, 0xFF000000); + graphics.outline(descriptionX, infoY, descriptionWidth, infoHeight, border); + graphics.textWithWordWrap(this.font, infoDescription(), descriptionX + 24, infoY + 16, descriptionWidth - 48, text, false); + graphics.disableScissor(); + this.renderScrollbar(graphics, panelX, panelY, panelWidth, panelHeight, border); + } + + // fixme: all of this sucks + @Override + public boolean mouseClicked(final MouseButtonEvent event, final boolean doubleClick) { + final Style clickedStyle = this.getStyleAt(description, event.x(), event.y(), descriptionTextX(), descriptionTextY(), descriptionTextWidth()); + if (clickedStyle != null && clickedStyle.getClickEvent() != null) { + defaultHandleClickEvent(clickedStyle.getClickEvent(), this.minecraft, this); + return true; + } + + if (event.y() < scrollTop() || event.y() > scrollBottom()) { + return super.mouseClicked(event, doubleClick); + } + + final Component info = infoDescription(); + final Style clickedInfoStyle = this.getStyleAt(info, event.x(), event.y(), infoTextX(), infoTextY(), descriptionTextWidth()); + if (clickedInfoStyle != null && clickedInfoStyle.getClickEvent() != null) { + defaultHandleClickEvent(clickedInfoStyle.getClickEvent(), this.minecraft, this); + return true; + } + + final String clickedInfoLine = this.getLineAt(info, event.x(), event.y(), infoTextX(), infoTextY(), descriptionTextWidth()); + if (clickedInfoLine != null) { + if (clickedInfoLine.contains("What data is collected?")) { + this.collectedDataExpanded = !this.collectedDataExpanded; + this.updateWidgetPositions(); + return true; + } + if (clickedInfoLine.contains("List of installed mods that use FastStats")) { + this.installedModsExpanded = !this.installedModsExpanded; + this.updateWidgetPositions(); + return true; + } + if (clickedInfoLine.contains("Additional Metrics")) { + expandedDetails.add(""); // todo: idk how to expand that crap + this.updateWidgetPositions(); + return true; + } + } + + return super.mouseClicked(event, doubleClick); + } + + @Override + public boolean mouseScrolled(final double mouseX, final double mouseY, final double horizontalAmount, final double verticalAmount) { + final int maxScrollOffset = this.maxScrollOffset(); + if (maxScrollOffset <= 0) { + return super.mouseScrolled(mouseX, mouseY, horizontalAmount, verticalAmount); + } + + final var b = this.scrollOffset - (int) (verticalAmount * 20); + this.scrollOffset = Math.clamp(b, 0, maxScrollOffset); + this.updateWidgetPositions(); + return true; + } + + // fixme: hacky bs + private @Nullable Style getStyleAt(final Component component, final double mouseX, final double mouseY, final int textX, final int textY, final int textWidth) { + final var line = this.getFormattedLineAt(component, mouseX, mouseY, textX, textY, textWidth); + if (line == null) { + return null; + } + + final var relativeX = mouseX - textX; + final var currentX = new double[]{0}; + final var style = line.visit((segmentStyle, text) -> { + final float segmentWidth = this.font.getSplitter().stringWidth(FormattedText.of(text, segmentStyle)); + if (relativeX >= currentX[0] && relativeX <= currentX[0] + segmentWidth) { + return Optional.of(segmentStyle); + } + + currentX[0] += segmentWidth; + return Optional.empty(); + }, Style.EMPTY); + + return style.orElse(null); + } + + private @Nullable String getLineAt(final Component component, final double mouseX, final double mouseY, final int textX, final int textY, final int textWidth) { + final FormattedText line = this.getFormattedLineAt(component, mouseX, mouseY, textX, textY, textWidth); + return line == null ? null : line.getString(); + } + + private @Nullable FormattedText getFormattedLineAt(final Component component, final double mouseX, final double mouseY, final int textX, final int textY, final int textWidth) { + if (mouseX < textX || mouseX > textX + textWidth || mouseY < textY) { + return null; + } + + final List lines = this.font.getSplitter().splitLines(component, textWidth, Style.EMPTY); + final int line = (int) ((mouseY - textY) / this.font.lineHeight); + if (line < 0 || line >= lines.size()) { + return null; + } + + return lines.get(line); + } + + private int descriptionTextX() { + final int panelWidth = Math.min(this.width - 32, 1186); + final int panelX = (this.width - panelWidth) / 2; + final int contentInset = contentInset(panelWidth); + return panelX + contentInset + 24; + } + + private int descriptionTextY() { + return descriptionY() + 16; + } + + private int infoTextX() { + return descriptionTextX(); + } + + private int infoTextY() { + return infoY() + 16; + } + + private int contentLeft() { + final int panelWidth = Math.min(this.width - 32, 1186); + final int panelX = (this.width - panelWidth) / 2; + final int contentInset = contentInset(panelWidth); + return panelX + contentInset + 2; + } + + private int contentWidth() { + final int panelWidth = Math.min(this.width - 32, 1186); + final int contentInset = contentInset(panelWidth); + return panelWidth - contentInset * 2 - 4; + } + + // fixme: magic numbers here we go!!! all of this is bound to break :) + private static final int CHECKBOX_GAP = 30; + + private int descriptionY() { + return 27 + 122 - this.scrollOffset; + } + + private int baseInfoY() { + return 27 + 122 + descriptionHeight() + 12 - this.scrollOffset; + } + + private int infoY() { + return this.baseInfoY(); + } + + private int scrollTop() { + return 27 + 88; + } + + private int scrollBottom() { + return this.height - 55; + } + + private int checkboxY() { + return this.infoY() + infoHeight() + 22; + } + + private int buttonY() { + return checkboxY() + CHECKBOX_GAP * 2 + 20 + 16; + } + + private int contentBottom() { + return 27 + 122 + descriptionHeight() + 12 + infoHeight() + 22 + CHECKBOX_GAP * 2 + 20 + 16 + 20; + } + + private int maxScrollOffset() { + return Math.max(0, contentBottom() - scrollBottom()); + } + + private void updateWidgetPositions() { + this.scrollOffset = Math.min(this.scrollOffset, this.maxScrollOffset()); + + final int buttonGap = 2; + final int buttonWidth = (contentWidth() - buttonGap) / 2; + this.submitMetricsWidget.setY(checkboxY()); + this.submitAdditionalMetricsWidget.setY(checkboxY() + CHECKBOX_GAP); + this.errorTrackingWidget.setY(checkboxY() + CHECKBOX_GAP * 2); + this.submitMetricsWidget.visible = isInsideScrollArea(this.submitMetricsWidget.getY(), this.submitMetricsWidget.getHeight()); + this.submitAdditionalMetricsWidget.visible = isInsideScrollArea(this.submitAdditionalMetricsWidget.getY(), this.submitAdditionalMetricsWidget.getHeight()); + this.errorTrackingWidget.visible = isInsideScrollArea(this.errorTrackingWidget.getY(), this.errorTrackingWidget.getHeight()); + this.declineButton.setX(contentLeft()); + this.declineButton.setY(buttonY()); + this.declineButton.setWidth(buttonWidth); + this.declineButton.setHeight(20); + this.acceptButton.setX(contentLeft() + buttonWidth + buttonGap); + this.acceptButton.setY(buttonY()); + this.acceptButton.setWidth(buttonWidth); + this.acceptButton.setHeight(20); + this.declineButton.visible = isInsideScrollArea(this.declineButton.getY(), this.declineButton.getHeight()); + this.acceptButton.visible = isInsideScrollArea(this.acceptButton.getY(), this.acceptButton.getHeight()); + } + + private Component additionalMetricsLabel() { + if (this.submitMetricsSelected) { + return Onboarding.submitAdditionalMetrics; + } + + return Onboarding.submitAdditionalMetrics.copy().withStyle(ChatFormatting.GRAY); + } + + private boolean isInsideScrollArea(final int y, final int height) { + return y + height > scrollTop() && y < scrollBottom(); + } + + // fixme: there has to be some inbuilt way to do this, right? RIGHT??? + private void renderScrollbar(final GuiGraphicsExtractor graphics, final int panelX, final int panelY, final int panelWidth, final int panelHeight, final int color) { + final int maxScrollOffset = this.maxScrollOffset(); + if (maxScrollOffset <= 0) { + return; + } + + final int trackX = panelX + panelWidth - 10; + final int trackY = scrollTop(); + final int trackHeight = scrollBottom() - scrollTop(); + final int contentHeight = contentBottom() - scrollTop(); + final int thumbHeight = Math.clamp((long) trackHeight * trackHeight / contentHeight, 18, trackHeight); + final int thumbY = trackY + this.scrollOffset * (trackHeight - thumbHeight) / maxScrollOffset; + graphics.fill(trackX, trackY, trackX + 2, trackY + trackHeight, 0x55FFFFFF); + graphics.fill(trackX - 1, thumbY, trackX + 3, thumbY + thumbHeight, color); + } + + private int descriptionTextWidth() { + final int panelWidth = Math.min(this.width - 32, 1186); + final int contentInset = contentInset(panelWidth); + final int descriptionWidth = panelWidth - contentInset * 2; + return descriptionWidth - 48; + } + + private int descriptionHeight() { + return Math.max(76, this.height / 5); + } + + private int infoHeight() { + return Math.max(60, this.font.wordWrapHeight(infoDescription(), descriptionTextWidth()) + 32); + } + + // todo: maybe an object that keeps track of that? collapsed text and everything… + private Component infoDescription() { + final MutableComponent component = Component.literal("") + .append(collapsible("What data is collected?", this.collectedDataExpanded)); + if (this.collectedDataExpanded) { + for (final var item : alwaysCollectedData) { + component.append("\n· ").append(item); + } + component.append("\nRead more here: ") + .append(link(INFO_URL, INFO_URL)); + } + + component.append("\n\n") + .append(collapsible("List of installed mods that use FastStats", this.installedModsExpanded)); + if (this.installedModsExpanded) mods.forEach(mod -> { + component.append("\n· ").append(mod.name()); + + for (final var metric : mod.metrics()) { + component.append("\n ").append(metric); + } + + final var length = mod.additionalMetrics().length; + if (length == 0) return; + + final boolean expanded = expandedDetails.contains(mod.name() + "-additional"); // todo: still no toggle + final var collapsible = collapsible("Additional Metrics (" + length + ")", expanded); + component.append("\n ").append(collapsible); + if (expanded) for (final var metric : mod.additionalMetrics()) { + component.append("\n · ").append(metric); + } + }); + + return component; + } + + private static int contentInset(final int panelWidth) { + return Math.clamp(panelWidth / 8, 24, 160); + } + + private static Component collapsible(final String text, final boolean expanded) { + return Component.literal((expanded ? "˅ " : "˃ ") + text) + .withStyle(ChatFormatting.YELLOW); + } + + private static Component link(final String text, final String url) { + return Component.literal(text).withStyle(style -> style + .withClickEvent(new ClickEvent.OpenUrl(URI.create(url))) + .withColor(ChatFormatting.AQUA) + .withUnderlined(true)); + } + + @Override + public boolean shouldCloseOnEsc() { + return false; + } + } +} diff --git a/fabric/example-mod/src/main/resources/example-mod.mixins.json b/fabric/example-mod/src/main/resources/example-mod.mixins.json new file mode 100644 index 00000000..d1e78105 --- /dev/null +++ b/fabric/example-mod/src/main/resources/example-mod.mixins.json @@ -0,0 +1,11 @@ +{ + "required": true, + "package": "com.example.mixin", + "compatibilityLevel": "JAVA_25", + "client": [ + "OnboardingScreen" + ], + "injectors": { + "defaultRequire": 1 + } +} diff --git a/fabric/example-mod/src/main/resources/fabric.mod.json b/fabric/example-mod/src/main/resources/fabric.mod.json index fa32a0e8..c71287d3 100644 --- a/fabric/example-mod/src/main/resources/fabric.mod.json +++ b/fabric/example-mod/src/main/resources/fabric.mod.json @@ -12,6 +12,9 @@ "com.example.ExampleMod" ] }, + "mixins": [ + "example-mod.mixins.json" + ], "depends": { "fabricloader": ">=0.18.4", "minecraft": "*", From d6063a124f1dd99f7a6da1b408cbe509e4192046 Mon Sep 17 00:00:00 2001 From: david Date: Mon, 27 Jul 2026 20:55:32 +0200 Subject: [PATCH 2/8] _proper?_ scaling --- .../com/example/mixin/OnboardingScreen.java | 158 ++++++++++++------ 1 file changed, 108 insertions(+), 50 deletions(-) diff --git a/fabric/example-mod/src/main/java/com/example/mixin/OnboardingScreen.java b/fabric/example-mod/src/main/java/com/example/mixin/OnboardingScreen.java index 1f9874d1..7a2a9042 100644 --- a/fabric/example-mod/src/main/java/com/example/mixin/OnboardingScreen.java +++ b/fabric/example-mod/src/main/java/com/example/mixin/OnboardingScreen.java @@ -27,6 +27,7 @@ import java.util.List; import java.util.Optional; import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; @NullMarked @Mixin(TitleScreen.class) @@ -40,7 +41,7 @@ private void showFastStatsOnboardingScreen(final CallbackInfo cir) { Minecraft.getInstance().setScreenAndShow(new Onboarding()); } } - + // todo: add a button to open the onboarding screen somewhere in the settings private static final class Onboarding extends Screen { @@ -60,6 +61,23 @@ private static final class Onboarding extends Screen { private static final Component submitAdditionalMetrics = Component.literal("Submit Additional Metrics (provided by the developer)"); private static final Component errorTracking = Component.literal("Submit Errors"); + private static final int REFERENCE_WIDTH = 1280; + private static final int REFERENCE_HEIGHT = 720; + private static final int MAX_PANEL_WIDTH = 1186; + private static final int PANEL_MARGIN = 27; + private static final int PANEL_SIDE_MARGIN = 16; + private static final int TITLE_OFFSET = 45; + private static final int SCROLL_TOP_OFFSET = 88; + private static final int CONTENT_TOP_OFFSET = 122; + private static final int BOX_PADDING_X = 24; + private static final int BOX_PADDING_Y = 16; + private static final int BOX_GAP = 12; + private static final int CHECKBOX_GAP = 30; + private static final int CHECKBOX_TOP_GAP = 22; + private static final int BUTTON_TOP_GAP = 16; + private static final int BUTTON_GAP = 2; + private static final int BUTTON_HEIGHT = 20; + private static final String[] alwaysCollectedData = new String[]{ "Mod version", "Fabric version", @@ -80,8 +98,6 @@ private record Mod(String name, String[] metrics, String[] additionalMetrics) { private boolean collectedDataExpanded; private boolean installedModsExpanded; private boolean submitMetricsSelected = true; - private boolean submitAdditionalMetricsSelected = true; - private boolean errorTrackingSelected = true; private int scrollOffset; private Checkbox submitMetricsWidget; @@ -96,6 +112,9 @@ private Onboarding() { @Override protected void init() { + var submitAdditionalMetricsSelected = new AtomicBoolean(true); + var errorTrackingSelected = new AtomicBoolean(true); + this.scrollOffset = Math.min(this.scrollOffset, this.maxScrollOffset()); this.submitMetricsWidget = this.addRenderableWidget(Checkbox.builder(Onboarding.submitMetrics, this.font) @@ -106,29 +125,30 @@ protected void init() { }) .selected(this.submitMetricsSelected) .build()); + this.submitAdditionalMetricsWidget = this.addRenderableWidget(Checkbox.builder(this.additionalMetricsLabel(), this.font) - .pos(contentLeft(), checkboxY() + CHECKBOX_GAP) - .onValueChange((checkbox, selected) -> this.submitAdditionalMetricsSelected = selected) - .selected(this.submitAdditionalMetricsSelected && this.submitMetricsSelected) + .pos(contentLeft(), checkboxY() + scaled(CHECKBOX_GAP)) + .onValueChange((checkbox, selected) -> submitAdditionalMetricsSelected.set(selected)) + .selected(submitAdditionalMetricsSelected.get() && this.submitMetricsSelected) .build()); this.submitAdditionalMetricsWidget.active = this.submitMetricsSelected; this.submitAdditionalMetricsWidget.setAlpha(this.submitMetricsSelected ? 1.0F : 0.45F); this.errorTrackingWidget = this.addRenderableWidget(Checkbox.builder(Onboarding.errorTracking, this.font) - .pos(contentLeft(), checkboxY() + CHECKBOX_GAP * 2) - .onValueChange((checkbox, selected) -> this.errorTrackingSelected = selected) - .selected(this.errorTrackingSelected) + .pos(contentLeft(), checkboxY() + scaled(CHECKBOX_GAP) * 2) + .onValueChange((checkbox, selected) -> errorTrackingSelected.set(selected)) + .selected(errorTrackingSelected.get()) .build()); - final var buttonGap = 2; + final var buttonGap = scaled(BUTTON_GAP); final var buttonWidth = (contentWidth() - buttonGap) / 2; this.declineButton = this.addRenderableWidget(Button.builder(Component.literal("Decline All").withColor(TextColor.RED), button -> { // todo: disable all… close immediately or let the user also click "confirm"? this.onClose(); - }).bounds(contentLeft(), buttonY(), buttonWidth, 20).build()); + }).bounds(contentLeft(), buttonY(), buttonWidth, buttonHeight()).build()); this.acceptButton = this.addRenderableWidget(Button.builder(Component.literal("Confirm Selection").withColor(TextColor.GREEN), button -> { // todo: save selection this.onClose(); - }).bounds(contentLeft() + buttonWidth + buttonGap, buttonY(), buttonWidth, 20).build()); + }).bounds(contentLeft() + buttonWidth + buttonGap, buttonY(), buttonWidth, buttonHeight()).build()); this.updateWidgetPositions(); } @@ -143,15 +163,15 @@ public void onClose() { public void extractRenderState(final GuiGraphicsExtractor graphics, final int mouseX, final int mouseY, final float a) { super.extractRenderState(graphics, mouseX, mouseY, a); - final int panelWidth = Math.min(this.width - 32, 1186); - final int panelHeight = this.height - 54; - final int panelX = (this.width - panelWidth) / 2; - final int panelY = 27; + final int panelWidth = panelWidth(); + final int panelHeight = panelHeight(); + final int panelX = panelX(); + final int panelY = panelY(); final int border = 0xFFFFFFFF; final int text = 0xFFE0E0E0; graphics.outline(panelX, panelY, panelWidth, panelHeight, border); - graphics.centeredText(this.font, title, this.width / 2, panelY + 45, text); + graphics.centeredText(this.font, title, this.width / 2, panelY + scaled(TITLE_OFFSET), text); final int contentInset = contentInset(panelWidth); final int descriptionX = panelX + contentInset; @@ -161,13 +181,13 @@ public void extractRenderState(final GuiGraphicsExtractor graphics, final int mo graphics.enableScissor(panelX + 1, scrollTop(), panelX + panelWidth - 1, scrollBottom()); graphics.fill(descriptionX + 1, descriptionY + 1, descriptionX + descriptionWidth - 1, descriptionY + descriptionHeight - 1, 0xFF000000); graphics.outline(descriptionX, descriptionY, descriptionWidth, descriptionHeight, border); - graphics.textWithWordWrap(this.font, description, descriptionX + 24, descriptionY + 16, descriptionWidth - 48, text, false); + graphics.textWithWordWrap(this.font, description, descriptionX + scaled(BOX_PADDING_X), descriptionY + scaled(BOX_PADDING_Y), descriptionTextWidth(), text, false); - final int infoY = descriptionY + descriptionHeight + 12; + final int infoY = descriptionY + descriptionHeight + scaled(BOX_GAP); final int infoHeight = infoHeight(); graphics.fill(descriptionX + 1, infoY + 1, descriptionX + descriptionWidth - 1, infoY + infoHeight - 1, 0xFF000000); graphics.outline(descriptionX, infoY, descriptionWidth, infoHeight, border); - graphics.textWithWordWrap(this.font, infoDescription(), descriptionX + 24, infoY + 16, descriptionWidth - 48, text, false); + graphics.textWithWordWrap(this.font, infoDescription(), descriptionX + scaled(BOX_PADDING_X), infoY + scaled(BOX_PADDING_Y), descriptionTextWidth(), text, false); graphics.disableScissor(); this.renderScrollbar(graphics, panelX, panelY, panelWidth, panelHeight, border); } @@ -269,14 +289,14 @@ public boolean mouseScrolled(final double mouseX, final double mouseY, final dou } private int descriptionTextX() { - final int panelWidth = Math.min(this.width - 32, 1186); - final int panelX = (this.width - panelWidth) / 2; + final int panelWidth = panelWidth(); + final int panelX = panelX(); final int contentInset = contentInset(panelWidth); - return panelX + contentInset + 24; + return panelX + contentInset + scaled(BOX_PADDING_X); } private int descriptionTextY() { - return descriptionY() + 16; + return descriptionY() + scaled(BOX_PADDING_Y); } private int infoTextX() { @@ -284,31 +304,28 @@ private int infoTextX() { } private int infoTextY() { - return infoY() + 16; + return infoY() + scaled(BOX_PADDING_Y); } private int contentLeft() { - final int panelWidth = Math.min(this.width - 32, 1186); - final int panelX = (this.width - panelWidth) / 2; + final int panelWidth = panelWidth(); + final int panelX = panelX(); final int contentInset = contentInset(panelWidth); return panelX + contentInset + 2; } private int contentWidth() { - final int panelWidth = Math.min(this.width - 32, 1186); + final int panelWidth = panelWidth(); final int contentInset = contentInset(panelWidth); return panelWidth - contentInset * 2 - 4; } - // fixme: magic numbers here we go!!! all of this is bound to break :) - private static final int CHECKBOX_GAP = 30; - private int descriptionY() { - return 27 + 122 - this.scrollOffset; + return contentTop() - this.scrollOffset; } private int baseInfoY() { - return 27 + 122 + descriptionHeight() + 12 - this.scrollOffset; + return contentTop() + descriptionHeight() + scaled(BOX_GAP) - this.scrollOffset; } private int infoY() { @@ -316,23 +333,24 @@ private int infoY() { } private int scrollTop() { - return 27 + 88; + return panelY() + scaled(SCROLL_TOP_OFFSET); } private int scrollBottom() { - return this.height - 55; + return panelY() + panelHeight() - 1; } private int checkboxY() { - return this.infoY() + infoHeight() + 22; + return this.infoY() + infoHeight() + scaled(CHECKBOX_TOP_GAP); } - + private int buttonY() { - return checkboxY() + CHECKBOX_GAP * 2 + 20 + 16; + return checkboxY() + scaled(CHECKBOX_GAP) * 2 + buttonHeight() + scaled(BUTTON_TOP_GAP); } private int contentBottom() { - return 27 + 122 + descriptionHeight() + 12 + infoHeight() + 22 + CHECKBOX_GAP * 2 + 20 + 16 + 20; + return contentTop() + descriptionHeight() + scaled(BOX_GAP) + infoHeight() + scaled(CHECKBOX_TOP_GAP) + + scaled(CHECKBOX_GAP) * 2 + buttonHeight() + scaled(BUTTON_TOP_GAP) + buttonHeight(); } private int maxScrollOffset() { @@ -342,22 +360,22 @@ private int maxScrollOffset() { private void updateWidgetPositions() { this.scrollOffset = Math.min(this.scrollOffset, this.maxScrollOffset()); - final int buttonGap = 2; + final int buttonGap = scaled(BUTTON_GAP); final int buttonWidth = (contentWidth() - buttonGap) / 2; this.submitMetricsWidget.setY(checkboxY()); - this.submitAdditionalMetricsWidget.setY(checkboxY() + CHECKBOX_GAP); - this.errorTrackingWidget.setY(checkboxY() + CHECKBOX_GAP * 2); + this.submitAdditionalMetricsWidget.setY(checkboxY() + scaled(CHECKBOX_GAP)); + this.errorTrackingWidget.setY(checkboxY() + scaled(CHECKBOX_GAP) * 2); this.submitMetricsWidget.visible = isInsideScrollArea(this.submitMetricsWidget.getY(), this.submitMetricsWidget.getHeight()); this.submitAdditionalMetricsWidget.visible = isInsideScrollArea(this.submitAdditionalMetricsWidget.getY(), this.submitAdditionalMetricsWidget.getHeight()); this.errorTrackingWidget.visible = isInsideScrollArea(this.errorTrackingWidget.getY(), this.errorTrackingWidget.getHeight()); this.declineButton.setX(contentLeft()); this.declineButton.setY(buttonY()); this.declineButton.setWidth(buttonWidth); - this.declineButton.setHeight(20); + this.declineButton.setHeight(buttonHeight()); this.acceptButton.setX(contentLeft() + buttonWidth + buttonGap); this.acceptButton.setY(buttonY()); this.acceptButton.setWidth(buttonWidth); - this.acceptButton.setHeight(20); + this.acceptButton.setHeight(buttonHeight()); this.declineButton.visible = isInsideScrollArea(this.declineButton.getY(), this.declineButton.getHeight()); this.acceptButton.visible = isInsideScrollArea(this.acceptButton.getY(), this.acceptButton.getHeight()); } @@ -392,18 +410,18 @@ private void renderScrollbar(final GuiGraphicsExtractor graphics, final int pane } private int descriptionTextWidth() { - final int panelWidth = Math.min(this.width - 32, 1186); + final int panelWidth = panelWidth(); final int contentInset = contentInset(panelWidth); final int descriptionWidth = panelWidth - contentInset * 2; - return descriptionWidth - 48; + return Math.max(1, descriptionWidth - scaled(BOX_PADDING_X) * 2); } private int descriptionHeight() { - return Math.max(76, this.height / 5); + return Math.max(scaled(76), this.height / 5); } private int infoHeight() { - return Math.max(60, this.font.wordWrapHeight(infoDescription(), descriptionTextWidth()) + 32); + return Math.max(scaled(60), this.font.wordWrapHeight(infoDescription(), descriptionTextWidth()) + scaled(BOX_PADDING_Y) * 2); } // todo: maybe an object that keeps track of that? collapsed text and everything… @@ -441,8 +459,48 @@ private Component infoDescription() { return component; } - private static int contentInset(final int panelWidth) { - return Math.clamp(panelWidth / 8, 24, 160); + private int panelX() { + return (this.width - panelWidth()) / 2; + } + + private int panelY() { + return scaled(PANEL_MARGIN); + } + + private int panelWidth() { + final int margin = scaled(PANEL_SIDE_MARGIN); + final int availableWidth = Math.max(1, this.width - margin * 2); + return Math.min(availableWidth, scaled(MAX_PANEL_WIDTH)); + } + + private int panelHeight() { + return Math.max(1, this.height - panelY() * 2); + } + + private int contentTop() { + return panelY() + scaled(CONTENT_TOP_OFFSET); + } + + private int buttonHeight() { + return Math.max(BUTTON_HEIGHT, scaled(BUTTON_HEIGHT)); + } + + private int scaled(final int value) { + return Math.max(1, Math.round(value * this.layoutScale())); + } + + private float layoutScale() { + if (this.width <= 0 || this.height <= 0) { + return 1.0F; + } + + final float widthScale = this.width / (float) REFERENCE_WIDTH; + final float heightScale = this.height / (float) REFERENCE_HEIGHT; + return Math.clamp(Math.min(widthScale, heightScale), 0.65F, 1.25F); + } + + private int contentInset(final int panelWidth) { + return Math.clamp(panelWidth / 8, scaled(24), scaled(160)); } private static Component collapsible(final String text, final boolean expanded) { From 163e27b96a930a1c9d12a53dc8ad91ad512cfc0e Mon Sep 17 00:00:00 2001 From: david Date: Mon, 27 Jul 2026 21:39:18 +0200 Subject: [PATCH 3/8] use pointing hand when hovering clickable text --- .../com/example/mixin/OnboardingScreen.java | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/fabric/example-mod/src/main/java/com/example/mixin/OnboardingScreen.java b/fabric/example-mod/src/main/java/com/example/mixin/OnboardingScreen.java index 7a2a9042..700ad9c2 100644 --- a/fabric/example-mod/src/main/java/com/example/mixin/OnboardingScreen.java +++ b/fabric/example-mod/src/main/java/com/example/mixin/OnboardingScreen.java @@ -1,5 +1,6 @@ package com.example.mixin; +import com.mojang.blaze3d.platform.cursor.CursorTypes; import net.minecraft.ChatFormatting; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiGraphicsExtractor; @@ -190,6 +191,9 @@ public void extractRenderState(final GuiGraphicsExtractor graphics, final int mo graphics.textWithWordWrap(this.font, infoDescription(), descriptionX + scaled(BOX_PADDING_X), infoY + scaled(BOX_PADDING_Y), descriptionTextWidth(), text, false); graphics.disableScissor(); this.renderScrollbar(graphics, panelX, panelY, panelWidth, panelHeight, border); + if (this.isHoveringClickableText(mouseX, mouseY)) { + graphics.requestCursor(CursorTypes.POINTING_HAND); + } } // fixme: all of this sucks @@ -247,6 +251,29 @@ public boolean mouseScrolled(final double mouseX, final double mouseY, final dou return true; } + private boolean isHoveringClickableText(final double mouseX, final double mouseY) { + if (mouseY < scrollTop() || mouseY > scrollBottom()) { + return false; + } + + final Style descriptionStyle = this.getStyleAt(description, mouseX, mouseY, descriptionTextX(), descriptionTextY(), descriptionTextWidth()); + if (descriptionStyle != null && descriptionStyle.getClickEvent() != null) { + return true; + } + + final Component info = infoDescription(); + final Style infoStyle = this.getStyleAt(info, mouseX, mouseY, infoTextX(), infoTextY(), descriptionTextWidth()); + if (infoStyle != null && infoStyle.getClickEvent() != null) { + return true; + } + + // fixme: do not hardcode any of this, we need a proper solution here + final String infoLine = this.getLineAt(info, mouseX, mouseY, infoTextX(), infoTextY(), descriptionTextWidth()); + return infoLine != null && (infoLine.contains("What data is collected?") + || infoLine.contains("List of installed mods that use FastStats") + || infoLine.contains("Additional Metrics")); + } + // fixme: hacky bs private @Nullable Style getStyleAt(final Component component, final double mouseX, final double mouseY, final int textX, final int textY, final int textWidth) { final var line = this.getFormattedLineAt(component, mouseX, mouseY, textX, textY, textWidth); From 012c7817840a7471113752fb56762439732ba4e3 Mon Sep 17 00:00:00 2001 From: david Date: Sun, 2 Aug 2026 08:06:43 +0200 Subject: [PATCH 4/8] Rework screen definitions --- fabric/build.gradle.kts | 2 + fabric/example-mod/build.gradle.kts | 4 +- .../com/example/mixin/OnboardingScreen.java | 4 +- .../fabric/screen/FabricScreenManager.java | 67 +++++++++++++++++ .../faststats/fabric/screen/FabricText.java | 71 +++++++++++++++++++ fabric/src/main/java/module-info.java | 7 ++ neoforge/example-mod/build.gradle.kts | 2 + onboarding/build.gradle.kts | 11 +++ .../java/dev/faststats/screen/Button.java | 15 ++++ .../java/dev/faststats/screen/Checkbox.java | 29 ++++++++ .../java/dev/faststats/screen/Division.java | 19 +++++ .../java/dev/faststats/screen/Element.java | 15 ++++ .../java/dev/faststats/screen/Screen.java | 19 +++++ .../dev/faststats/screen/ScreenManager.java | 20 ++++++ .../dev/faststats/screen/SimpleButton.java | 30 ++++++++ .../dev/faststats/screen/SimpleCheckbox.java | 61 ++++++++++++++++ .../dev/faststats/screen/SimpleDivision.java | 35 +++++++++ .../dev/faststats/screen/SimpleElement.java | 32 +++++++++ .../dev/faststats/screen/SimpleScreen.java | 44 ++++++++++++ .../dev/faststats/screen/SimpleTextBox.java | 14 ++++ .../main/java/dev/faststats/screen/Text.java | 66 +++++++++++++++++ .../java/dev/faststats/screen/TextBox.java | 9 +++ .../onboarding/OnboardingDefinition.java | 63 ++++++++++++++++ onboarding/src/main/java/module-info.java | 14 ++++ settings.gradle.kts | 1 + 25 files changed, 652 insertions(+), 2 deletions(-) create mode 100644 fabric/src/main/java/dev/faststats/fabric/screen/FabricScreenManager.java create mode 100644 fabric/src/main/java/dev/faststats/fabric/screen/FabricText.java create mode 100644 onboarding/build.gradle.kts create mode 100644 onboarding/src/main/java/dev/faststats/screen/Button.java create mode 100644 onboarding/src/main/java/dev/faststats/screen/Checkbox.java create mode 100644 onboarding/src/main/java/dev/faststats/screen/Division.java create mode 100644 onboarding/src/main/java/dev/faststats/screen/Element.java create mode 100644 onboarding/src/main/java/dev/faststats/screen/Screen.java create mode 100644 onboarding/src/main/java/dev/faststats/screen/ScreenManager.java create mode 100644 onboarding/src/main/java/dev/faststats/screen/SimpleButton.java create mode 100644 onboarding/src/main/java/dev/faststats/screen/SimpleCheckbox.java create mode 100644 onboarding/src/main/java/dev/faststats/screen/SimpleDivision.java create mode 100644 onboarding/src/main/java/dev/faststats/screen/SimpleElement.java create mode 100644 onboarding/src/main/java/dev/faststats/screen/SimpleScreen.java create mode 100644 onboarding/src/main/java/dev/faststats/screen/SimpleTextBox.java create mode 100644 onboarding/src/main/java/dev/faststats/screen/Text.java create mode 100644 onboarding/src/main/java/dev/faststats/screen/TextBox.java create mode 100644 onboarding/src/main/java/dev/faststats/screen/onboarding/OnboardingDefinition.java create mode 100644 onboarding/src/main/java/module-info.java diff --git a/fabric/build.gradle.kts b/fabric/build.gradle.kts index b0badbab..47845d78 100644 --- a/fabric/build.gradle.kts +++ b/fabric/build.gradle.kts @@ -48,6 +48,8 @@ subprojects { dependencies { compileOnlyApi(project(":core")) compileOnly(project(":config")) + compileOnly(project(":onboarding")) + include(project(":onboarding")) minecraft("com.mojang:minecraft:26.1.2") compileOnly("net.fabricmc.fabric-api:fabric-api:0.150.0+26.1.2") compileOnly("net.fabricmc:fabric-loader:0.19.3") diff --git a/fabric/example-mod/build.gradle.kts b/fabric/example-mod/build.gradle.kts index a998da26..4da8a1dc 100644 --- a/fabric/example-mod/build.gradle.kts +++ b/fabric/example-mod/build.gradle.kts @@ -1,6 +1,6 @@ plugins { id("net.fabricmc.fabric-loom") version "1.15-SNAPSHOT" - kotlin("jvm") + //kotlin("jvm") } java.toolchain.languageVersion = JavaLanguageVersion.of(25) @@ -10,8 +10,10 @@ tasks.compileJava { } dependencies { + compileOnly(project(":onboarding")) compileOnly("net.fabricmc:fabric-loader:0.19.3") compileOnly(project(":fabric:versions:26.1-26.3")) + include(project(":onboarding")) include(project(":fabric:versions:26.1-26.3")) minecraft("com.mojang:minecraft:26.2") } diff --git a/fabric/example-mod/src/main/java/com/example/mixin/OnboardingScreen.java b/fabric/example-mod/src/main/java/com/example/mixin/OnboardingScreen.java index 700ad9c2..04720a51 100644 --- a/fabric/example-mod/src/main/java/com/example/mixin/OnboardingScreen.java +++ b/fabric/example-mod/src/main/java/com/example/mixin/OnboardingScreen.java @@ -1,6 +1,7 @@ package com.example.mixin; import com.mojang.blaze3d.platform.cursor.CursorTypes; +import dev.faststats.screen.onboarding.OnboardingDefinition; import net.minecraft.ChatFormatting; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiGraphicsExtractor; @@ -39,7 +40,8 @@ public abstract class OnboardingScreen { @Inject(method = "init", at = @At(value = "TAIL")) private void showFastStatsOnboardingScreen(final CallbackInfo cir) { if (!dismissed) { - Minecraft.getInstance().setScreenAndShow(new Onboarding()); + OnboardingDefinition.create().open(); + // Minecraft.getInstance().setScreenAndShow(new Onboarding()); } } diff --git a/fabric/src/main/java/dev/faststats/fabric/screen/FabricScreenManager.java b/fabric/src/main/java/dev/faststats/fabric/screen/FabricScreenManager.java new file mode 100644 index 00000000..56e316c6 --- /dev/null +++ b/fabric/src/main/java/dev/faststats/fabric/screen/FabricScreenManager.java @@ -0,0 +1,67 @@ +package dev.faststats.fabric.screen; + +import dev.faststats.screen.Button; +import dev.faststats.screen.Checkbox; +import dev.faststats.screen.Screen; +import dev.faststats.screen.ScreenManager; +import dev.faststats.screen.Text; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.screens.TitleScreen; +import net.minecraft.network.chat.Component; + +import java.util.function.Consumer; + +public final class FabricScreenManager implements ScreenManager { + @Override + public Text newText() { + return new FabricText(); + } + + @Override + public void closeScreen(Screen screen) { + Minecraft.getInstance().setScreenAndShow(new TitleScreen()); + } + + @Override + public void openScreen(Screen screen) { + Minecraft.getInstance().setScreenAndShow(wrap(screen)); + } + + private Component wrap(Text text) { + return ((FabricText) text).text; + } + + private net.minecraft.client.gui.screens.Screen wrap(Screen screen) { + return new net.minecraft.client.gui.screens.Screen(wrap(screen.title())) { + @Override + protected void init() { + super.init(); + } + + }; + } + + private net.minecraft.client.gui.components.Checkbox wrap(Checkbox checkbox, net.minecraft.client.gui.screens.Screen nmc, Screen screen) { + var build = net.minecraft.client.gui.components.Checkbox.builder(wrap(checkbox.label()), nmc.getFont()) + .onValueChange((self, value) -> { + checkbox.selected(value); + checkbox.onStateChange(screen); + }) + .selected(checkbox.selected()) + .build(); + var action = (Consumer) self -> { + // todo: update built checkbox selection state + build.active = self.enabled(); + build.setAlpha(self.enabled() ? 1.0F : 0.45F); + }; + checkbox.onStateChange((ignored, self) -> action.accept(self)); + action.accept(checkbox); + return build; + } + + private net.minecraft.client.gui.components.Button wrap(Button button, Screen screen) { + return net.minecraft.client.gui.components.Button.builder(wrap(button.label()), button1 -> { + button.onClick(screen); + }).bounds(0, 0, button.width(), button.height()).build(); // todo: calculate x and y based on the visible content + } +} diff --git a/fabric/src/main/java/dev/faststats/fabric/screen/FabricText.java b/fabric/src/main/java/dev/faststats/fabric/screen/FabricText.java new file mode 100644 index 00000000..4ed15c88 --- /dev/null +++ b/fabric/src/main/java/dev/faststats/fabric/screen/FabricText.java @@ -0,0 +1,71 @@ +package dev.faststats.fabric.screen; + +import dev.faststats.screen.Text; +import net.minecraft.ChatFormatting; +import net.minecraft.network.chat.ClickEvent; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.MutableComponent; +import net.minecraft.network.chat.Style; + +import java.net.URI; + +public final class FabricText implements Text { + public final MutableComponent text = Component.empty(); + + @Override + public Text append(Text text) { + this.text.append(((FabricText) text).text); + return this; + } + + @Override + public Text append(String string) { + this.text.append(string); + return this; + } + + @Override + public Text append(String string, URI url) { + this.text.append(Component.literal(string).withStyle(Style.EMPTY.withClickEvent(new ClickEvent.OpenUrl(url)))); + return this; + } + + @Override + public Text color(int color) { + this.text.withStyle(Style.EMPTY.withColor(color)); + return this; + } + + @Override + public Text format(Formatting formatting) { + this.text.withStyle(toFormat(formatting)); + return this; + } + + private ChatFormatting toFormat(Formatting formatting) { + return switch (formatting) { + case BLACK -> ChatFormatting.BLACK; + case DARK_BLUE -> ChatFormatting.DARK_BLUE; + case DARK_GREEN -> ChatFormatting.DARK_GREEN; + case DARK_AQUA -> ChatFormatting.DARK_AQUA; + case DARK_RED -> ChatFormatting.DARK_RED; + case DARK_PURPLE -> ChatFormatting.DARK_PURPLE; + case GOLD -> ChatFormatting.GOLD; + case GRAY -> ChatFormatting.GRAY; + case DARK_GRAY -> ChatFormatting.DARK_GRAY; + case BLUE -> ChatFormatting.BLUE; + case GREEN -> ChatFormatting.GREEN; + case AQUA -> ChatFormatting.AQUA; + case RED -> ChatFormatting.RED; + case LIGHT_PURPLE -> ChatFormatting.LIGHT_PURPLE; + case YELLOW -> ChatFormatting.YELLOW; + case WHITE -> ChatFormatting.WHITE; + case OBFUSCATED -> ChatFormatting.OBFUSCATED; + case BOLD -> ChatFormatting.BOLD; + case STRIKETHROUGH -> ChatFormatting.STRIKETHROUGH; + case UNDERLINE -> ChatFormatting.UNDERLINE; + case ITALIC -> ChatFormatting.ITALIC; + case RESET -> ChatFormatting.RESET; + }; + } +} diff --git a/fabric/src/main/java/module-info.java b/fabric/src/main/java/module-info.java index 7a85bdcf..124e6106 100644 --- a/fabric/src/main/java/module-info.java +++ b/fabric/src/main/java/module-info.java @@ -1,13 +1,18 @@ +import dev.faststats.fabric.screen.FabricScreenManager; +import dev.faststats.screen.ScreenManager; import org.jspecify.annotations.NullMarked; @NullMarked module dev.faststats.fabric { exports dev.faststats.fabric.compat; + exports dev.faststats.fabric.screen; exports dev.faststats.fabric; requires com.google.gson; requires dev.faststats.config; + requires dev.faststats.onboarding; requires dev.faststats; + requires java.desktop; requires java.logging; requires net.fabricmc.loader; requires org.slf4j; @@ -15,5 +20,7 @@ requires static org.jetbrains.annotations; requires static org.jspecify; + provides ScreenManager with FabricScreenManager; + uses dev.faststats.fabric.compat.CompatibilityLayer; } diff --git a/neoforge/example-mod/build.gradle.kts b/neoforge/example-mod/build.gradle.kts index 5b92927c..abd68a77 100644 --- a/neoforge/example-mod/build.gradle.kts +++ b/neoforge/example-mod/build.gradle.kts @@ -12,6 +12,8 @@ configurations.configureEach { } dependencies { + compileOnly(project(":onboarding")) compileOnly(project(":neoforge:versions:26.1-26.2")) + jarJar(project(":onboarding")) jarJar(project(":neoforge:versions:26.1-26.2")) } diff --git a/onboarding/build.gradle.kts b/onboarding/build.gradle.kts new file mode 100644 index 00000000..70a6928b --- /dev/null +++ b/onboarding/build.gradle.kts @@ -0,0 +1,11 @@ +extra.set("moduleName", "dev.faststats.onboarding") + +java.toolchain.languageVersion = JavaLanguageVersion.of(25) + +tasks.compileJava { + options.release.set(17) +} + +dependencies { + compileOnlyApi("org.jspecify:jspecify:1.0.0") +} diff --git a/onboarding/src/main/java/dev/faststats/screen/Button.java b/onboarding/src/main/java/dev/faststats/screen/Button.java new file mode 100644 index 00000000..6b9f73a2 --- /dev/null +++ b/onboarding/src/main/java/dev/faststats/screen/Button.java @@ -0,0 +1,15 @@ +package dev.faststats.screen; + +import java.util.function.Consumer; + +public sealed interface Button extends Element