\u200B";
final var dependentsLabel = new JLabel(dependents);
final var defaultDepFont = dependentsLabel.getFont();
dependentsLabel.setFont(defaultDepFont.deriveFont(defaultDepFont.getSize() * 1.1f));
dependentsLabel.setHorizontalAlignment(SwingConstants.CENTER);
window.getContentPane().add(dependentsLabel, BorderLayout.CENTER);
// -------
// Buttons
// -------
var buttonsPanel = new JPanel();
// Download
final var downloadButton = new JButton("Download and install");
final var progressBar = new JProgressBar();
progressBar.setIndeterminate(true);
downloadButton.addActionListener(e -> {
downloadButton.setEnabled(false);
downloadButton.add(progressBar);
downloadButton.updateUI();
titleLabel.setText("Installing oωo-lib");
window.getContentPane().remove(dependentsLabel);
final var logBox = new JTextArea();
logBox.setEditable(false);
logBox.setMargin(new Insets(15, 15, 15, 15));
final var scrollPane = new JScrollPane(logBox);
scrollPane.setBorder(new EmptyBorder(0, 15, 0, 15));
window.getContentPane().add(scrollPane, BorderLayout.CENTER);
var task = new DownloadTask(s -> {
OwoSentinel.LOGGER.info(s);
logBox.setText(logBox.getText() + (logBox.getText().isBlank() ? "" : "\n") + s);
scrollPane.getVerticalScrollBar().setValue(scrollPane.getVerticalScrollBar().getMaximum());
}, () -> {
progressBar.setVisible(false);
titleLabel.setText("");
downloadButton.setText("Installed");
});
task.execute();
});
// What is this
final var whatIsThisButton = new JButton("What is this?");
whatIsThisButton.addActionListener(e -> {
String[] options = {"Open GitHub", "OK"};
int selection = JOptionPane.showOptionDialog(window, OwoSentinel.OWO_EXPLANATION, "oωo-sentinel",
JOptionPane.YES_NO_OPTION, JOptionPane.INFORMATION_MESSAGE, new ImageIcon(owoIconImage),
options, options[0]);
if (selection == 0 && Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) {
try {
Desktop.getDesktop().browse(URI.create("https://github.com/wisp-forest/owo-lib"));
} catch (IOException ignored) {}
}
});
// Exit
final var exitButton = new JButton("Close");
exitButton.addActionListener(e -> window.dispose());
// Panel setup
buttonsPanel.add(downloadButton);
buttonsPanel.add(whatIsThisButton);
buttonsPanel.add(exitButton);
// ---------------
// Window creation
// ---------------
window.getContentPane().add(buttonsPanel, BorderLayout.SOUTH);
window.pack();
window.setVisible(true);
window.requestFocus();
synchronized (SentinelWindow.class) {
SentinelWindow.class.wait();
}
}
}
================================================
FILE: owo-sentinel/src/main/resources/fabric.mod.json
================================================
{
"schemaVersion": 1,
"id": "owo-sentinel",
"version": "${version}",
"name": "oωo-sentinel",
"description": "makes u download oωo",
"authors": [
"glisco"
],
"contact": {},
"license": "MIT",
"icon": "owo_sentinel_icon.png",
"environment": "*",
"provides": [
"owo",
"owo-lib"
],
"languageAdapters": {
"maldenhagen": "io.wispforest.owosentinel.Maldenhagen"
},
"depends": {
"fabricloader": "*",
"minecraft": ">=1.18"
},
"custom": {
"modmenu": {
"links": {
"modmenu.discord": "https://discord.gg/xrwHKktV2d"
},
"badges": [
"library"
]
}
}
}
================================================
FILE: owo-ui.xsd
================================================
Insets describing an offset on each side of a rectangle.
Elements which occur after one another override each other, meaning
that a `bottom` element after an `all` element will only redefine
the bottom offset and leave the rest intact
Any of the three positioning types supported by owo-ui,
with the content formatted as `{horizontal},{vertical}`, eg `25,50`
A container for the horizontal and vertical sizing
declaration, each of which may occur once
Some literal or translated text, depending on whether
the `translate` attribute is `true`
A standard integer color in either `#AARRGGBB` or `#RRGGBB` format.
Alternatively, the all-lowercase name of any of Minecraft's 16 text colors
One or multiple surfaces chained together. If multiple surfaces
appear in this declaration, they are chained together in order of
appearance via the `and(...)` method
A standard Minecraft panel, optionally with a dark texture
An inset into a panel, used to create an area
enclosed by a standard light panel
A panel inset bordered by a standard light panel
of the specified width on each border
A simple surface repeating the given texture, just like the
options background does with the dirt texture
A simple, colorless surface that blurs everything
underneath itself
The standard Minecraft options background,
usually a repeating dirt texture
The standard dark translucent background
most Vanilla UIs use
The same renderer used by vanilla item
and UI element tooltips
A simple rectangular outline of the specified color
A flat rectangle of the specified color
A standard Minecraft identifier, optionally with the
namespace omitted and defaulted to `minecraft`
================================================
FILE: settings.gradle
================================================
pluginManagement {
repositories {
maven {
name = 'Fabric'
url = 'https://maven.fabricmc.net/'
}
gradlePluginPortal()
}
}
include 'owo-sentinel'
================================================
FILE: src/main/java/io/wispforest/owo/Owo.java
================================================
package io.wispforest.owo;
import io.wispforest.owo.client.screens.MenuNetworkingInternals;
import io.wispforest.owo.command.debug.OwoDebugCommands;
import io.wispforest.owo.ops.LootOps;
import io.wispforest.owo.text.CustomTextRegistry;
import io.wispforest.owo.text.InsertingTextContent;
import io.wispforest.owo.util.Wisdom;
import net.fabricmc.api.ModInitializer;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents;
import net.fabricmc.loader.api.FabricLoader;
import net.minecraft.ChatFormatting;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.Identifier;
import net.minecraft.server.MinecraftServer;
import org.jetbrains.annotations.ApiStatus;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static io.wispforest.owo.ops.TextOps.withColor;
public class Owo implements ModInitializer {
public static final String MOD_ID = "owo";
/**
* Whether oωo debug is enabled, this defaults to {@code true} in a development environment.
* To override that behavior, add the {@code -Dowo.debug=false} java argument
*/
public static final boolean DEBUG;
public static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID);
private static MinecraftServer SERVER;
public static final Component PREFIX = Component.empty().withStyle(ChatFormatting.GRAY)
.append(withColor("o", 0x3955e5))
.append(withColor("ω", 0x13a6f0))
.append(withColor("o", 0x3955e5))
.append(Component.literal(" > ").withStyle(ChatFormatting.GRAY));
static {
boolean debug = FabricLoader.getInstance().isDevelopmentEnvironment();
if (System.getProperty("owo.debug") != null) debug = Boolean.getBoolean("owo.debug");
if (Boolean.getBoolean("owo.forceDisableDebug")) {
LOGGER.warn("Deprecated system property 'owo.forceDisableDebug=true' was used - use 'owo.debug=false' instead");
debug = false;
}
DEBUG = debug;
}
@Override
@ApiStatus.Internal
public void onInitialize() {
LootOps.registerListener();
CustomTextRegistry.register("index", InsertingTextContent.CODEC);
MenuNetworkingInternals.init();
ServerLifecycleEvents.SERVER_STARTING.register(server -> SERVER = server);
ServerLifecycleEvents.SERVER_STOPPED.register(server -> SERVER = null);
Wisdom.spread();
if (!DEBUG) return;
OwoDebugCommands.register();
}
@ApiStatus.Internal
public static void debugWarn(Logger logger, String message) {
if (!DEBUG) return;
logger.warn(message);
}
@ApiStatus.Internal
public static void debugWarn(Logger logger, String message, Object... params) {
if (!DEBUG) return;
logger.warn(message, params);
}
/**
* @return The currently active minecraft server instance. If running
* on a physical client, this will return the integrated server while in
* a local singleplayer world and {@code null} otherwise
*/
public static MinecraftServer currentServer() {
return SERVER;
}
// "eh it's only like 10-15 of them what's the big deal" - glisco, while writing the 52nd hardcoded Identifier.of("owo", ...)
@ApiStatus.Internal
public static Identifier id(String path) {
return Identifier.fromNamespaceAndPath(MOD_ID, path);
}
}
================================================
FILE: src/main/java/io/wispforest/owo/blockentity/LinearProcess.java
================================================
package io.wispforest.owo.blockentity;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import net.minecraft.world.level.Level;
import java.util.function.BiConsumer;
import java.util.function.Predicate;
/**
* Represents a process made of steps than can be executed tick by tick using a respective
* {@link LinearProcessExecutor}. This can, for example, be used on BlockEntities that perform
* rituals or similar activities that are made of consecutive steps.
*
* A process defines the pattern of steps and events that shall be followed, thus there is one (usually static)
* instance of it. You then create a new instance of {@link LinearProcessExecutor} using the
* {@link #createExecutor(Object)} method for each instance of your BlockEntity of whatever else if supposed to run it
*
* To create a new process, call {@link #LinearProcess(int)} with the length it should have. A process always has the same
* length. Then, in the constructor of each object that will use an executor, use {@link #createExecutor(Object)} to
* obtain an instance. This then has to be told whether it lives on the client or server using
* {@link #configureExecutor(LinearProcessExecutor, boolean)}. On a BlockEntity this can be achieved by overriding
* {@link net.minecraft.world.level.block.entity.BlockEntity#setLevel(Level)} and configuring after the super call using the provided
* world
*
* Steps and events should be added to process once, ideally in the {@code static} initializer block of the containing class.
* After the process is complete, call {@link #finish()} to prevent further changes
*
* @param The type of object this process will be executed on,
* a {@link net.minecraft.world.level.block.entity.BlockEntity} in most cases
*/
public class LinearProcess {
private final Int2ObjectMap, T>> clientEventTable = new Int2ObjectOpenHashMap<>();
private final Int2ObjectMap> clientProcessStepTable = new Int2ObjectOpenHashMap<>();
private final Int2ObjectMap, T>> serverEventTable = new Int2ObjectOpenHashMap<>();
private final Int2ObjectMap> serverProcessStepTable = new Int2ObjectOpenHashMap<>();
private Predicate> condition = tLinearProcessExecutor -> true;
private final int processLength;
private boolean finished = false;
/**
* Creates a new process
*
* @param processLength The length of the process. This is immutable
*/
public LinearProcess(int processLength) {
this.processLength = processLength;
}
/**
* Creates a new executor for the given target object
*
* @param target The object the executor should operate on
* @return The created executor. This is not ready for use yet
* @see #configureExecutor(LinearProcessExecutor, boolean)
*/
public LinearProcessExecutor createExecutor(T target) {
if (!finished) throw new IllegalStateException("Illegal attempt to create executor for unfinished process");
return new LinearProcessExecutor<>(target, processLength, condition, serverProcessStepTable);
}
/**
* Configures an executor to use either the
* server or client instructions
*
* @param executor The executor to configure
* @param client {@code true} if the client instructions should be used
*/
public void configureExecutor(LinearProcessExecutor executor, boolean client) {
if (!finished) throw new IllegalStateException("Illegal attempt to configure executor using unfinished process");
if (client) {
executor.configure(clientEventTable, clientProcessStepTable);
} else {
executor.configure(serverEventTable, serverProcessStepTable);
}
}
/**
* Adds a new step to this process on both client and server
*
* @param when When the step should start
* @param length How long it should last
* @param executor The code to be run each tick while the step is active
*/
public void addCommonStep(int when, int length, BiConsumer, T> executor) {
checkForIllegalModification();
var step = new LinearProcessExecutor.ProcessStep<>(length, executor);
clientProcessStepTable.put(when, step);
serverProcessStepTable.put(when, step);
}
/**
* @see #addCommonStep(int, int, BiConsumer)
*/
public void addClientStep(int when, int length, BiConsumer, T> executor) {
checkForIllegalModification();
var step = new LinearProcessExecutor.ProcessStep<>(length, executor);
clientProcessStepTable.put(when, step);
}
/**
* @see #addCommonStep(int, int, BiConsumer)
*/
public void addServerStep(int when, int length, BiConsumer, T> executor) {
checkForIllegalModification();
var step = new LinearProcessExecutor.ProcessStep<>(length, executor);
serverProcessStepTable.put(when, step);
}
/**
* Adds an event that is executed once, on both client and server
*
* @param when When the event should occur
* @param executor The code to be run on the given tick
* @see #addClientEvent(int, BiConsumer)
* @see #addServerEvent(int, BiConsumer)
*/
public void addCommonEvent(int when, BiConsumer, T> executor) {
eventAtIndex(when, clientEventTable, executor);
eventAtIndex(when, serverEventTable, executor);
}
/**
* @see #addCommonEvent(int, BiConsumer)
*/
public void addClientEvent(int when, BiConsumer, T> executor) {
eventAtIndex(when, clientEventTable, executor);
}
/**
* @see #addCommonEvent(int, BiConsumer)
*/
public void addServerEvent(int when, BiConsumer, T> executor) {
eventAtIndex(when, serverEventTable, executor);
}
/**
* Defines code to be run when this process has successfully
* finished, on both client and server
*
* @param executor The code to be run
* @see #whenFinishedClient(BiConsumer)
* @see #whenFinishedServer(BiConsumer)
*/
public void whenFinishedCommon(BiConsumer, T> executor) {
eventAtIndex(LinearProcessExecutor.FINISH_EVENT_INDEX, clientEventTable, executor);
eventAtIndex(LinearProcessExecutor.FINISH_EVENT_INDEX, serverEventTable, executor);
}
/**
* @see #whenFinishedCommon(BiConsumer)
*/
public void whenFinishedServer(BiConsumer, T> executor) {
eventAtIndex(LinearProcessExecutor.FINISH_EVENT_INDEX, serverEventTable, executor);
}
/**
* @see #whenFinishedCommon(BiConsumer)
*/
public void whenFinishedClient(BiConsumer, T> executor) {
eventAtIndex(LinearProcessExecutor.FINISH_EVENT_INDEX, clientEventTable, executor);
}
/**
* Defines code to be run on both client and server when this process
* is unexpectedly cancelled mid-execution, use this to clean up after you.
*
* @param executor The code to be run
* @see #onCancelledClient(BiConsumer)
* @see #onCancelledServer(BiConsumer)
*/
public void onCancelledCommon(BiConsumer, T> executor) {
eventAtIndex(LinearProcessExecutor.CANCEL_EVENT_INDEX, clientEventTable, executor);
eventAtIndex(LinearProcessExecutor.CANCEL_EVENT_INDEX, serverEventTable, executor);
}
/**
* @see #onCancelledCommon(BiConsumer)
*/
public void onCancelledServer(BiConsumer, T> executor) {
eventAtIndex(LinearProcessExecutor.CANCEL_EVENT_INDEX, serverEventTable, executor);
}
/**
* @see #onCancelledCommon(BiConsumer)
*/
public void onCancelledClient(BiConsumer, T> executor) {
eventAtIndex(LinearProcessExecutor.CANCEL_EVENT_INDEX, clientEventTable, executor);
}
/**
* Defines a condition that has to be met every tick this process runs,
* otherwise it cancels itself
*
* @param condition The condition that should be satisfied during the entire
* process execution
*/
public void runConditionally(Predicate> condition) {
this.condition = condition;
}
/**
* Marks this process and completely built and ready for execution
*/
public void finish() {
this.finished = true;
}
private void checkForIllegalModification() {
if (finished) throw new IllegalStateException("Illegal attempt to modify finished process");
}
private void eventAtIndex(int index, Int2ObjectMap, T>> eventTable, BiConsumer, T> executor) {
checkForIllegalModification();
eventTable.put(index, executor);
}
}
================================================
FILE: src/main/java/io/wispforest/owo/blockentity/LinearProcessExecutor.java
================================================
package io.wispforest.owo.blockentity;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import net.minecraft.nbt.CompoundTag;
import org.jetbrains.annotations.ApiStatus;
import java.util.HashSet;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.Predicate;
/**
* A handler that executes the steps defined in a {@link LinearProcess}. Each object that is
* supposed to run the process needs an instance of this, and each instance of this refers back
* to the object it operates on
*
* @param The type of object this executor operates on
*/
public class LinearProcessExecutor {
public static final int CANCEL_EVENT_INDEX = -1;
public static final int FINISH_EVENT_INDEX = -2;
private final T target;
private final int processLength;
private final Predicate> condition;
private Int2ObjectMap, T>> eventTable;
private Int2ObjectMap> processStepTable;
private final Set> activeSteps = new HashSet<>();
private int processTick = 0;
protected LinearProcessExecutor(T target, int processLength, Predicate> condition, Int2ObjectMap> serverStepTable) {
this.target = target;
this.processLength = processLength;
this.condition = condition;
this.eventTable = null;
this.processStepTable = serverStepTable;
}
protected void configure(Int2ObjectMap, T>> eventTable, Int2ObjectMap> processStepTable) {
this.eventTable = eventTable;
this.processStepTable = processStepTable;
}
public void tick() {
if (this.eventTable == null) throw new IllegalStateException("Illegal attempt to tick unconfigured executor");
if (!this.running()) return;
if (this.cancelIfAppropriate()) return;
if (this.finishIfAppropriate()) return;
int tableIndex = processTick - 1;
if (this.eventTable.containsKey(tableIndex)) this.eventTable.get(tableIndex).accept(this, this.target);
if (this.processStepTable.containsKey(tableIndex)) this.activeSteps.add(this.processStepTable.get(tableIndex).createInfo(tableIndex));
this.activeSteps.removeIf(stepInfo -> !stepInfo.tick(this));
this.processTick++;
}
/**
* Attempts to begin execution
*
* @return {@code true} if execution will start next tick,
* {@code false} if execution is already running
*/
public boolean begin() {
if (this.processTick != 0) return false;
this.processTick = 1;
return true;
}
/**
* @return {@code true} if this executor is currently running
*/
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
public boolean running() {
return this.processTick > 0;
}
/**
* @return The last processing tick this executor completed
*/
public int getProcessTick() {
return processTick;
}
/**
* @return The object this executor is operating on
*/
public T getTarget() {
return target;
}
/**
* Attempts to instantly cancel execution
*
* @return {@code true} if execution was successfully cancelled,
* {@code false} if this executor was not running
*/
public boolean cancel() {
if (!this.running()) return false;
this.processTick = 0;
this.activeSteps.clear();
if (this.eventTable.containsKey(CANCEL_EVENT_INDEX)) this.eventTable.get(CANCEL_EVENT_INDEX).accept(this, this.target);
return true;
}
private boolean finishIfAppropriate() {
if (!this.running()) return false;
if (this.processTick < processLength) return false;
if (this.eventTable.containsKey(FINISH_EVENT_INDEX)) this.eventTable.get(FINISH_EVENT_INDEX).accept(this, this.target);
this.processTick = 0;
this.activeSteps.clear();
return true;
}
private boolean cancelIfAppropriate() {
if (this.condition.test(this)) return false;
this.cancel();
return true;
}
/**
* Saves the state of this executor
*
* @param targetTag The nbt to write state into
*/
public void writeState(CompoundTag targetTag) {
targetTag.putInt("ProcessTick", processTick);
}
/**
* Restores the saved state of this executor
*
* @param targetTag The nbt to read state from
*/
public void readState(CompoundTag targetTag) {
this.processTick = targetTag.getIntOr("ProcessTick", 0);
activeSteps.clear();
processStepTable.forEach((index, step) -> {
if (processTick >= index && processTick <= index + step.length) {
activeSteps.add(step.createInfo(index, processTick - index));
}
});
}
@ApiStatus.Internal
public record ProcessStep(int length, BiConsumer, T> executor) {
public Info createInfo(int index) {
return new Info<>(index, this);
}
public Info createInfo(int index, int tick) {
return new Info<>(index, tick, this);
}
public static final class Info {
private final ProcessStep step;
private final int index;
private int tick = 0;
public Info(int index, ProcessStep step) {
this.index = index;
this.step = step;
}
public Info(int index, int tick, ProcessStep step) {
this.index = index;
this.tick = tick;
this.step = step;
}
public boolean tick(LinearProcessExecutor target) {
this.tick++;
if (this.tick == step.length) return false;
this.step.executor.accept(target, target.getTarget());
return true;
}
}
}
}
================================================
FILE: src/main/java/io/wispforest/owo/braid/animation/AlignmentLerp.java
================================================
package io.wispforest.owo.braid.animation;
import io.wispforest.owo.braid.core.Alignment;
import net.minecraft.util.Mth;
public class AlignmentLerp extends Lerp {
public AlignmentLerp(Alignment start, Alignment end) {
super(start, end);
}
@Override
protected Alignment at(double t) {
return Alignment.of(
Mth.lerp(t, this.start.horizontal(), this.end.horizontal()),
Mth.lerp(t, this.start.vertical(), this.end.vertical())
);
}
}
================================================
FILE: src/main/java/io/wispforest/owo/braid/animation/Animation.java
================================================
package io.wispforest.owo.braid.animation;
import io.wispforest.owo.braid.framework.proxy.ProxyHost;
import net.minecraft.util.Mth;
import org.jetbrains.annotations.Nullable;
import java.time.Duration;
public class Animation {
private final Scheduler scheduler;
private final Listener listener;
private final @Nullable FinishListener finishListener;
public Easing easing;
public Duration duration;
private double progress;
private @Nullable Target target;
public Animation(Easing easing, Duration duration, Scheduler scheduler, Listener listener, @Nullable FinishListener finishListener, Target startFrom) {
this.easing = easing;
this.duration = duration;
this.scheduler = scheduler;
this.listener = listener;
this.finishListener = finishListener;
this.progress = startFrom.targetProgress;
}
public Animation(Easing easing, Duration duration, Scheduler scheduler, Listener listener, Target startFrom) {
this(easing, duration, scheduler, listener, null, startFrom);
}
public @Nullable Target target() {
return this.target;
}
public double progress() {
return this.easing.apply((float) this.progress);
}
public void towards(Target target) {
this.towards(target, true);
}
public void towards(Target target, boolean restart) {
if (restart) {
this.progress = 1 - target.targetProgress;
}
if (this.target == null) {
this.scheduler.schedule(this::callback);
}
this.target = target;
}
public void pause() {
this.target = null;
}
public void stop() {
this.stop(null);
}
public void stop(@Nullable Target at) {
if (this.target == null && at == null) return;
this.progress = at != null ? at.targetProgress : this.target.targetProgress;
this.target = null;
}
private void callback(Duration delta) {
if (this.target == null) return;
this.progress = Mth.clamp(
this.progress + this.target.direction * delta.toNanos() / (double) this.duration.toNanos(),
0,
1
);
this.listener.onUpdate(this.easing.apply((float) this.progress));
if (Math.abs(this.progress - this.target.targetProgress) > EPSILON) {
this.scheduler.schedule(this::callback);
} else {
if (this.finishListener != null) {
this.finishListener.onFinished(this.target);
}
this.progress = this.target.targetProgress;
this.target = null;
}
}
// ---
private static final double EPSILON = 1e-3;
// ---
public enum Target {
START(-1, 0),
END(1, 1);
public final long direction;
public final double targetProgress;
Target(long direction, double targetProgress) {
this.direction = direction;
this.targetProgress = targetProgress;
}
}
@FunctionalInterface
public interface Listener {
void onUpdate(double progress);
}
@FunctionalInterface
public interface FinishListener {
void onFinished(Target atTarget);
}
@FunctionalInterface
public interface Scheduler {
void schedule(ProxyHost.AnimationCallback callback);
}
}
================================================
FILE: src/main/java/io/wispforest/owo/braid/animation/AutomaticallyAnimatedWidget.java
================================================
package io.wispforest.owo.braid.animation;
import io.wispforest.owo.braid.framework.proxy.WidgetState;
import io.wispforest.owo.braid.framework.widget.StatefulWidget;
import org.apache.commons.lang3.mutable.MutableBoolean;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.time.Duration;
import java.util.Objects;
public abstract class AutomaticallyAnimatedWidget extends StatefulWidget {
private static final Logger log = LoggerFactory.getLogger(AutomaticallyAnimatedWidget.class);
public final Duration duration;
public final Easing easing;
protected AutomaticallyAnimatedWidget(Duration duration, Easing easing) {
this.duration = duration;
this.easing = easing;
}
@Override
public abstract State> createState();
@SuppressWarnings({"unchecked", "rawtypes"})
public static abstract class State extends WidgetState {
private Animation animation;
private LerpVisitor activeVisitor;
private void callback(double progress) {
this.setState(() -> {});
}
@Override
public void init() {
this.animation = new Animation(
this.widget().easing,
this.widget().duration,
this::scheduleAnimationCallback,
this::callback,
Animation.Target.END
);
this.visitLerps((previous, targetValue, factory) -> {
return factory.make(targetValue, targetValue);
});
}
@Override
public void didUpdateWidget(AutomaticallyAnimatedWidget oldWidget) {
var restartAnimation = new MutableBoolean(this.widget().easing != oldWidget.easing);
this.animation.duration = this.widget().duration;
if (restartAnimation.isFalse()) {
this.visitLerps((previous, targetValue, factory) -> {
if (!Objects.equals(previous.end, targetValue)) {
restartAnimation.setTrue();
}
return previous;
});
}
if (restartAnimation.isTrue()) {
this.visitLerps((previous, targetValue, factory) -> factory.make(previous.compute(this.animationValue()), targetValue));
this.animation.easing = this.widget().easing;
this.animation.towards(Animation.Target.END);
}
}
private void visitLerps(LerpVisitor visitor) {
this.activeVisitor = visitor;
this.updateLerps();
}
// ---
protected double animationValue() {
return this.animation.progress();
}
protected , V> L visitLerp(@Nullable Lerp previous, V targetValue, Lerp.Factory factory) {
return (L) this.activeVisitor.visit(previous, targetValue, factory);
}
protected , V> L visitNullableLerp(@Nullable Lerp previous, V targetValue, Lerp.Factory factory) {
return (L) this.activeVisitor.visit(previous, targetValue, (start, end) -> new NullableLerp(start, end, factory));
}
protected abstract void updateLerps();
}
@FunctionalInterface
private interface LerpVisitor, V> {
L visit(@Nullable Lerp previous, V targetValue, Lerp.Factory factory);
}
}
================================================
FILE: src/main/java/io/wispforest/owo/braid/animation/ColorLerp.java
================================================
package io.wispforest.owo.braid.animation;
import io.wispforest.owo.braid.core.Color;
public class ColorLerp extends Lerp {
public ColorLerp(Color start, Color end) {
super(start, end);
}
@Override
protected Color at(double t) {
return Color.mix(t, this.start, this.end);
}
}
================================================
FILE: src/main/java/io/wispforest/owo/braid/animation/DoubleLerp.java
================================================
package io.wispforest.owo.braid.animation;
import net.minecraft.util.Mth;
public class DoubleLerp extends Lerp {
public DoubleLerp(Double start, Double end) {
super(start, end);
}
@Override
protected Double at(double t) {
return Mth.lerp(t, this.start, this.end);
}
}
================================================
FILE: src/main/java/io/wispforest/owo/braid/animation/Easing.java
================================================
package io.wispforest.owo.braid.animation;
public class Easing {
public static final Easing LINEAR = new Easing(x -> x);
public static final Easing IN_QUAD = new Easing(x -> x * x);
public static final Easing OUT_QUAD = new Easing(x -> 1.0 - (1.0 - x) * (1.0 - x));
public static final Easing IN_OUT_QUAD = new Easing(x -> x < 0.5 ? 2.0 * x * x : 1.0 - Math.pow(-2.0 * x + 2.0, 2.0) / 2.0);
public static final Easing IN_CUBIC = new Easing(x -> x * x * x);
public static final Easing OUT_CUBIC = new Easing(x -> 1.0 - Math.pow(1.0 - x, 3));
public static final Easing IN_OUT_CUBIC = new Easing(x -> x < 0.5 ? 4.0 * x * x * x : 1.0 - Math.pow(-2.0 * x + 2.0, 3.0) / 2.0);
public static final Easing IN_QUART = new Easing(x -> x * x * x * x);
public static final Easing OUT_QUART = new Easing(x -> 1.0 - Math.pow(1.0 - x, 4.0));
public static final Easing IN_OUT_QUART = new Easing(x -> x < 0.5 ? 8.0 * x * x * x * x : 1.0 - Math.pow(-2.0 * x + 2.0, 4.0) / 2.0);
public static final Easing IN_QUINT = new Easing(x -> x * x * x * x * x);
public static final Easing OUT_QUINT = new Easing(x -> 1.0 - Math.pow(1.0 - x, 5.0));
public static final Easing IN_OUT_QUINT = new Easing(x -> x < 0.5 ? 16.0 * x * x * x * x * x : 1.0 - Math.pow(-2.0 * x + 2.0, 5.0) / 2.0);
public static final Easing IN_SINE = new Easing(x -> 1.0 - Math.cos((x * Math.PI) / 2.0));
public static final Easing OUT_SINE = new Easing(x -> Math.sin((x * Math.PI) / 2.0));
public static final Easing IN_OUT_SINE = new Easing(x -> -(Math.cos(Math.PI * x) - 1) / 2.0);
public static final Easing IN_EXPO = new Easing(x -> x == 0.0 ? 0.0 : Math.pow(2.0, 10.0 * x - 10.0));
public static final Easing OUT_EXPO = new Easing(x -> x == 1.0 ? 1.0 : 1.0 - Math.pow(2.0, -10.0 * x));
public static final Easing IN_OUT_EXPO = new Easing(x -> x == 0.0 ? 0.0 : x == 1.0 ? 1.0 : x < 0.5 ? Math.pow(2.0, 20.0 * x - 10.0) / 2.0 : (2.0 - Math.pow(2.0, -20.0 * x + 10.0)) / 2.0);
public static final Easing IN_CIRC = new Easing(x -> 1.0 - Math.sqrt(1.0 - Math.pow(x, 2.0)));
public static final Easing OUT_CIRC = new Easing(x -> Math.sqrt(1.0 - Math.pow(x - 1.0, 2.0)));
public static final Easing IN_OUT_CIRC = new Easing(x -> x < 0.5 ? (1.0 - Math.sqrt(1.0 - Math.pow(2.0 * x, 2.0))) / 2 : (Math.sqrt(1.0 - Math.pow(-2.0 * x + 2.0, 2.0)) + 1.0) / 2.0);
public static final Easing OUT_BOUNCE = new Easing(x -> {
var n1 = 7.5625;
var d1 = 2.75;
if (x < 1 / d1) {
return n1 * x * x;
} else if (x < 2 / d1) {
return n1 * (x -= 1.5 / d1) * x + 0.75;
} else if (x < 2.5 / d1) {
return n1 * (x -= 2.25 / d1) * x + 0.9375;
} else {
return n1 * (x -= 2.625 / d1) * x + 0.984375;
}
});
// ---
private final Function function;
public Easing(Function function) {
this.function = function;
}
public final double apply(double x) {
if (x == 0 || x == 1) return x;
return this.compute(x);
}
protected double compute(double x) {
return this.function.compute(x);
}
@FunctionalInterface
public interface Function {
double compute(double x);
}
}
================================================
FILE: src/main/java/io/wispforest/owo/braid/animation/InsetsLerp.java
================================================
package io.wispforest.owo.braid.animation;
import io.wispforest.owo.braid.core.Insets;
import net.minecraft.util.Mth;
public class InsetsLerp extends Lerp {
public InsetsLerp(Insets start, Insets end) {
super(start, end);
}
@Override
protected Insets at(double t) {
return Insets.of(
Mth.lerp(t, this.start.top(), this.end.top()),
Mth.lerp(t, this.start.bottom(), this.end.bottom()),
Mth.lerp(t, this.start.left(), this.end.left()),
Mth.lerp(t, this.start.right(), this.end.right())
);
}
}
================================================
FILE: src/main/java/io/wispforest/owo/braid/animation/Lerp.java
================================================
package io.wispforest.owo.braid.animation;
public abstract class Lerp {
public final T start;
public final T end;
protected Lerp(T start, T end) {
this.start = start;
this.end = end;
}
public T compute(double t) {
if (t - EPSILON <= 0) return this.start;
if (t + EPSILON >= 1) return this.end;
return this.at(t);
}
protected abstract T at(double t);
// ---
private static final double EPSILON = 1e-4;
// ---
@FunctionalInterface
public interface Factory, V> {
T make(V start, V end);
}
}
================================================
FILE: src/main/java/io/wispforest/owo/braid/animation/NullableLerp.java
================================================
package io.wispforest.owo.braid.animation;
import org.jetbrains.annotations.Nullable;
public class NullableLerp extends Lerp {
private final @Nullable Lerp delegate;
public NullableLerp(@Nullable T start, @Nullable T end, Lerp.Factory, T> delegateFactory) {
super(start, end);
if (start != null) {
this.delegate = delegateFactory.make(start, end);
} else {
this.delegate = null;
}
}
@Override
protected T at(double t) {
return this.delegate != null ? this.delegate.at(t) : this.end;
}
}
================================================
FILE: src/main/java/io/wispforest/owo/braid/core/Aabb2d.java
================================================
package io.wispforest.owo.braid.core;
import org.joml.Matrix3x2f;
import org.joml.Vector2f;
public class Aabb2d {
public double x;
public double y;
public double width;
public double height;
public Aabb2d(double x, double y, double width, double height) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
}
public double minX() {
return this.x;
}
public double maxX() {
return this.x + this.width;
}
public double minY() {
return this.y;
}
public double maxY() {
return this.y + this.height;
}
public Aabb2d transform(Matrix3x2f matrix) {
var topLeft = matrix.transformPosition((float) this.x, (float) this.y, new Vector2f());
var topRight = matrix.transformPosition((float) (this.x + this.width), (float) this.y, new Vector2f());
var bottomLeft = matrix.transformPosition((float) this.x, (float) (this.y + this.height), new Vector2f());
var bottomRight = matrix.transformPosition((float) (this.x + this.width), (float) (this.y + this.height), new Vector2f());
this.x = Math.min(Math.min(Math.min(topLeft.x, topRight.x), bottomLeft.x), bottomRight.x);
this.width = Math.max(Math.max(Math.max(topLeft.x, topRight.x), bottomLeft.x), bottomRight.x) - this.x;
this.y = Math.min(Math.min(Math.min(topLeft.y, topRight.y), bottomLeft.y), bottomRight.y);
this.height = Math.max(Math.max(Math.max(topLeft.y, topRight.y), bottomLeft.y), bottomRight.y) - this.y;
return this;
}
}
================================================
FILE: src/main/java/io/wispforest/owo/braid/core/Alignment.java
================================================
package io.wispforest.owo.braid.core;
import org.jetbrains.annotations.ApiStatus;
public record Alignment(double horizontal, double vertical) {
public static final Alignment TOP_LEFT = Alignment.of(0, 0);
public static final Alignment TOP = Alignment.of(.5, 0);
public static final Alignment TOP_RIGHT = Alignment.of(1, 0);
public static final Alignment LEFT = Alignment.of(0, .5);
public static final Alignment CENTER = Alignment.of(.5, .5);
public static final Alignment RIGHT = Alignment.of(1, .5);
public static final Alignment BOTTOM_LEFT = Alignment.of(0, 1);
public static final Alignment BOTTOM = Alignment.of(.5, 1);
public static final Alignment BOTTOM_RIGHT = Alignment.of(1, 1);
// ---
@ApiStatus.Internal
@Deprecated(forRemoval = true)
public Alignment {}
public static Alignment of(double horizontal, double vertical) {
return new Alignment(horizontal, vertical);
}
public double alignHorizontal(double space, double object) {
return Math.floor((space - object) * horizontal);
}
public double alignVertical(double space, double object) {
return Math.floor((space - object) * vertical);
}
}
================================================
FILE: src/main/java/io/wispforest/owo/braid/core/AppState.java
================================================
package io.wispforest.owo.braid.core;
import com.google.common.collect.Iterables;
import com.mojang.blaze3d.opengl.GlStateManager;
import io.wispforest.owo.braid.core.cursor.CursorStyle;
import io.wispforest.owo.braid.core.events.*;
import io.wispforest.owo.braid.framework.BuildContext;
import io.wispforest.owo.braid.framework.instance.*;
import io.wispforest.owo.braid.framework.proxy.BuildScope;
import io.wispforest.owo.braid.framework.proxy.ProxyHost;
import io.wispforest.owo.braid.framework.proxy.SingleChildInstanceWidgetProxy;
import io.wispforest.owo.braid.framework.proxy.WidgetProxy;
import io.wispforest.owo.braid.framework.widget.InheritedWidget;
import io.wispforest.owo.braid.framework.widget.SingleChildInstanceWidget;
import io.wispforest.owo.braid.framework.widget.Widget;
import io.wispforest.owo.braid.widgets.basic.Tooltip;
import io.wispforest.owo.braid.widgets.basic.VisitorWidget;
import io.wispforest.owo.braid.widgets.eventstream.BraidEventStream;
import io.wispforest.owo.braid.widgets.focus.FocusClickArea;
import io.wispforest.owo.braid.widgets.focus.RootFocusScope;
import io.wispforest.owo.braid.widgets.inspector.BraidInspector;
import io.wispforest.owo.braid.widgets.inspector.InstancePicker;
import io.wispforest.owo.util.EventSource;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.client.gui.screens.inventory.tooltip.ClientTooltipComponent;
import net.minecraft.network.chat.Style;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.joml.Vector2d;
import org.joml.Vector2dc;
import org.joml.Vector2f;
import org.lwjgl.glfw.GLFW;
import org.slf4j.Logger;
import java.time.Duration;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.function.Consumer;
public class AppState implements InstanceHost, ProxyHost {
public final @Nullable Logger logger;
private final Minecraft client;
public final Surface surface;
public final EventBinding eventBinding;
private final BuildScope rootBuildScope = new BuildScope();
private Deque animationCallbacks = new LinkedList<>();
private final PriorityQueue callbacks = new PriorityQueue<>();
private Deque postLayoutCallbacks = new LinkedList<>();
private final String name;
private final RootProxy root;
private final Vector2d cursorPosition = new Vector2d();
private Set hovered = new HashSet<>();
private final WeakHashMap mousePositions = new WeakHashMap<>();
private @Nullable MouseListener dragging = null;
private @Nullable CursorStyle draggingCursorStyle = null;
private int draggingButton = -1;
private KeyModifiers draggingModifiers = null;
private boolean dragStarted = false;
private static final Duration MIN_GRACE_PERIOD = Duration.ofMillis(200);
private static final Duration MAX_GRACE_PERIOD = Duration.ofMillis(500);
private static final int SCROLL_MOVEMENT_THRESHOLD = 5;
private @Nullable HitTestState scrollHit = null;
private Vector2d scrollPos = new Vector2d();
private Instant lastScrollTime = Instant.EPOCH;
private final BraidEventStream keyDownStream = new BraidEventStream<>();
private final BraidEventStream keyUpStream = new BraidEventStream<>();
private final BraidEventStream charStream = new BraidEventStream<>();
private final BraidHotReloadCallback.Listener reloadListener;
private final EventSource>.Subscription resizeSubscription;
private final List onTerminate = new ArrayList<>();
private boolean running = true;
private final BraidInspector inspector = new BraidInspector(this);
public AppState(
@Nullable Logger logger,
@Nullable String name,
Minecraft client,
Surface surface,
EventBinding eventBinding,
Widget root
) {
this.logger = logger;
this.client = client;
this.surface = surface;
this.eventBinding = eventBinding;
this.name = name != null ? name : root.getClass().getName();
this.root = new RootWidget(
new AppWidget(
this,
new InstancePicker(
this.inspector.onPick(),
this.inspector::revealInstance,
new RootFocusScope(
this.keyDownStream.source(),
this.keyUpStream.source(),
this.charStream.source(),
new UserRoot(
widgetProxy -> inspector.rootProxy = widgetProxy,
widgetInstance -> inspector.rootInstance = widgetInstance,
root
)
)
)
),
this.rootBuildScope
).proxy();
this.root.bootstrap(this, this);
this.scheduleLayout(this.rootInstance());
this.reloadListener = BraidHotReloadCallback.register();
this.resizeSubscription = this.surface.onResize().subscribe((newWidth, newHeight) -> {
this.rootInstance().markNeedsLayout();
});
}
public boolean running() {
return this.running;
}
public void onTerminate(Runnable callback) {
this.onTerminate.add(callback);
}
public void scheduleShutdown() {
this.running = false;
this.onTerminate.forEach(Runnable::run);
}
public void activateInspector() {
this.inspector.activate();
}
private @Nullable TooltipState activeTooltip;
public void draw(GuiGraphics graphics) {
this.surface.beginRendering();
graphics.push();
this.rootInstance().transform.transformToParent(graphics.pose());
var braidContext = BraidGraphics.create(graphics, this.surface);
GlStateManager._enableScissorTest();
this.rootInstance().draw(braidContext);
GlStateManager._disableScissorTest();
if (this.activeTooltip != null) {
if (this.activeTooltip.components() != null) braidContext.drawTooltip(this.client.font, this.activeTooltip.x(), this.activeTooltip.y(), this.activeTooltip.components());
if (this.activeTooltip.style() != null) graphics.renderComponentHoverEffect(this.client.font, this.activeTooltip.style(), this.activeTooltip.x(), this.activeTooltip.y());
}
graphics.pop();
this.surface.endRendering();
}
public void processEvents(float frameDeltaInTicks) {
this.pollAndDispatchEvents();
var state = this.hitTest();
var tooltipSupplier = state.firstWhere(hit -> hit.instance() instanceof TooltipProvider);
if (tooltipSupplier != null) {
var tooltip = (TooltipProvider) tooltipSupplier.instance();
var components = tooltip.getTooltipComponentsAt(tooltipSupplier.x(), tooltipSupplier.y());
var style = tooltip.getStyleAt(tooltipSupplier.x(), tooltipSupplier.y());
if (components != null || style != null) this.activeTooltip = new TooltipState(components, style, (int) this.cursorPosition.x, (int) this.cursorPosition.y);
} else {
this.activeTooltip = null;
}
// ---
var nowHovered = new HashSet();
for (var hit : Iterables.filter(state.occludedTrace(), hit -> hit.instance() instanceof MouseListener)) {
var listener = (MouseListener) hit.instance();
nowHovered.add(listener);
if (this.hovered.contains(listener)) {
this.hovered.remove(listener);
} else {
listener.onMouseEnter();
}
var mousePosition = this.mousePositions.getOrDefault(listener, MousePosition.ORIGIN);
if (mousePosition.x() != hit.x() || mousePosition.y() != hit.y()) {
listener.onMouseMove(hit.x(), hit.y());
this.mousePositions.put(listener, new MousePosition(hit.x(), hit.y()));
}
}
for (var noLongerHovered : this.hovered) {
noLongerHovered.onMouseExit();
}
this.hovered = nowHovered;
// ---
@Nullable CursorStyle activeStyle = null;
if (this.dragging != null) {
activeStyle = this.draggingCursorStyle;
} else {
var cursorStyleSource = state.firstWhere(
(hit) ->
hit.instance() instanceof MouseListener &&
((MouseListener) hit.instance()).cursorStyleAt(hit.x(), hit.y()) != null
);
if (cursorStyleSource != null) {
activeStyle = ((MouseListener) cursorStyleSource.instance()).cursorStyleAt(
cursorStyleSource.x(),
cursorStyleSource.y()
);
}
}
this.surface.setCursorStyle(activeStyle != null ? activeStyle : CursorStyle.NONE);
// ---
if (this.reloadListener.poll()) {
this.rebuildRoot();
}
if (!this.animationCallbacks.isEmpty()) {
var callbacksForThisFrame = this.animationCallbacks;
this.animationCallbacks = new LinkedList<>();
while (!callbacksForThisFrame.isEmpty()) {
callbacksForThisFrame.poll().run(Duration.ofMillis((long) (frameDeltaInTicks * 50)));
}
}
var now = Instant.now();
while (!this.callbacks.isEmpty() && this.callbacks.peek().after().isBefore(now)) {
this.callbacks.poll().callback().run();
}
var anyTreeMutations = false;
anyTreeMutations |= this.rootBuildScope.rebuildDirtyProxies();
anyTreeMutations |= this.flushLayoutQueue();
if (anyTreeMutations) {
this.inspector.refresh();
}
if (!this.postLayoutCallbacks.isEmpty()) {
var callbacksForThisFrame = this.postLayoutCallbacks;
this.postLayoutCallbacks = new LinkedList<>();
while (!callbacksForThisFrame.isEmpty()) {
callbacksForThisFrame.poll().run();
}
}
}
private void pollAndDispatchEvents() {
var events = this.eventBinding.poll();
for (var slot : events) {
switch (slot.event) {
case MouseButtonPressEvent(int button, KeyModifiers modifiers) -> {
this.scrollHit = null;
var state = this.hitTest();
state.firstWhere(hit -> {
if (!(hit.instance() instanceof FocusClickArea.Instance instance)) return false;
instance.widget().clickCallback.run();
return true;
});
var clicked = state.firstWhere(
(hit) -> hit.instance() instanceof MouseListener && ((MouseListener) hit.instance()).onMouseDown(hit.x(), hit.y(), button, modifiers)
);
if (clicked != null) {
slot.markHandled();
if (this.dragging == null) {
this.dragging = (MouseListener) clicked.instance();
this.draggingCursorStyle = ((MouseListener) clicked.instance()).cursorStyleAt(
clicked.x(),
clicked.y()
);
this.dragStarted = false;
this.draggingButton = button;
this.draggingModifiers = modifiers;
}
}
}
case MouseMoveEvent(double x, double y) -> {
slot.markHandled();
var deltaX = x - this.cursorPosition.x;
var deltaY = y - this.cursorPosition.y;
if (deltaX == 0 && deltaY == 0) break;
this.cursorPosition.x = x;
this.cursorPosition.y = y;
if (this.cursorPosition.distance(this.scrollPos) > SCROLL_MOVEMENT_THRESHOLD) this.scrollHit = null;
if (!(this.dragging instanceof WidgetInstance>)) break;
if (!this.dragStarted) {
this.dragging.onMouseDragStart(draggingButton, draggingModifiers);
this.dragStarted = true;
}
var globalTransform = ((WidgetInstance>) this.dragging).computeGlobalTransform();
var coordinates = new Vector2f((float) x, (float) y);
globalTransform.transformPosition(coordinates);
// apply *only the rotation* of the instance's transform
// to the mouse movement
var delta = new Vector2f((float) deltaX, (float) deltaY);
globalTransform.transformDirection(delta);
this.dragging.onMouseDrag(coordinates.x, coordinates.y, delta.x, delta.y);
}
case MouseButtonReleaseEvent(int button, KeyModifiers modifiers) -> {
this.scrollHit = null;
var state = this.hitTest();
var unClicked = state.firstWhere(
(hit) -> hit.instance() instanceof MouseListener && ((MouseListener) hit.instance()).onMouseUp(hit.x(), hit.y(), button, modifiers)
);
if (unClicked != null) {
slot.markHandled();
}
if (this.draggingButton == button) {
if (this.dragStarted && this.dragging != null) {
this.dragging.onMouseDragEnd();
}
this.dragging = null;
}
}
case MouseScrollEvent(double xOffset, double yOffset) -> {
var now = Instant.now();
var grace = this.cursorPosition.distance(this.scrollPos) > SCROLL_MOVEMENT_THRESHOLD ? MIN_GRACE_PERIOD : MAX_GRACE_PERIOD;
if (this.scrollHit == null || now.minus(grace).isAfter(this.lastScrollTime) ) this.scrollHit = this.hitTest();
this.lastScrollTime = now;
this.scrollPos = new Vector2d(this.cursorPosition);
var scrolled = this.scrollHit.firstWhere(
(hit) -> hit.instance() instanceof MouseListener &&
((MouseListener) hit.instance()).onMouseScroll(
hit.x(),
hit.y(),
xOffset,
yOffset
)
);
if (scrolled != null) {
slot.markHandled();
}
}
case KeyPressEvent(int keyCode, int scancode, KeyModifiers modifiers) -> {
if (keyCode == GLFW.GLFW_KEY_R && modifiers.shift() && modifiers.alt()) {
this.rebuildRoot();
slot.markHandled();
break;
}
if (keyCode == GLFW.GLFW_KEY_I && modifiers.ctrl() && modifiers.shift()) {
this.inspector.activate();
slot.markHandled();
break;
}
var event = new RootFocusScope.KeyDownEvent(keyCode, modifiers);
this.keyDownStream.sink().onEvent(event);
if (event.handled()) {
slot.markHandled();
}
}
case KeyReleaseEvent(int keycode, int scancode, KeyModifiers modifiers) -> {
var event = new RootFocusScope.KeyUpEvent(keycode, modifiers);
this.keyUpStream.sink().onEvent(event);
if (event.handled()) {
slot.markHandled();
}
}
case CharInputEvent(char codepoint, KeyModifiers modifiers) -> {
var event = new RootFocusScope.CharEvent(codepoint, modifiers);
this.charStream.sink().onEvent(event);
if (event.handled()) {
slot.markHandled();
}
}
case FilesDroppedEvent filesDroppedEvent -> {}
case CloseEvent ignored -> {
slot.markHandled();
this.scheduleShutdown();
}
}
}
}
public void rebuildRoot() {
var before = Instant.now();
this.root.reassemble();
var elapsed = ChronoUnit.MICROS.between(before, Instant.now());
if (this.logger != null) this.logger.debug("completed full app rebuild in {}us", elapsed);
}
public void dispose() {
this.inspector.close();
this.reloadListener.unregister();
this.resizeSubscription.cancel();
this.surface.dispose();
this.root.unmount();
}
private HitTestState hitTest() {
return this.hitTest(this.cursorPosition.x, this.cursorPosition.y);
}
public HitTestState hitTest(double x, double y) {
var state = new HitTestState();
this.rootInstance().hitTest(x, y, state);
return state;
}
// ---
@Override
public Minecraft client() {
return this.client;
}
public SingleChildWidgetInstance> rootInstance() {
return this.root.instance();
}
// ---
private List> layoutQueue = new ArrayList<>();
private boolean mergeToLayoutQueue = false;
private boolean flushLayoutQueue() {
if (this.layoutQueue.isEmpty()) return false;
while (!this.layoutQueue.isEmpty()) {
var queue = this.layoutQueue;
this.layoutQueue = new ArrayList<>();
queue.sort(Comparator.naturalOrder());
for (var idx = 0; idx < queue.size(); idx++) {
var instance = queue.get(idx);
if (this.mergeToLayoutQueue) {
this.mergeToLayoutQueue = false;
if (!this.layoutQueue.isEmpty()) {
this.layoutQueue.addAll(queue.subList(idx, queue.size()));
break;
}
}
if (instance.needsLayout()) {
instance.layout(
instance.hasParent()
? instance.constraints()
: Constraints.tight(Size.of(this.surface.width(), this.surface.height()))
);
}
}
this.mergeToLayoutQueue = false;
}
return true;
}
@Override
public void scheduleLayout(WidgetInstance> instance) {
this.layoutQueue.add(instance);
}
@Override
public void notifySubtreeRebuild() {
this.mergeToLayoutQueue = true;
}
@Override
public void scheduleAnimationCallback(AnimationCallback callback) {
this.animationCallbacks.offer(callback);
}
@Override
public long scheduleDelayedCallback(Duration delay, Runnable callback) {
var id = ScheduledCallback.nextId++;
this.callbacks.add(new ScheduledCallback(
Instant.now().plus(delay),
callback, id
));
return id;
}
@Override
public void cancelDelayedCallback(long id) {
this.callbacks.removeIf(scheduledCallback -> scheduledCallback.id() == id);
}
@Override
public void schedulePostLayoutCallback(Runnable callback) {
this.postLayoutCallbacks.offer(callback);
}
@Override
public Vector2dc cursorPosition() {
return this.cursorPosition;
}
@Override
public String toString() {
return String.format("%s (AppState@%s)", this.name, Integer.toHexString(hashCode()));
}
// ---
public static String formatName(String category, Widget userRoot) {
var classPath = userRoot.getClass().getName().split("\\.");
return String.format("%s[%s]", category, classPath[classPath.length - 1]);
}
public static String formatName(String category, Widget userRoot, String... attributes) {
var classPath = userRoot.getClass().getName().split("\\.");
return String.format("%s[%s, %s]", category, String.join(", ", attributes), classPath[classPath.length - 1]);
}
public static AppState of(BuildContext context) {
//noinspection DataFlowIssue
return context.getAncestor(AppWidget.class).app;
}
}
record ScheduledCallback(Instant after, Runnable callback, long id) implements Comparable {
//"fuck you we starting at 7" -chyz
public static long nextId = 7;
@Override
public int compareTo(@NotNull ScheduledCallback o) {
return this.after.compareTo(o.after);
}
}
class RootWidget extends SingleChildInstanceWidget {
public final BuildScope rootBuildScope;
public RootWidget(Widget child, BuildScope rootBuildScope) {
super(child);
this.rootBuildScope = rootBuildScope;
}
@Override
public RootProxy proxy() {
return new RootProxy(this);
}
@Override
public RootInstance instantiate() {
return new RootInstance(this);
}
}
class RootProxy extends SingleChildInstanceWidgetProxy {
public RootProxy(RootWidget widget) {
super(widget);
}
@Override
public BuildScope buildScope() {
return ((RootWidget) this.widget()).rootBuildScope;
}
@Override
public boolean mounted() {
return this.bootstrapped;
}
private boolean bootstrapped = false;
void bootstrap(InstanceHost instanceHost, ProxyHost proxyHost) {
this.bootstrapped = true;
this.lifecycle = Lifecycle.LIVE;
this.rootSetHost(proxyHost);
rebuild();
this.setDepth(0);
this.instance.setDepth(0);
this.instance.attachHost(instanceHost);
}
}
class RootInstance extends SingleChildWidgetInstance.ShrinkWrap {
public RootInstance(RootWidget widget) {
super(widget);
}
}
class UserRoot extends VisitorWidget {
public final Consumer proxyCallback;
public final Consumer> instanceCallback;
public UserRoot(Consumer proxyCallback, Consumer> instanceCallback, Widget child) {
super(child);
this.proxyCallback = proxyCallback;
this.instanceCallback = instanceCallback;
}
private static final Visitor VISITOR = (widget, instance) -> {
widget.instanceCallback.accept(instance);
};
@Override
public Proxy> proxy() {
var proxy = new Proxy<>(this, VISITOR);
this.proxyCallback.accept(proxy);
return proxy;
}
}
class AppWidget extends InheritedWidget {
public final AppState app;
protected AppWidget(AppState app, Widget child) {
super(child);
this.app = app;
}
@Override
public boolean mustRebuildDependents(InheritedWidget newWidget) {
if (((AppWidget) newWidget).app != this.app) {
throw new UnsupportedOperationException("changing the AppState of a widget tree is not supported");
}
return false;
}
}
record TooltipState(@Nullable List components, @Nullable Style style, int x, int y) {}
record MousePosition(double x, double y) {
public static final MousePosition ORIGIN = new MousePosition(0, 0);
}
================================================
FILE: src/main/java/io/wispforest/owo/braid/core/BraidGraphics.java
================================================
package io.wispforest.owo.braid.core;
import com.mojang.blaze3d.pipeline.RenderPipeline;
import io.wispforest.owo.braid.core.element.BraidDashedLineElement;
import io.wispforest.owo.mixin.braid.Matrix3x2fStackAccessor;
import io.wispforest.owo.mixin.ui.access.GuiGraphicsAccessor;
import io.wispforest.owo.ui.core.OwoUIGraphics;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.client.gui.navigation.ScreenRectangle;
import net.minecraft.client.gui.render.state.GuiRenderState;
import org.joml.Matrix3x2f;
import org.joml.Matrix3x2fStack;
import org.joml.Matrix3x2fc;
import java.util.function.Consumer;
public class BraidGraphics extends OwoUIGraphics {
private final Surface surface;
protected BraidGraphics(Minecraft client, GuiRenderState renderState, int mouseX, int mouseY, Consumer setTooltipDrawer, Surface surface) {
super(client, renderState, mouseX, mouseY, setTooltipDrawer);
this.surface = surface;
}
public static BraidGraphics create(GuiGraphics grpahics, Surface surface) {
var braidContext = new BraidGraphics(
Minecraft.getInstance(),
grpahics.guiRenderState,
((GuiGraphicsAccessor) grpahics).owo$getMouseX(),
((GuiGraphicsAccessor) grpahics).owo$getMouseY(),
((GuiGraphicsAccessor) grpahics)::owo$setDeferredTooltip,
surface
);
((GuiGraphicsAccessor) braidContext).owo$setScissorStack(((GuiGraphicsAccessor) grpahics).owo$getScissorStack());
((GuiGraphicsAccessor) braidContext).owo$setPose(new MatrixStack(((GuiGraphicsAccessor) grpahics).owo$getPose()));
return braidContext;
}
@Override
public int guiWidth() {
return this.surface.width();
}
@Override
public int guiHeight() {
return this.surface.height();
}
public void buildRectOutline(double x, double y, double width, double height, RectEdgeBuilder builder) {
builder.edge(x, y, x + width, y);
builder.edge(x, y + height, x + width, y + height);
builder.edge(x, y, x, y + height);
builder.edge(x + width, y, x + width, y + height);
}
public void drawDashedLine(RenderPipeline pipeline, double x1, double y1, double x2, double y2, double thiccness, double segmentLength, Color color) {
this.guiRenderState.submitGuiElement(new BraidDashedLineElement(
color,
thiccness,
segmentLength,
pipeline,
new Matrix3x2f(this.pose()),
new ScreenRectangle((int) x1, (int) y1, (int) (x2 - x1), (int) (y2 - y1)),
this.scissorStack.peek()
));
}
@FunctionalInterface
public interface RectEdgeBuilder {
void edge(double x1, double y1, double x2, double y2);
}
@SuppressWarnings("ExternalizableWithoutPublicNoArgConstructor")
public static class MatrixStack extends Matrix3x2fStack {
public MatrixStack(Matrix3x2fc source) {
super(16);
this.mul(source);
}
@Override
public Matrix3x2fStack pushMatrix() {
var accessor = (Matrix3x2fStackAccessor) this;
if (accessor.owo$getCurr() == accessor.owo$getMats().length) {
var newMats = new Matrix3x2f[accessor.owo$getMats().length * 2];
System.arraycopy(accessor.owo$getMats(), 0, newMats, 0, accessor.owo$getMats().length);
for (int idx = newMats.length / 2; idx < newMats.length; idx++) {
newMats[idx] = new Matrix3x2f();
}
accessor.owo$setMats(newMats);
}
return super.pushMatrix();
}
}
}
================================================
FILE: src/main/java/io/wispforest/owo/braid/core/BraidHotReloadCallback.java
================================================
package io.wispforest.owo.braid.core;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
public final class BraidHotReloadCallback {
private static final Set LISTENERS = new HashSet<>();
public static final Logger LOGGER = LoggerFactory.getLogger("braid reload agent");
public static Listener register() {
var listener = new Listener();
LISTENERS.add(listener);
return listener;
}
@ApiStatus.Internal
public static void setupComplete() {
LOGGER.info("setup complete, debounce time is {}ms", Listener.DEBOUNCE_TIME);
}
@ApiStatus.Internal
public static void invoke() {
for (var listener : LISTENERS) {
listener.triggered.set(true);
}
}
public static class Listener {
private static final int DEBOUNCE_TIME = Integer.getInteger("owo.braid.hotswapDebounceTime", 250);
private final AtomicBoolean triggered = new AtomicBoolean();
private @Nullable Instant lastTriggerTimestamp = null;
public boolean poll() {
if (this.triggered.getAndSet(false)) {
this.lastTriggerTimestamp = Instant.now();
}
if (this.lastTriggerTimestamp != null && ChronoUnit.MILLIS.between(this.lastTriggerTimestamp, Instant.now()) > DEBOUNCE_TIME) {
this.lastTriggerTimestamp = null;
return true;
}
return false;
}
public void unregister() {
LISTENERS.remove(this);
}
}
}
================================================
FILE: src/main/java/io/wispforest/owo/braid/core/BraidRenderPipelines.java
================================================
package io.wispforest.owo.braid.core;
import com.mojang.blaze3d.pipeline.RenderPipeline;
import io.wispforest.owo.Owo;
import net.minecraft.client.renderer.RenderPipelines;
import org.jetbrains.annotations.ApiStatus;
public class BraidRenderPipelines {
public static final RenderPipeline TEXTURED_DEFAULT = RenderPipeline.builder(RenderPipelines.GUI_TEXTURED_SNIPPET)
.withLocation(Owo.id("pipeline/braid_textured_default"))
.build();
public static final RenderPipeline TEXTURED_NEAREST = RenderPipeline.builder(RenderPipelines.GUI_TEXTURED_SNIPPET)
.withLocation(Owo.id("pipeline/braid_textured_nearest"))
.build();
public static final RenderPipeline TEXTURED_BILINEAR = RenderPipeline.builder(RenderPipelines.GUI_TEXTURED_SNIPPET)
.withLocation(Owo.id("pipeline/braid_textured_bilinear"))
.build();
@ApiStatus.Internal
public static void register() {
RenderPipelines.register(TEXTURED_DEFAULT);
RenderPipelines.register(TEXTURED_NEAREST);
RenderPipelines.register(TEXTURED_BILINEAR);
}
}
================================================
FILE: src/main/java/io/wispforest/owo/braid/core/BraidScreen.java
================================================
package io.wispforest.owo.braid.core;
import io.wispforest.owo.braid.core.events.*;
import io.wispforest.owo.braid.framework.BuildContext;
import io.wispforest.owo.braid.framework.widget.InheritedWidget;
import io.wispforest.owo.braid.framework.widget.Widget;
import io.wispforest.owo.braid.widgets.BraidApp;
import io.wispforest.owo.ui.util.DisposableScreen;
import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.client.gui.screens.Screen;
import net.minecraft.client.input.CharacterEvent;
import net.minecraft.client.input.KeyEvent;
import net.minecraft.client.input.MouseButtonEvent;
import net.minecraft.network.chat.Component;
import org.jetbrains.annotations.Nullable;
public class BraidScreen extends Screen implements DisposableScreen {
protected final EventBinding eventBinding = new EventBinding.Default();
protected final Surface.Default surface = new Surface.Default();
protected final Settings settings;
protected final Widget rootWidget;
public AppState state;
public BraidScreen(Settings settings, Widget rootWidget) {
super(Component.empty());
this.settings = settings;
this.rootWidget = rootWidget;
}
public BraidScreen(Widget rootWidget) {
this(new Settings(), rootWidget);
}
@Override
protected void init() {
super.init();
if (this.state == null) {
var widget = this.settings.useBraidAppWidget
? new BraidApp(this.rootWidget)
: this.rootWidget;
this.state = new AppState(
null,
AppState.formatName("BraidScreen", this.rootWidget),
this.minecraft,
this.surface,
this.eventBinding,
new BraidScreenProvider(this, widget)
);
}
}
@Override
public void render(GuiGraphics graphics, int mouseX, int mouseY, float delta) {
super.render(graphics, mouseX, mouseY, delta);
this.eventBinding.add(new MouseMoveEvent(mouseX, mouseY));
this.state.processEvents(
this.minecraft.getDeltaTracker().getGameTimeDeltaTicks()
);
this.state.draw(graphics);
}
@Override
public void dispose() {
this.state.dispose();
}
@Override
public boolean isPauseScreen() {
return this.settings.shouldPause;
}
@Override
public boolean mouseClicked(MouseButtonEvent click, boolean doubled) {
this.eventBinding.add(new MouseButtonPressEvent(click.button(), click.modifiers()));
return true;
}
@Override
public boolean mouseReleased(MouseButtonEvent click) {
this.eventBinding.add(new MouseButtonReleaseEvent(click.button(), click.modifiers()));
return true;
}
@Override
public boolean mouseScrolled(double mouseX, double mouseY, double horizontalAmount, double verticalAmount) {
this.eventBinding.add(new MouseScrollEvent(horizontalAmount, verticalAmount));
return true;
}
@Override
public boolean keyPressed(KeyEvent input) {
this.eventBinding.add(new KeyPressEvent(input.key(), input.scancode(), input.modifiers()));
return super.keyPressed(input);
}
@Override
public boolean keyReleased(KeyEvent input) {
this.eventBinding.add(new KeyReleaseEvent(input.key(), input.scancode(), input.modifiers()));
return true;
}
@Override
public boolean charTyped(CharacterEvent input) {
this.eventBinding.add(new CharInputEvent((char) input.codepoint(), input.modifiers()));
return true;
}
// ---
public static @Nullable BraidScreen maybeOf(BuildContext context) {
var provider = context.getAncestor(BraidScreenProvider.class);
return provider != null ? provider.screen : null;
}
public static class Settings {
public boolean shouldPause = true;
public boolean useBraidAppWidget = true;
}
}
class BraidScreenProvider extends InheritedWidget {
public final BraidScreen screen;
public BraidScreenProvider(BraidScreen screen, Widget child) {
super(child);
this.screen = screen;
}
@Override
public boolean mustRebuildDependents(InheritedWidget newWidget) {
return false;
}
}
================================================
FILE: src/main/java/io/wispforest/owo/braid/core/BraidUtils.java
================================================
package io.wispforest.owo.braid.core;
import java.util.function.BiFunction;
public class BraidUtils {
public static T fold(Iterable values, T initial, BiFunction step) {
var result = initial;
for (var value : values) {
result = step.apply(result, value);
}
return result;
}
}
================================================
FILE: src/main/java/io/wispforest/owo/braid/core/BraidWindow.java
================================================
package io.wispforest.owo.braid.core;
import com.mojang.blaze3d.opengl.GlDebug;
import com.mojang.blaze3d.opengl.GlTexture;
import com.mojang.blaze3d.pipeline.TextureTarget;
import com.mojang.blaze3d.systems.RenderSystem;
import io.wispforest.owo.Owo;
import io.wispforest.owo.braid.core.cursor.CursorController;
import io.wispforest.owo.braid.core.cursor.CursorStyle;
import io.wispforest.owo.braid.core.events.*;
import io.wispforest.owo.braid.framework.widget.Widget;
import io.wispforest.owo.braid.util.BraidGuiRenderer;
import io.wispforest.owo.util.EventSource;
import io.wispforest.owo.util.EventStream;
import net.minecraft.client.Minecraft;
import org.apache.commons.lang3.mutable.MutableLong;
import org.lwjgl.glfw.*;
import org.lwjgl.opengl.GL32;
import org.lwjgl.system.NativeResource;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
// TODO: consider somehow getting notified or polling
// for changes in the gui scale option so we can react
// instantly when it changes rather than on next resize
public class BraidWindow implements Surface {
public final EventBinding eventBinding = new WindowEventBinding(this);
public final long handle;
private final List resources = new ArrayList<>();
private final EventStream onResize = ResizeCallback.newStream();
private TextureTarget remoteTarget;
private int localFbo;
public final BraidGuiRenderer guiRenderer;
private final CursorController cursorController;
private int framebufferWidth;
private int framebufferHeight;
private int scaledWidth;
private int scaledHeight;
private int scaleFactor;
public BraidWindow(long handle) {
this.handle = handle;
this.cursorController = new CursorController(this.handle);
this.guiRenderer = new BraidGuiRenderer(Minecraft.getInstance());
var framebufferWidthOut = new int[1];
var framebufferHeightOut = new int[1];
GLFW.glfwGetFramebufferSize(this.handle, framebufferWidthOut, framebufferHeightOut);
this.framebufferWidth = framebufferWidthOut[0];
this.framebufferHeight = framebufferHeightOut[0];
this.remoteTarget = new TextureTarget("braid window", this.framebufferWidth, this.framebufferHeight, true);
this.recreateLocalFbo();
GLFW.glfwSetWindowCloseCallback(this.handle, this.storeNativeResource(GLFWWindowCloseCallback.create(window -> {
this.eventBinding.add(CloseEvent.INSTANCE);
})));
GLFW.glfwSetFramebufferSizeCallback(this.handle, this.storeNativeResource(GLFWFramebufferSizeCallback.create((window, width, height) -> {
this.framebufferWidth = width;
this.framebufferHeight = height;
withContext(Minecraft.getInstance().getWindow().handle(), () -> {
this.remoteTarget.destroyBuffers();
this.remoteTarget = new TextureTarget("braid window", this.framebufferWidth, this.framebufferHeight, true);
});
this.recreateLocalFbo();
this.onResize.sink().onResize(this.scaledWidth, this.scaledHeight);
})));
GLFW.glfwSetMouseButtonCallback(this.handle, this.storeNativeResource(GLFWMouseButtonCallback.create((window, button, action, mods) -> {
this.eventBinding.add(switch (action) {
case GLFW.GLFW_PRESS -> new MouseButtonPressEvent(button, new KeyModifiers(mods));
case GLFW.GLFW_RELEASE -> new MouseButtonReleaseEvent(button, new KeyModifiers(mods));
default -> throw new UnsupportedOperationException("incompatible glfw event type");
});
})));
GLFW.glfwSetCursorPosCallback(this.handle, this.storeNativeResource(GLFWCursorPosCallback.create((window, mouseX, mouseY) -> {
this.eventBinding.add(new MouseMoveEvent(
mouseX / this.scaleFactor,
mouseY / this.scaleFactor
));
})));
GLFW.glfwSetScrollCallback(this.handle, this.storeNativeResource(GLFWScrollCallback.create((window, xOffset, yOffset) -> {
this.eventBinding.add(new MouseScrollEvent(xOffset, yOffset));
})));
GLFW.glfwSetKeyCallback(this.handle, this.storeNativeResource(GLFWKeyCallback.create((window, key, scancode, action, mods) -> {
this.eventBinding.add(switch (action) {
case GLFW.GLFW_PRESS, GLFW.GLFW_REPEAT -> new KeyPressEvent(key, scancode, new KeyModifiers(mods));
case GLFW.GLFW_RELEASE -> new KeyReleaseEvent(key, scancode, new KeyModifiers(mods));
default -> throw new UnsupportedOperationException("incompatible glfw event type");
});
})));
GLFW.glfwSetCharModsCallback(this.handle, this.storeNativeResource(GLFWCharModsCallback.create((window, codepoint, mods) -> {
this.eventBinding.add(new CharInputEvent((char) codepoint, new KeyModifiers(mods)));
})));
GLFW.glfwSetDropCallback(this.handle, this.storeNativeResource(GLFWDropCallback.create((window, count, names) -> {
var paths = new ArrayList(count);
for (int pathIdx = 0; pathIdx < count; pathIdx++) {
var pathString = GLFWDropCallback.getName(names, pathIdx);
try {
paths.add(Paths.get(pathString));
} catch (InvalidPathException e) {
Owo.LOGGER.error("Failed to parse path '{}'", pathString, e);
}
}
if (!paths.isEmpty()) {
this.eventBinding.add(new FilesDroppedEvent(paths));
}
})));
}
private void recreateLocalFbo() {
withContext(this.handle, () -> {
if (this.localFbo != 0) {
GL32.glDeleteFramebuffers(this.localFbo);
}
this.localFbo = GL32.glGenFramebuffers();
GL32.glBindFramebuffer(GL32.GL_FRAMEBUFFER, this.localFbo);
GL32.glFramebufferTexture2D(GL32.GL_FRAMEBUFFER, GL32.GL_COLOR_ATTACHMENT0, GL32.GL_TEXTURE_2D, ((GlTexture) this.remoteTarget.getColorTexture()).glId(), 0);
if (GL32.glCheckFramebufferStatus(GL32.GL_FRAMEBUFFER) != GL32.GL_FRAMEBUFFER_COMPLETE) {
throw new UnsupportedOperationException("Failed to initialize local FBO");
}
});
this.recalculateScale();
}
private void recalculateScale() {
var guiScale = Minecraft.getInstance().options.guiScale().get();
var forceUnicodeFont = Minecraft.getInstance().options.forceUnicodeFont().get();
var factor = 1;
while (
factor != guiScale
&& factor < this.framebufferWidth
&& factor < this.framebufferHeight
&& this.framebufferWidth / (factor + 1) >= 320
&& this.framebufferHeight / (factor + 1) >= 240
) {
++factor;
}
if (forceUnicodeFont && factor % 2 != 0) {
++factor;
}
this.scaleFactor = factor;
var scaledWidth = (int) ((double) this.framebufferWidth / this.scaleFactor);
this.scaledWidth = (double) this.framebufferWidth / this.scaleFactor > (double) scaledWidth ? scaledWidth + 1 : scaledWidth;
var scaledHeight = (int) ((double) this.framebufferHeight / this.scaleFactor);
this.scaledHeight = (double) this.framebufferHeight / this.scaleFactor > (double) scaledHeight ? scaledHeight + 1 : scaledHeight;
}
public static BraidWindow create(String title, int width, int height) {
var handleOut = new MutableLong();
withContext(0, () -> {
GLFW.glfwWindowHint(GLFW.GLFW_CLIENT_API, GLFW.GLFW_OPENGL_API);
GLFW.glfwWindowHint(GLFW.GLFW_CONTEXT_CREATION_API, GLFW.GLFW_NATIVE_CONTEXT_API);
GLFW.glfwWindowHint(GLFW.GLFW_CONTEXT_VERSION_MAJOR, 3);
GLFW.glfwWindowHint(GLFW.GLFW_CONTEXT_VERSION_MINOR, 2);
GLFW.glfwWindowHint(GLFW.GLFW_OPENGL_PROFILE, GLFW.GLFW_OPENGL_CORE_PROFILE);
GLFW.glfwWindowHint(GLFW.GLFW_OPENGL_FORWARD_COMPAT, GLFW.GLFW_TRUE);
var handle = GLFW.glfwCreateWindow(width, height, title, 0, Minecraft.getInstance().getWindow().handle());
if (handle == 0) {
throw new UnsupportedOperationException("Failed to create a GLFW window");
}
GLFW.glfwMakeContextCurrent(handle);
GLFW.glfwSwapInterval(0);
GlDebug.enableDebugCallback(Minecraft.getInstance().options.glDebugVerbosity, true, new HashSet<>());
handleOut.setValue(handle);
});
return new BraidWindow(handleOut.longValue());
}
public static OpenResult open(String title, int width, int height, Widget widget) {
var window = create(title, width, height);
var app = new AppState(
Owo.LOGGER,
AppState.formatName("BraidWindow", widget, title),
Minecraft.getInstance(),
window,
window.eventBinding,
widget
);
BraidWindowScheduler.add(window, app);
return new OpenResult(app, window);
}
// ---
@Override
public void dispose() {
GLFW.glfwDestroyWindow(this.handle);
this.cursorController.dispose();
this.guiRenderer.close();
this.remoteTarget.destroyBuffers();
for (var resource : this.resources) {
resource.free();
}
}
// ---
@Override
public int width() {
return this.scaledWidth;
}
@Override
public int height() {
return this.scaledHeight;
}
@Override
public double scaleFactor() {
return this.scaleFactor;
}
@Override
public EventSource onResize() {
return this.onResize.source();
}
@Override
public CursorStyle currentCursorStyle() {
return this.cursorController.currentStyle();
}
@Override
public void setCursorStyle(CursorStyle style) {
this.cursorController.setStyle(style);
}
// ---
@Override
public void beginRendering() {
RenderSystem.getDevice().createCommandEncoder().clearColorAndDepthTextures(
this.remoteTarget.getColorTexture(),
0xFF000000,
this.remoteTarget.getDepthTexture(),
1
);
}
@Override
public void endRendering() {
this.guiRenderer.render(new BraidGuiRenderer.Target(
this.remoteTarget,
this
));
// ---
withContext(this.handle, () -> {
GL32.glBindFramebuffer(GL32.GL_READ_FRAMEBUFFER, this.localFbo);
GL32.glBindFramebuffer(GL32.GL_DRAW_FRAMEBUFFER, 0);
GL32.glBlitFramebuffer(
0, 0, this.framebufferWidth, this.framebufferHeight,
0, 0, this.framebufferWidth, this.framebufferHeight,
GL32.GL_COLOR_BUFFER_BIT,
GL32.GL_NEAREST
);
GLFW.glfwSwapBuffers(this.handle);
});
}
// ---
private R storeNativeResource(R resource) {
this.resources.add(resource);
return resource;
}
public static void withContext(long contextHandle, Runnable fn) {
var activeContext = GLFW.glfwGetCurrentContext();
try {
GLFW.glfwMakeContextCurrent(contextHandle);
fn.run();
} finally {
GLFW.glfwMakeContextCurrent(activeContext);
}
}
// ---
public static class WindowEventBinding extends EventBinding {
public final BraidWindow window;
public WindowEventBinding(BraidWindow window) {
this.window = window;
}
@Override
public boolean isKeyPressed(int keyCode) {
return GLFW.glfwGetKey(this.window.handle, keyCode) == GLFW.GLFW_PRESS;
}
}
public record OpenResult(AppState state, BraidWindow window) {}
}
================================================
FILE: src/main/java/io/wispforest/owo/braid/core/BraidWindowScheduler.java
================================================
package io.wispforest.owo.braid.core;
import io.wispforest.owo.ui.event.ClientRenderCallback;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents;
import net.minecraft.client.Minecraft;
import java.util.ArrayList;
import java.util.List;
public class BraidWindowScheduler {
private static final List APPS = new ArrayList<>();
public static void add(BraidWindow window, AppState app) {
APPS.add(new App(window, app));
}
private static void frame() {
for (var app : new ArrayList<>(APPS)) {
if (!app.state().running()) {
app.state().dispose();
APPS.remove(app);
continue;
}
app.state().processEvents(
Minecraft.getInstance().getDeltaTracker().getGameTimeDeltaTicks()
);
app.state().draw(app.surface().guiRenderer.newGraphics(app.state().cursorPosition().x(), app.state().cursorPosition().y()));
}
}
static {
ClientRenderCallback.BEFORE_SWAP.register(client -> frame());
ClientLifecycleEvents.CLIENT_STOPPING.register(client -> {
APPS.forEach(app -> app.state().dispose());
APPS.clear();
});
}
}
record App(BraidWindow surface, AppState state) {}
================================================
FILE: src/main/java/io/wispforest/owo/braid/core/Color.java
================================================
package io.wispforest.owo.braid.core;
import net.minecraft.ChatFormatting;
import net.minecraft.util.Mth;
public class Color {
public static final Color RED = Color.values(1, 0, 0);
public static final Color YELLOW = Color.values(1, 1, 0);
public static final Color GREEN = Color.values(0, 1, 0);
public static final Color AQUA = Color.values(0, 1, 1);
public static final Color BLUE = Color.values(0, 0, 1);
public static final Color MAGENTA = Color.values(1, 0, 1);
public static final Color WHITE = Color.values(1, 1, 1);
public static final Color BLACK = Color.values(0, 0, 0);
//
public final double r, g, b, a;
private Color(double r, double g, double b, double a) {
this.r = r;
this.g = g;
this.b = b;
this.a = a;
}
// ---
public Color(int argb) {
this(
((argb >> 16) & 0xFF) / 255.0,
((argb >> 8) & 0xFF) / 255.0,
(argb & 0xFF) / 255.0,
(argb >>> 24) / 255.0
);
}
public static Color values(double r, double g, double b, double a) {
return new Color(r, g, b, a);
}
public static Color values(double r, double g, double b) {
return values(r, g, b, 1);
}
public static Color rgb(int rgb) {
return values(
((rgb >> 16) & 0xFF) / 255.0,
((rgb >> 8) & 0xFF) / 255.0,
(rgb & 0xFF) / 255.0
);
}
public static Color hsv(double hue, double saturation, double value, double alpha) {
// we call .5e-7f the magic "do not turn a hue value of 1f into yellow" constant
return new Color((int) (alpha * 255) << 24 | Mth.hsvToRgb((float) (hue - .5e-7f), (float) saturation, (float) value));
}
public static Color hsv(double hue, double saturation, double value) {
return hsv(hue, saturation, value, 1);
}
public static Color formatting(ChatFormatting formatting) {
var rgb = formatting.getColor();
return rgb(rgb != null ? rgb : 0);
}
public static Color mix(double t, Color a, Color b) {
return Color.values(
Mth.lerp(t, a.r, b.r),
Mth.lerp(t, a.g, b.g),
Mth.lerp(t, a.b, b.b),
Mth.lerp(t, a.a, b.a)
);
}
public static Color randomHue() {
return hsv(Math.random(), .75, 1);
}
// ---
public io.wispforest.owo.ui.core.Color toOwoUi() {
return new io.wispforest.owo.ui.core.Color(
(float) this.r, (float) this.g, (float) this.b, (float) this.a
);
}
public String toHexString(boolean includeAlpha) {
return includeAlpha
? String.format("#%08X", this.argb())
: String.format("#%06X", this.rgb());
}
//
public Color withR(double r) {
return new Color(r, this.g, this.b, this.a);
}
public Color withG(double g) {
return new Color(this.r, g, this.b, this.a);
}
public Color withB(double b) {
return new Color(this.r, this.g, b, this.a);
}
public Color withA(double a) {
return new Color(this.r, this.g, this.b, a);
}
// ---
public int rgb() {
return (int) (this.r * 255) << 16
| (int) (this.g * 255) << 8
| (int) (this.b * 255);
}
public int argb() {
return (int) (this.a * 255) << 24
| (int) (this.r * 255) << 16
| (int) (this.g * 255) << 8
| (int) (this.b * 255);
}
public float[] hsv() {
return this.toOwoUi().hsv();
}
@Override
public boolean equals(Object o) {
if (o == null || this.getClass() != o.getClass()) return false;
var other = (Color) o;
return this.r == other.r
&& this.g == other.g
&& this.b == other.b
&& this.a == other.a;
}
@Override
public int hashCode() {
int result = Double.hashCode(r);
result = 31 * result + Double.hashCode(g);
result = 31 * result + Double.hashCode(b);
result = 31 * result + Double.hashCode(a);
return result;
}
}
================================================
FILE: src/main/java/io/wispforest/owo/braid/core/CompoundListenable.java
================================================
package io.wispforest.owo.braid.core;
import java.util.ArrayList;
import java.util.List;
public class CompoundListenable extends Listenable {
protected final Runnable listener = this::notifyListeners;
protected final List children = new ArrayList<>();
public CompoundListenable(Listenable... initialChildren) {
for (var child : initialChildren) {
this.addChild(child);
}
}
public void addChild(Listenable child) {
this.children.add(child);
child.addListener(this.listener);
}
public void removeChild(Listenable child) {
this.children.remove(child);
child.removeListener(this.listener);
}
public void clear() {
for (var child : this.children) {
child.removeListener(this.listener);
}
this.children.clear();
}
}
================================================
FILE: src/main/java/io/wispforest/owo/braid/core/Constraints.java
================================================
package io.wispforest.owo.braid.core;
import net.minecraft.util.Mth;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Nullable;
public record Constraints(double minWidth, double minHeight, double maxWidth, double maxHeight) {
private static final Constraints UNCONSTRAINED = new Constraints(0, 0, Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY);
@ApiStatus.Internal
@Deprecated(forRemoval = true)
public Constraints {}
public static Constraints unconstrained() {
return UNCONSTRAINED;
}
public static Constraints of(double minWidth, double minHeight, double maxWidth, double maxHeight) {
return new Constraints(minWidth, minHeight, maxWidth, maxHeight);
}
public static Constraints ofMinWidth(double minWidth) {
return new Constraints(minWidth, 0, Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY);
}
public static Constraints ofMinHeight(double minHeight) {
return new Constraints(0, minHeight, Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY);
}
public static Constraints ofMaxWidth(double maxWidth) {
return new Constraints(0, 0, maxWidth, Double.POSITIVE_INFINITY);
}
public static Constraints ofMaxHeight(double maxHeight) {
return new Constraints(0, 0, Double.POSITIVE_INFINITY, maxHeight);
}
public static Constraints only(@Nullable Double minWidth, @Nullable Double minHeight, @Nullable Double maxWidth, @Nullable Double maxHeight) {
return new Constraints(
minWidth != null ? minWidth : 0,
minHeight != null ? minHeight : 0,
maxWidth != null ? maxWidth : Double.POSITIVE_INFINITY,
maxHeight != null ? maxHeight : Double.POSITIVE_INFINITY
);
}
public static Constraints tight(Size exactSize) {
return new Constraints(exactSize.width(), exactSize.height(), exactSize.width(), exactSize.height());
}
public static Constraints loose(Size maxSize) {
return new Constraints(0, 0, maxSize.width(), maxSize.height());
}
public static Constraints tightOnAxis(@Nullable Double horizontal, @Nullable Double vertical) {
return only(horizontal, vertical, horizontal, vertical);
}
// ---
public Constraints withMinWidth(double minWidth) {
return new Constraints(minWidth, this.minHeight, this.maxWidth, this.maxHeight);
}
public Constraints withMinHeight(double minHeight) {
return new Constraints(this.minWidth, minHeight, this.maxWidth, this.maxHeight);
}
public Constraints withMaxWidth(double maxWidth) {
return new Constraints(this.minWidth, this.minHeight, maxWidth, this.maxHeight);
}
public Constraints withMaxHeight(double maxHeight) {
return new Constraints(this.minWidth, this.minHeight, this.maxWidth, maxHeight);
}
// ---
public double minOnAxis(LayoutAxis axis) {
return switch (axis) {
case HORIZONTAL -> this.minWidth();
case VERTICAL -> this.minHeight();
};
}
public double maxOnAxis(LayoutAxis axis) {
return switch (axis) {
case HORIZONTAL -> this.maxWidth();
case VERTICAL -> this.maxHeight();
};
}
public double maxFiniteOrMinOnAxis(LayoutAxis axis) {
return switch (axis) {
case HORIZONTAL -> this.maxFiniteOrMinWidth();
case VERTICAL -> this.maxFiniteOrMinHeight();
};
}
public double maxFiniteOrMinWidth() {
return this.hasBoundedWidth() ? this.maxWidth() : this.minWidth();
}
public double maxFiniteOrMinHeight() {
return this.hasBoundedHeight() ? this.maxHeight() : this.minHeight();
}
// ---
public Constraints asLoose() {
return this.isLoose() ? this : new Constraints(0, 0, this.maxWidth, this.maxHeight);
}
public Constraints respecting(Constraints other) {
if (this.minWidth >= other.minWidth && this.minWidth <= other.maxWidth
&& this.maxWidth >= other.minWidth && this.maxWidth <= other.maxWidth
&& this.minHeight >= other.minHeight && this.minHeight <= other.maxHeight
&& this.maxHeight >= other.minHeight && this.maxHeight <= other.maxHeight) {
return this;
}
return new Constraints(
Mth.clamp(this.minWidth, other.minWidth, other.maxWidth),
Mth.clamp(this.minHeight, other.minHeight, other.maxHeight),
Mth.clamp(this.maxWidth, other.minWidth, other.maxWidth),
Mth.clamp(this.maxHeight, other.minHeight, other.maxHeight)
);
}
public boolean hasLooseWidth() {
return this.minWidth == 0;
}
public boolean hasLooseHeight() {
return this.minHeight == 0;
}
public boolean hasTightWidth() {
return this.minWidth == this.maxWidth;
}
public boolean hasTightHeight() {
return this.minHeight == this.maxHeight;
}
public boolean isLoose() {
return this.hasLooseWidth() && this.hasLooseHeight();
}
public boolean isTight() {
return this.hasTightWidth() && this.hasTightHeight();
}
public boolean hasBoundedWidth() {
return this.maxWidth < Double.POSITIVE_INFINITY;
}
public boolean hasBoundedHeight() {
return this.maxHeight < Double.POSITIVE_INFINITY;
}
public Size minSize() {
return Size.of(this.minWidth, this.minHeight);
}
public Size maxSize() {
return Size.of(this.maxWidth, this.maxHeight);
}
public Size maxFiniteOrMinSize() {
return Size.of(
this.maxFiniteOrMinWidth(),
this.maxFiniteOrMinHeight()
);
}
}
================================================
FILE: src/main/java/io/wispforest/owo/braid/core/EventBinding.java
================================================
package io.wispforest.owo.braid.core;
import com.mojang.blaze3d.platform.InputConstants;
import io.wispforest.owo.braid.core.events.UserEvent;
import net.minecraft.client.Minecraft;
import org.lwjgl.glfw.GLFW;
import java.util.ArrayList;
import java.util.List;
public abstract class EventBinding {
private final List bufferedEvents = new ArrayList<>();
public EventSlot add(UserEvent event) {
var slot = new EventSlot(event);
this.bufferedEvents.add(slot);
return slot;
}
List poll() {
var events = new ArrayList<>(this.bufferedEvents);
this.bufferedEvents.clear();
return events;
}
public abstract boolean isKeyPressed(int keyCode);
public KeyModifiers activeModifiers() {
return new KeyModifiers(
(this.isKeyPressed(GLFW.GLFW_KEY_LEFT_SHIFT) || this.isKeyPressed(GLFW.GLFW_KEY_RIGHT_SHIFT) ? GLFW.GLFW_MOD_SHIFT : 0)
| (this.isKeyPressed(GLFW.GLFW_KEY_LEFT_CONTROL) || this.isKeyPressed(GLFW.GLFW_KEY_RIGHT_CONTROL) ? GLFW.GLFW_MOD_CONTROL : 0)
| (this.isKeyPressed(GLFW.GLFW_KEY_LEFT_ALT) || this.isKeyPressed(GLFW.GLFW_KEY_RIGHT_ALT) ? GLFW.GLFW_MOD_ALT : 0)
| (this.isKeyPressed(GLFW.GLFW_KEY_RIGHT_SUPER) || this.isKeyPressed(GLFW.GLFW_KEY_RIGHT_SUPER) ? GLFW.GLFW_MOD_SUPER : 0)
| (this.isKeyPressed(GLFW.GLFW_KEY_NUM_LOCK) ? GLFW.GLFW_MOD_NUM_LOCK : 0)
| (this.isKeyPressed(GLFW.GLFW_KEY_CAPS_LOCK) ? GLFW.GLFW_MOD_CAPS_LOCK : 0)
);
}
public static class EventSlot {
final UserEvent event;
private boolean handled = false;
public EventSlot(UserEvent event) {
this.event = event;
}
public boolean handled() {
return this.handled;
}
void markHandled() {
this.handled = true;
}
}
// ---
public static class Headless extends EventBinding {
@Override
public boolean isKeyPressed(int keyCode) {
return false;
}
}
public static class Default extends EventBinding {
@Override
public boolean isKeyPressed(int keyCode) {
return InputConstants.isKeyDown(Minecraft.getInstance().getWindow(), keyCode);
}
}
}
================================================
FILE: src/main/java/io/wispforest/owo/braid/core/Insets.java
================================================
package io.wispforest.owo.braid.core;
import org.jetbrains.annotations.ApiStatus;
public record Insets(double top, double bottom, double left, double right) {
private static final Insets NONE = new Insets(0, 0, 0, 0);
@ApiStatus.Internal
@Deprecated(forRemoval = true)
public Insets {}
// ---
public static Insets of(double top, double bottom, double left, double right) {
return new Insets(top, bottom, left, right);
}
public static Insets all(double inset) {
return new Insets(inset, inset, inset, inset);
}
public static Insets both(double horizontal, double vertical) {
return new Insets(vertical, vertical, horizontal, horizontal);
}
public static Insets top(double top) {
return new Insets(top, 0, 0, 0);
}
public static Insets bottom(double bottom) {
return new Insets(0, bottom, 0, 0);
}
public static Insets left(double left) {
return new Insets(0, 0, left, 0);
}
public static Insets right(double right) {
return new Insets(0, 0, 0, right);
}
public static Insets vertical(double inset) {
return new Insets(inset, inset, 0, 0);
}
public static Insets horizontal(double inset) {
return new Insets(0, 0, inset, inset);
}
public static Insets none() {
return NONE;
}
// ---
public Insets withTop(double top) {
return new Insets(top, this.bottom, this.left, this.right);
}
public Insets withBottom(double bottom) {
return new Insets(this.top, bottom, this.left, this.right);
}
public Insets withLeft(double left) {
return new Insets(this.top, this.bottom, left, this.right);
}
public Insets withRight(double right) {
return new Insets(this.top, this.bottom, this.left, right);
}
public double horizontal() {
return this.left + this.right;
}
public double vertical() {
return this.top + this.bottom;
}
}
================================================
FILE: src/main/java/io/wispforest/owo/braid/core/KeyModifiers.java
================================================
package io.wispforest.owo.braid.core;
import it.unimi.dsi.fastutil.ints.IntList;
import static org.lwjgl.glfw.GLFW.*;
public record KeyModifiers(int bitMask) {
public static final KeyModifiers NONE = new KeyModifiers(0);
public boolean shift() {
return (this.bitMask & GLFW_MOD_SHIFT) != 0;
}
public boolean ctrl() {
return (this.bitMask & GLFW_MOD_CONTROL) != 0;
}
public boolean alt() {
return (this.bitMask & GLFW_MOD_ALT) != 0;
}
public boolean meta() {
return (this.bitMask & GLFW_MOD_SUPER) != 0;
}
public boolean capsLock() {
return (this.bitMask & GLFW_MOD_CAPS_LOCK) != 0;
}
public boolean numLock() {
return (this.bitMask & GLFW_MOD_NUM_LOCK) != 0;
}
public static boolean isModifier(int keyCode) {
return MODIFIER_KEYS.contains(keyCode);
}
public static KeyModifiers both(KeyModifiers a, KeyModifiers b) {
return new KeyModifiers(a.bitMask | b.bitMask);
}
public static final IntList MODIFIER_KEYS = IntList.of(
GLFW_KEY_LEFT_SHIFT,
GLFW_KEY_RIGHT_SHIFT,
GLFW_KEY_LEFT_CONTROL,
GLFW_KEY_RIGHT_CONTROL,
GLFW_KEY_LEFT_ALT,
GLFW_KEY_RIGHT_ALT,
GLFW_KEY_LEFT_SUPER,
GLFW_KEY_RIGHT_SUPER
);
}
================================================
FILE: src/main/java/io/wispforest/owo/braid/core/LayoutAxis.java
================================================
package io.wispforest.owo.braid.core;
import java.util.function.Supplier;
public enum LayoutAxis {
HORIZONTAL,
VERTICAL;
public T choose(T horizontal, T vertical) {
return switch (this) {
case HORIZONTAL -> horizontal;
case VERTICAL -> vertical;
};
}
public T chooseCompute(Supplier horizontal, Supplier vertical) {
return switch (this) {
case HORIZONTAL -> horizontal.get();
case VERTICAL -> vertical.get();
};
}
public Size createSize(double extent, double crossExtent) {
return switch (this) {
case HORIZONTAL -> Size.of(extent, crossExtent);
case VERTICAL -> Size.of(crossExtent, extent);
};
}
public LayoutAxis opposite() {
return switch (this) {
case HORIZONTAL -> VERTICAL;
case VERTICAL -> HORIZONTAL;
};
}
}
================================================
FILE: src/main/java/io/wispforest/owo/braid/core/Listenable.java
================================================
package io.wispforest.owo.braid.core;
import java.util.ArrayList;
import java.util.List;
public abstract class Listenable {
protected final List listeners = new ArrayList<>();
public void addListener(Runnable listener) {
this.listeners.add(listener);
}
public void removeListener(Runnable listener) {
this.listeners.remove(listener);
}
protected void notifyListeners() {
this.listeners.forEach(Runnable::run);
}
}
================================================
FILE: src/main/java/io/wispforest/owo/braid/core/ListenableValue.java
================================================
package io.wispforest.owo.braid.core;
public class ListenableValue extends Listenable {
private V value;
public ListenableValue(V value) {
this.value = value;
}
public V value() {
return this.value;
}
public void setValue(V value) {
this.value = value;
this.notifyListeners();
}
}
================================================
FILE: src/main/java/io/wispforest/owo/braid/core/RelativePosition.java
================================================
package io.wispforest.owo.braid.core;
import com.google.common.base.Preconditions;
import io.wispforest.owo.Owo;
import io.wispforest.owo.braid.framework.BuildContext;
import org.joml.Vector2d;
import org.joml.Vector2f;
public record RelativePosition(BuildContext context, double x, double y) {
public Vector2d convertTo(BuildContext ancestor) {
var contextInstance = context.instance();
var ancestorInstance = ancestor.instance();
if (Owo.DEBUG) {
Preconditions.checkArgument(
contextInstance.ancestors().contains(ancestorInstance),
"a RelativePosition can only be converted to the coordinate system of an ancestor"
);
}
var coordinates = new Vector2f((float) this.x, (float) this.y);
contextInstance.computeTransformFrom(ancestorInstance).invert().transformPosition(coordinates);
return new Vector2d(coordinates.x, coordinates.y);
}
}
================================================
FILE: src/main/java/io/wispforest/owo/braid/core/Size.java
================================================
package io.wispforest.owo.braid.core;
import net.minecraft.util.Mth;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Nullable;
public record Size(double width, double height) {
private static final Size ZERO = new Size(0, 0);
@ApiStatus.Internal
@Deprecated(forRemoval = true)
public Size {}
// ---
public static Size zero() {
return ZERO;
}
public static Size of(double width, double height) {
return new Size(width, height);
}
public static Size square(double sideLength) {
return new Size(sideLength, sideLength);
}
public static Size max(Size a, Size b) {
return new Size(Math.max(a.width, b.width), Math.max(a.height, b.height));
}
// ---
public Size withInsets(Insets insets) {
return new Size(this.width + insets.horizontal(), this.height + insets.vertical());
}
public Size with(@Nullable Double width, @Nullable Double height) {
return new Size(width != null ? width : this.width, height != null ? height : this.height);
}
public Size floor() {
return new Size(Math.floor(this.width), Math.floor(this.height));
}
public Size ceil() {
return new Size(Math.ceil(this.width), Math.ceil(this.height));
}
public double getExtent(LayoutAxis axis) {
return switch (axis) {
case HORIZONTAL -> width();
case VERTICAL -> height();
};
}
public Size constrained(Constraints constraints) {
return new Size(
Mth.clamp(this.width, constraints.minWidth(), constraints.maxWidth()),
Mth.clamp(this.height, constraints.minHeight(), constraints.maxHeight())
);
}
}
================================================
FILE: src/main/java/io/wispforest/owo/braid/core/Surface.java
================================================
package io.wispforest.owo.braid.core;
import com.mojang.blaze3d.platform.Window;
import io.wispforest.owo.braid.core.cursor.CursorController;
import io.wispforest.owo.braid.core.cursor.CursorStyle;
import io.wispforest.owo.ui.event.WindowResizeCallback;
import io.wispforest.owo.util.EventSource;
import io.wispforest.owo.util.EventStream;
import net.minecraft.client.Minecraft;
public interface Surface {
int width();
int height();
double scaleFactor();
EventSource onResize();
CursorStyle currentCursorStyle();
void setCursorStyle(CursorStyle style);
void beginRendering();
void endRendering();
void dispose();
class Default implements Surface {
private static EventStream resizeEvents;
private final Window window;
private final CursorController cursorController;
public Default() {
this.window = Minecraft.getInstance().getWindow();
this.cursorController = new CursorController(this.window.handle());
if (resizeEvents == null) {
resizeEvents = ResizeCallback.newStream();
WindowResizeCallback.EVENT.register((client, resizedWindow) -> {
resizeEvents.sink().onResize(resizedWindow.getGuiScaledWidth(), resizedWindow.getGuiScaledHeight());
});
}
}
@Override
public int width() {
return this.window.getGuiScaledWidth();
}
@Override
public int height() {
return this.window.getGuiScaledHeight();
}
@Override
public double scaleFactor() {
return this.window.getGuiScale();
}
@Override
public EventSource onResize() {
return resizeEvents.source();
}
@Override
public CursorStyle currentCursorStyle() {
return this.cursorController.currentStyle();
}
@Override
public void setCursorStyle(CursorStyle style) {
this.cursorController.setStyle(style);
}
@Override
public void beginRendering() {}
@Override
public void endRendering() {}
@Override
public void dispose() {
this.cursorController.dispose();
}
}
interface ResizeCallback {
void onResize(int newWidth, int newHeight);
static EventStream newStream() {
return new EventStream<>(callbacks -> (newWidth, newHeight) -> {
for (var callback : callbacks) {
callback.onResize(newWidth, newHeight);
}
});
}
}
}
================================================
FILE: src/main/java/io/wispforest/owo/braid/core/TextLayout.java
================================================
package io.wispforest.owo.braid.core;
import net.minecraft.client.gui.Font;
import net.minecraft.network.chat.Component;
import net.minecraft.network.chat.Style;
import java.util.ArrayList;
import java.util.List;
public class TextLayout {
public static EditMetrics measure(Font font, String text, Style baseStyle, int maxWidth) {
var lines = new ArrayList();
font.getSplitter().splitLines(
text,
maxWidth,
baseStyle,
false,
(style, start, end) -> lines.add(new Line(style, start, end))
);
if (text.endsWith("\n")) {
lines.add(new Line(baseStyle, text.length(), text.length()));
}
if (lines.isEmpty()) {
lines.add(new Line(baseStyle, 0, 0));
}
// ---
var textWidth = 0;
var textHeight = 0;
var lineMetrics = new ArrayList();
for (var line : lines) {
var lineWidth = font.width(line.substring(text));
lineMetrics.add(new LineMetrics(line.beginIdx, line.endIdx, lineWidth));
textWidth = Math.max(textWidth, lineWidth);
textHeight += font.lineHeight;
}
return new EditMetrics(textWidth, textHeight, lineMetrics);
}
public record LineMetrics(int beginIdx, int endIdx, double width) {
public String substring(String fullContent) {
return fullContent.substring(this.beginIdx, this.endIdx);
}
}
public record EditMetrics(int width, int height, List lineMetrics) {}
private record Line(Style style, int beginIdx, int endIdx) {
public Component substring(String fullContent) {
return Component.literal(fullContent.substring(this.beginIdx, this.endIdx)).setStyle(this.style);
}
}
}
================================================
FILE: src/main/java/io/wispforest/owo/braid/core/TextureSurface.java
================================================
package io.wispforest.owo.braid.core;
import com.mojang.blaze3d.pipeline.TextureTarget;
import com.mojang.blaze3d.systems.RenderSystem;
import com.mojang.blaze3d.textures.FilterMode;
import com.mojang.blaze3d.textures.GpuTextureView;
import io.wispforest.owo.Owo;
import io.wispforest.owo.braid.core.cursor.CursorStyle;
import io.wispforest.owo.braid.util.BraidGuiRenderer;
import io.wispforest.owo.util.EventSource;
import io.wispforest.owo.util.EventStream;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.texture.AbstractTexture;
import net.minecraft.resources.Identifier;
import java.util.UUID;
public class TextureSurface implements Surface {
private final TextureTarget target;
private final EventStream resizeEvents = ResizeCallback.newStream();
public final TextureSurfaceTexture registeredTexture;
public final Identifier registeredTextureId;
private CursorStyle currentCursorStyle = CursorStyle.NONE;
public final BraidGuiRenderer guiRenderer;
public TextureSurface(int width, int height) {
this.target = new TextureTarget("texture surface", width, height, true);
this.guiRenderer = new BraidGuiRenderer(Minecraft.getInstance());
this.registeredTexture = new TextureSurfaceTexture();
this.registeredTextureId = Owo.id("texture_surface_" + UUID.randomUUID());
Minecraft.getInstance().getTextureManager().register(this.registeredTextureId, this.registeredTexture);
}
public void resize(int width, int height) {
this.target.resize(width, height);
this.resizeEvents.sink().onResize(width, height);
this.registeredTexture.sync();
}
public GpuTextureView texture() {
return this.target.getColorTextureView();
}
@Override
public int width() {
return this.target.width;
}
@Override
public int height() {
return this.target.height;
}
@Override
public double scaleFactor() {
return 1;
}
@Override
public EventSource onResize() {
return this.resizeEvents.source();
}
@Override
public CursorStyle currentCursorStyle() {
return this.currentCursorStyle;
}
@Override
public void setCursorStyle(CursorStyle style) {
this.currentCursorStyle = style;
}
// ---
@Override
public void beginRendering() {
RenderSystem.getDevice().createCommandEncoder().clearColorAndDepthTextures(
this.target.getColorTexture(),
0x00000000,
this.target.getDepthTexture(),
1
);
}
@Override
public void endRendering() {
this.guiRenderer.render(new BraidGuiRenderer.Target(
this.target,
this
));
}
@Override
public void dispose() {
this.target.destroyBuffers();
Minecraft.getInstance().getTextureManager().release(this.registeredTextureId);
}
// ---
public class TextureSurfaceTexture extends AbstractTexture {
public TextureSurfaceTexture() {
this.sync();
this.sampler = RenderSystem.getSamplerCache().getClampToEdge(FilterMode.NEAREST);
}
private void sync() {
this.texture = TextureSurface.this.target.getColorTexture();
this.textureView = TextureSurface.this.target.getColorTextureView();
}
@Override
public void close() {}
}
}
================================================
FILE: src/main/java/io/wispforest/owo/braid/core/cursor/CursorController.java
================================================
package io.wispforest.owo.braid.core.cursor;
import org.lwjgl.glfw.GLFW;
import java.util.HashMap;
import java.util.Map;
public class CursorController {
private final Map cursors = new HashMap<>();
private final long windowHandle;
private CursorStyle lastCursorStyle = CursorStyle.NONE;
private boolean disposed = false;
public CursorController(long windowHandle) {
this.windowHandle = windowHandle;
}
public CursorStyle currentStyle() {
return this.lastCursorStyle;
}
public void setStyle(CursorStyle style) {
if (this.disposed || this.lastCursorStyle == style) return;
if (style == CursorStyle.NONE) {
GLFW.glfwSetCursor(this.windowHandle, 0);
} else {
if (!this.cursors.containsKey(style)) {
this.cursors.put(style, style.allocate());
}
GLFW.glfwSetCursor(this.windowHandle, this.cursors.get(style));
}
this.lastCursorStyle = style;
}
public void dispose() {
if (this.disposed) return;
for (var ptr : this.cursors.values()) {
if (ptr == 0) return;
GLFW.glfwDestroyCursor(ptr);
}
this.disposed = true;
}
}
================================================
FILE: src/main/java/io/wispforest/owo/braid/core/cursor/CursorStyle.java
================================================
package io.wispforest.owo.braid.core.cursor;
import io.wispforest.owo.braid.core.LayoutAxis;
import net.minecraft.util.Mth;
import org.joml.Matrix3x2f;
import org.lwjgl.glfw.GLFW;
public sealed interface CursorStyle permits SystemCursorStyle {
CursorStyle NONE = new SystemCursorStyle(0);
CursorStyle POINTER = new SystemCursorStyle(GLFW.GLFW_ARROW_CURSOR);
CursorStyle TEXT = new SystemCursorStyle(GLFW.GLFW_IBEAM_CURSOR);
CursorStyle HAND = new SystemCursorStyle(GLFW.GLFW_HAND_CURSOR);
CursorStyle MOVE = new SystemCursorStyle(GLFW.GLFW_RESIZE_ALL_CURSOR);
CursorStyle CROSSHAIR = new SystemCursorStyle(GLFW.GLFW_CROSSHAIR_CURSOR);
CursorStyle HORIZONTAL_RESIZE = new SystemCursorStyle(GLFW.GLFW_HRESIZE_CURSOR);
CursorStyle VERTICAL_RESIZE = new SystemCursorStyle(GLFW.GLFW_VRESIZE_CURSOR);
CursorStyle NWSE_RESIZE = new SystemCursorStyle(GLFW.GLFW_RESIZE_NWSE_CURSOR);
CursorStyle NESW_RESIZE = new SystemCursorStyle(GLFW.GLFW_RESIZE_NESW_CURSOR);
CursorStyle NOT_ALLOWED = new SystemCursorStyle(GLFW.GLFW_NOT_ALLOWED_CURSOR);
long allocate();
static CursorStyle forDraggingAlong(LayoutAxis axis, Matrix3x2f transform3x2) {
// Extract the Z rotation from the transform
var rotation = Math.atan2(transform3x2.m01, transform3x2.m11);
// Convert to degrees
rotation = Math.toDegrees(rotation);
// apply axis adjustment
if (axis == LayoutAxis.VERTICAL) rotation += 90;
// Normalize to [0, 180) (because the cursors are symmetric)
rotation = Mth.positiveModulo(rotation, 180);
// Map to [0, 8)
rotation /= 22.5;
if (rotation < 1 || rotation >= 7) return HORIZONTAL_RESIZE;
else if (rotation >= 3 && rotation < 5) return VERTICAL_RESIZE;
else if (rotation >= 1 && rotation < 3) return NESW_RESIZE;
else return NWSE_RESIZE;
}
}
================================================
FILE: src/main/java/io/wispforest/owo/braid/core/cursor/SystemCursorStyle.java
================================================
package io.wispforest.owo.braid.core.cursor;
import org.lwjgl.glfw.GLFW;
public final class SystemCursorStyle implements CursorStyle {
public final int glfwId;
SystemCursorStyle(int glfwId) {
this.glfwId = glfwId;
}
@Override
public long allocate() {
return GLFW.glfwCreateStandardCursor(this.glfwId);
}
}
================================================
FILE: src/main/java/io/wispforest/owo/braid/core/element/BraidBlockElement.java
================================================
package io.wispforest.owo.braid.core.element;
import com.mojang.blaze3d.platform.Lighting;
import com.mojang.blaze3d.vertex.PoseStack;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.navigation.ScreenRectangle;
import net.minecraft.client.gui.render.pip.PictureInPictureRenderer;
import net.minecraft.client.gui.render.state.pip.PictureInPictureRenderState;
import net.minecraft.client.renderer.LightTexture;
import net.minecraft.client.renderer.MultiBufferSource;
import net.minecraft.client.renderer.blockentity.state.BlockEntityRenderState;
import net.minecraft.client.renderer.state.CameraRenderState;
import net.minecraft.client.renderer.texture.OverlayTexture;
import net.minecraft.world.level.block.RenderShape;
import net.minecraft.world.level.block.state.BlockState;
import org.jetbrains.annotations.Nullable;
import org.joml.Matrix3x2f;
import org.joml.Matrix4f;
public record BraidBlockElement(
BlockState block,
@Nullable BlockEntityRenderState entity,
Matrix4f transform,
Matrix3x2f pose,
double width,
double height,
ScreenRectangle scissorArea
) implements PictureInPictureRenderState {
@Override
public int x0() {
return 0;
}
@Override
public int x1() {
return (int) this.width;
}
@Override
public int y0() {
return 0;
}
@Override
public int y1() {
return (int) this.height;
}
@Override
public float scale() {
return 1;
}
@Override
public Matrix3x2f pose() {
return this.pose;
}
@Override
public @Nullable ScreenRectangle scissorArea() {
return this.scissorArea;
}
@Override
public @Nullable ScreenRectangle bounds() {
var bounds = new ScreenRectangle(0, 0, (int) this.width, (int) this.height).transformMaxBounds(this.pose);
return this.scissorArea != null
? this.scissorArea.intersection(bounds)
: bounds;
}
public static class Renderer extends PictureInPictureRenderer {
public Renderer(MultiBufferSource.BufferSource vertexConsumers) {
super(vertexConsumers);
}
@Override
public Class getRenderStateClass() {
return BraidBlockElement.class;
}
@Override
@SuppressWarnings("NonAsciiCharacters")
protected void renderToTexture(BraidBlockElement state, PoseStack matrices) {
Minecraft.getInstance().gameRenderer.getLighting().setupFor(Lighting.Entry.ENTITY_IN_UI);
matrices.mulPose(state.transform);
if (state.block.getRenderShape() != RenderShape.INVISIBLE) {
Minecraft.getInstance().getBlockRenderer().renderSingleBlock(
state.block, matrices, bufferSource,
LightTexture.FULL_BRIGHT, OverlayTexture.NO_OVERLAY
);
}
if (state.entity != null) {
var медведь = Minecraft.getInstance().getBlockEntityRenderDispatcher().getRenderer(state.entity);
if (медведь != null) {
var dispatcher = Minecraft.getInstance().gameRenderer.getFeatureRenderDispatcher();
медведь.submit(state.entity, matrices, dispatcher.getSubmitNodeStorage(), new CameraRenderState());
dispatcher.renderAllFeatures();
}
}
}
@Override
protected float getTranslateY(int height, int windowScaleFactor) {
return height / 2f;
}
@Override
protected String getTextureLabel() {
return "owo-ui_block";
}
}
}
================================================
FILE: src/main/java/io/wispforest/owo/braid/core/element/BraidDashedLineElement.java
================================================
package io.wispforest.owo.braid.core.element;
import com.mojang.blaze3d.pipeline.RenderPipeline;
import com.mojang.blaze3d.vertex.VertexConsumer;
import io.wispforest.owo.braid.core.Color;
import net.minecraft.client.gui.navigation.ScreenRectangle;
import net.minecraft.client.gui.render.TextureSetup;
import net.minecraft.client.gui.render.state.GuiElementRenderState;
import org.joml.Matrix3x2f;
import org.joml.Vector2d;
public record BraidDashedLineElement(
Color color,
double thiccness,
double segmentLength,
RenderPipeline pipeline,
Matrix3x2f pose,
ScreenRectangle bounds,
ScreenRectangle scissorArea
) implements GuiElementRenderState {
@Override
public void buildVertices(VertexConsumer buffer) {
var colorArgb = this.color.argb();
var begin = new Vector2d(this.bounds.left(), this.bounds.top());
var end = new Vector2d(this.bounds.right(), this.bounds.bottom());
var step = end.sub(begin, new Vector2d()).normalize().mul(this.segmentLength);
var segmentCount = (int) ((end.distance(begin) + this.segmentLength) / (this.segmentLength * 2));
var offset = end.sub(begin, new Vector2d()).perpendicular().normalize().mul(this.thiccness * .5d);
end.set(begin).add(step);
step.mul(2);
for (var i = 0; i < segmentCount; i++) {
buffer.addVertexWith2DPose(this.pose, (float) (begin.x + offset.x), (float) (begin.y + offset.y)).setColor(colorArgb);
buffer.addVertexWith2DPose(this.pose, (float) (begin.x - offset.x), (float) (begin.y - offset.y)).setColor(colorArgb);
buffer.addVertexWith2DPose(this.pose, (float) (end.x - offset.x), (float) (end.y - offset.y)).setColor(colorArgb);
buffer.addVertexWith2DPose(this.pose, (float) (end.x + offset.x), (float) (end.y + offset.y)).setColor(colorArgb);
begin.add(step);
end.add(step);
}
}
@Override
public RenderPipeline pipeline() {
return this.pipeline;
}
@Override
public TextureSetup textureSetup() {
return TextureSetup.noTexture();
}
@Override
public ScreenRectangle scissorArea() {
return this.scissorArea;
}
@Override
public ScreenRectangle bounds() {
var bounds = this.bounds.transformMaxBounds(this.pose);
return this.scissorArea != null ? this.scissorArea.intersection(bounds) : bounds;
}
}
================================================
FILE: src/main/java/io/wispforest/owo/braid/core/element/BraidEntityElement.java
================================================
package io.wispforest.owo.braid.core.element;
import com.mojang.blaze3d.platform.Lighting;
import com.mojang.blaze3d.vertex.PoseStack;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.navigation.ScreenRectangle;
import net.minecraft.client.gui.render.pip.PictureInPictureRenderer;
import net.minecraft.client.gui.render.state.pip.PictureInPictureRenderState;
import net.minecraft.client.renderer.MultiBufferSource;
import net.minecraft.client.renderer.entity.EntityRenderDispatcher;
import net.minecraft.client.renderer.entity.state.EntityRenderState;
import net.minecraft.client.renderer.state.CameraRenderState;
import org.jetbrains.annotations.Nullable;
import org.joml.Matrix3x2f;
import org.joml.Matrix4f;
import org.joml.Quaternionf;
public record BraidEntityElement(
EntityRenderState entityState,
Matrix4f transform,
Matrix3x2f pose,
double width,
double height,
ScreenRectangle scissorArea
) implements PictureInPictureRenderState {
@Override
public int x0() {
return 0;
}
@Override
public int x1() {
return (int) this.width;
}
@Override
public int y0() {
return 0;
}
@Override
public int y1() {
return (int) this.height;
}
@Override
public float scale() {
return 1;
}
@Override
public Matrix3x2f pose() {
return this.pose;
}
@Override
public @Nullable ScreenRectangle scissorArea() {
return this.scissorArea;
}
@Override
public @Nullable ScreenRectangle bounds() {
var bounds = new ScreenRectangle(0, 0, (int) this.width, (int) this.height).transformMaxBounds(this.pose);
return this.scissorArea != null
? this.scissorArea.intersection(bounds)
: bounds;
}
public static class Renderer extends PictureInPictureRenderer {
private final EntityRenderDispatcher renderManager = Minecraft.getInstance().getEntityRenderDispatcher();
public Renderer(MultiBufferSource.BufferSource vertexConsumers) {
super(vertexConsumers);
}
@Override
public Class getRenderStateClass() {
return BraidEntityElement.class;
}
@Override
protected void renderToTexture(BraidEntityElement state, PoseStack matrices) {
Minecraft.getInstance().gameRenderer.getLighting().setupFor(Lighting.Entry.ENTITY_IN_UI);
matrices.mulPose(state.transform);
var camera = new CameraRenderState();
camera.orientation = state.transform.invert().getUnnormalizedRotation(new Quaternionf());
var dispatcher = Minecraft.getInstance().gameRenderer.getFeatureRenderDispatcher();
this.renderManager.submit(state.entityState, camera, 0, 0, 0, matrices, dispatcher.getSubmitNodeStorage());
dispatcher.renderAllFeatures();
}
@Override
protected float getTranslateY(int height, int windowScaleFactor) {
return 0;
}
@Override
protected String getTextureLabel() {
return "owo-entity";
}
}
}
================================================
FILE: src/main/java/io/wispforest/owo/braid/core/element/BraidItemElement.java
================================================
package io.wispforest.owo.braid.core.element;
import com.mojang.blaze3d.platform.Lighting;
import com.mojang.blaze3d.vertex.PoseStack;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.navigation.ScreenRectangle;
import net.minecraft.client.gui.render.pip.PictureInPictureRenderer;
import net.minecraft.client.gui.render.state.pip.PictureInPictureRenderState;
import net.minecraft.client.renderer.LightTexture;
import net.minecraft.client.renderer.MultiBufferSource;
import net.minecraft.client.renderer.item.ItemStackRenderState;
import net.minecraft.client.renderer.texture.OverlayTexture;
import org.jetbrains.annotations.Nullable;
import org.joml.Matrix3x2f;
import org.joml.Matrix4fc;
public record BraidItemElement(
ItemStackRenderState item,
double width,
double height,
ScreenRectangle scissorArea,
Matrix4fc transform,
Matrix3x2f pose
) implements PictureInPictureRenderState {
@Override
public int x0() {
return 0;
}
@Override
public int x1() {
return (int) this.width;
}
@Override
public int y0() {
return 0;
}
@Override
public int y1() {
return (int) this.height;
}
@Override
public float scale() {
return 1;
}
@Override
public Matrix3x2f pose() {
return this.pose;
}
@Override
public @Nullable ScreenRectangle scissorArea() {
return this.scissorArea;
}
@Override
public @Nullable ScreenRectangle bounds() {
var bounds = new ScreenRectangle(0, 0, (int) this.width, (int) this.height).transformMaxBounds(this.pose);
return this.scissorArea != null
? this.scissorArea.intersection(bounds)
: bounds;
}
public static class Renderer extends PictureInPictureRenderer {
public Renderer(MultiBufferSource.BufferSource vertexConsumers) {
super(vertexConsumers);
}
@Override
public Class getRenderStateClass() {
return BraidItemElement.class;
}
@Override
protected void renderToTexture(BraidItemElement state, PoseStack matrices) {
matrices.scale((float) state.width, (float) -state.height, (float) -Math.min(state.width, state.height));
matrices.mulPose(state.transform);
var notSideLit = !state.item.usesBlockLight();
if (notSideLit) {
Minecraft.getInstance().gameRenderer.getLighting().setupFor(Lighting.Entry.ITEMS_FLAT);
} else {
Minecraft.getInstance().gameRenderer.getLighting().setupFor(Lighting.Entry.ITEMS_3D);
}
var dispatcher = Minecraft.getInstance().gameRenderer.getFeatureRenderDispatcher();
state.item.submit(matrices, dispatcher.getSubmitNodeStorage(), LightTexture.FULL_BRIGHT, OverlayTexture.NO_OVERLAY, 0);
dispatcher.renderAllFeatures();
}
@Override
protected float getTranslateY(int height, int windowScaleFactor) {
return height / 2f;
}
@Override
protected String getTextureLabel() {
return "owo-item";
}
}
}
================================================
FILE: src/main/java/io/wispforest/owo/braid/core/events/CharInputEvent.java
================================================
package io.wispforest.owo.braid.core.events;
import io.wispforest.owo.braid.core.KeyModifiers;
public record CharInputEvent(char codepoint, KeyModifiers modifiers) implements UserEvent {
public CharInputEvent(char codepoint, int modifiers) {
this(codepoint, new KeyModifiers(modifiers));
}
}
================================================
FILE: src/main/java/io/wispforest/owo/braid/core/events/CloseEvent.java
================================================
package io.wispforest.owo.braid.core.events;
public enum CloseEvent implements UserEvent {
INSTANCE;
}
================================================
FILE: src/main/java/io/wispforest/owo/braid/core/events/FilesDroppedEvent.java
================================================
package io.wispforest.owo.braid.core.events;
import java.nio.file.Path;
import java.util.List;
public record FilesDroppedEvent(List paths) implements UserEvent {}
================================================
FILE: src/main/java/io/wispforest/owo/braid/core/events/KeyPressEvent.java
================================================
package io.wispforest.owo.braid.core.events;
import io.wispforest.owo.braid.core.KeyModifiers;
public record KeyPressEvent(int keyCode, int scancode, KeyModifiers modifiers) implements UserEvent {
public KeyPressEvent(int keyCode, int scancode, int modifiers) {
this(keyCode, scancode, new KeyModifiers(modifiers));
}
}
================================================
FILE: src/main/java/io/wispforest/owo/braid/core/events/KeyReleaseEvent.java
================================================
package io.wispforest.owo.braid.core.events;
import io.wispforest.owo.braid.core.KeyModifiers;
public record KeyReleaseEvent(int keycode, int scancode, KeyModifiers modifiers) implements UserEvent {
public KeyReleaseEvent(int keycode, int scancode, int modifiers) {
this(keycode, scancode, new KeyModifiers(modifiers));
}
}
================================================
FILE: src/main/java/io/wispforest/owo/braid/core/events/MouseButtonPressEvent.java
================================================
package io.wispforest.owo.braid.core.events;
import io.wispforest.owo.braid.core.KeyModifiers;
public record MouseButtonPressEvent(int button, KeyModifiers modifiers) implements UserEvent {
public MouseButtonPressEvent(int button, int modifiers) {
this(button, new KeyModifiers(modifiers));
}
}
================================================
FILE: src/main/java/io/wispforest/owo/braid/core/events/MouseButtonReleaseEvent.java
================================================
package io.wispforest.owo.braid.core.events;
import io.wispforest.owo.braid.core.KeyModifiers;
public record MouseButtonReleaseEvent(int button, KeyModifiers modifiers) implements UserEvent {
public MouseButtonReleaseEvent(int button, int modifiers) {
this(button, new KeyModifiers(modifiers));
}
}
================================================
FILE: src/main/java/io/wispforest/owo/braid/core/events/MouseMoveEvent.java
================================================
package io.wispforest.owo.braid.core.events;
public record MouseMoveEvent(double x, double y) implements UserEvent {}
================================================
FILE: src/main/java/io/wispforest/owo/braid/core/events/MouseScrollEvent.java
================================================
package io.wispforest.owo.braid.core.events;
public record MouseScrollEvent(double xOffset, double yOffset) implements UserEvent {}
================================================
FILE: src/main/java/io/wispforest/owo/braid/core/events/UserEvent.java
================================================
package io.wispforest.owo.braid.core.events;
public sealed interface UserEvent permits
CloseEvent,
CharInputEvent,
FilesDroppedEvent,
KeyPressEvent,
KeyReleaseEvent,
MouseButtonPressEvent,
MouseButtonReleaseEvent,
MouseMoveEvent,
MouseScrollEvent {}
================================================
FILE: src/main/java/io/wispforest/owo/braid/display/BraidDisplay.java
================================================
package io.wispforest.owo.braid.display;
import com.mojang.blaze3d.pipeline.BlendFunction;
import com.mojang.blaze3d.pipeline.RenderPipeline;
import com.mojang.blaze3d.vertex.PoseStack;
import io.wispforest.owo.Owo;
import io.wispforest.owo.braid.core.AppState;
import io.wispforest.owo.braid.core.EventBinding;
import io.wispforest.owo.braid.core.TextureSurface;
import io.wispforest.owo.braid.framework.widget.Widget;
import io.wispforest.owo.mixin.braid.RenderTypeInvoker;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.RenderPipelines;
import net.minecraft.client.renderer.SubmitNodeCollector;
import net.minecraft.client.renderer.rendertype.RenderSetup;
import net.minecraft.client.renderer.rendertype.RenderType;
import org.jetbrains.annotations.ApiStatus;
import java.util.function.Function;
public class BraidDisplay {
public DisplayQuad quad;
public final AppState app;
public final TextureSurface surface;
@ApiStatus.Internal
public boolean primaryPressed = false;
@ApiStatus.Internal
public boolean secondaryPressed = false;
boolean renderAutomatically = false;
public BraidDisplay(DisplayQuad quad, int surfaceWidth, int surfaceHeight, Widget widget) {
this.quad = quad;
this.surface = new TextureSurface(surfaceWidth, surfaceHeight);
this.app = new AppState(
null,
AppState.formatName("BraidDisplay", widget),
Minecraft.getInstance(),
this.surface,
new EventBinding.Headless(),
widget
);
}
public BraidDisplay renderAutomatically() {
this.renderAutomatically = true;
return this;
}
public void updateAndDrawApp() {
var client = this.app.client();
this.app.processEvents(
client.getDeltaTracker().getGameTimeDeltaTicks()
);
this.app.draw(this.surface.guiRenderer.newGraphics(this.app.cursorPosition().x(), this.app.cursorPosition().y()));
}
public void render(PoseStack matrices, SubmitNodeCollector queue, int light) {
var layer = RENDER_TYPE.apply(this.surface);
queue.submitCustomGeometry(matrices, layer, (matricesEntry, buffer) -> {
var normal = this.quad.normal.toVector3f();
buffer.addVertex(matricesEntry, 0, 0, 0).setColor(1f, 1f, 1f, 1f).setUv(0, 1).setLight(light).setNormal(matricesEntry, normal);
buffer.addVertex(matricesEntry, this.quad.left.toVector3f()).setColor(1f, 1f, 1f, 1f).setUv(0, 0).setLight(light).setNormal(matricesEntry, normal);
buffer.addVertex(matricesEntry, this.quad.top.add(this.quad.left).toVector3f()).setColor(1f, 1f, 1f, 1f).setUv(1, 0).setLight(light).setNormal(matricesEntry, normal);
buffer.addVertex(matricesEntry, this.quad.top.toVector3f()).setColor(1f, 1f, 1f, 1f).setUv(1, 1).setLight(light).setNormal(matricesEntry, normal);
});
}
// ---
public static final RenderPipeline PIPELINE = RenderPipeline.builder(RenderPipelines.BLOCK_SNIPPET)
.withLocation(Owo.id("pipeline/braid_display"))
.withShaderDefine("ALPHA_CUTOUT", 0.1F)
.withCull(false)
.withBlend(BlendFunction.TRANSLUCENT)
.build();
private static final Function RENDER_TYPE = surface -> RenderTypeInvoker.owo$of(
Owo.id("braid_display").toString(),
RenderSetup.builder(PIPELINE)
.withTexture("Sampler0", surface.registeredTextureId)
.useLightmap()
.createRenderSetup()
);
}
================================================
FILE: src/main/java/io/wispforest/owo/braid/display/BraidDisplayBinding.java
================================================
package io.wispforest.owo.braid.display;
import com.mojang.blaze3d.vertex.PoseStack;
import io.wispforest.owo.braid.core.events.MouseMoveEvent;
import net.minecraft.client.renderer.LightTexture;
import net.minecraft.client.renderer.SubmitNodeCollector;
import net.minecraft.client.renderer.state.CameraRenderState;
import net.minecraft.world.phys.Vec3;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Nullable;
import org.joml.Vector2dc;
import java.util.ArrayList;
import java.util.List;
public class BraidDisplayBinding {
private static final List ACTIVE_DISPLAYS = new ArrayList<>();
// ---
public static void activate(BraidDisplay display) {
ACTIVE_DISPLAYS.add(display);
}
public static void deactivate(BraidDisplay display) {
ACTIVE_DISPLAYS.remove(display);
}
// ---
public static @Nullable DisplayHitResult targetDisplay;
@ApiStatus.Internal
public static @Nullable DisplayHitResult queryTargetDisplay(Vec3 rayOrigin, Vec3 rayDirection) {
DisplayHitResult closestResult = null;
double closestRayOffset = Double.POSITIVE_INFINITY;
for (var display : ACTIVE_DISPLAYS) {
var result = display.quad.hitTest(rayOrigin, rayDirection);
if (result == null || result.t() >= closestRayOffset) continue;
closestResult = new DisplayHitResult(display, result.point());
closestRayOffset = result.t();
}
return closestResult;
}
@ApiStatus.Internal
public static void onDisplayHit(DisplayHitResult targetDisplay) {
var app = targetDisplay.display.app;
var cursorX = targetDisplay.point.x() * app.surface.width();
var cursorY = targetDisplay.point.y() * app.surface.height();
app.eventBinding.add(new MouseMoveEvent(cursorX, cursorY));
}
@ApiStatus.Internal
public static void updateAndDrawDisplays() {
for (var display : ACTIVE_DISPLAYS) {
display.updateAndDrawApp();
}
}
@ApiStatus.Internal
public static void renderAutomaticDisplays(PoseStack matrices, CameraRenderState camera, SubmitNodeCollector nodeCollector) {
for (var display : ACTIVE_DISPLAYS) {
if (!display.renderAutomatically) continue;
matrices.pushPose();
matrices.translate(display.quad.pos.subtract(camera.pos));
display.render(matrices, nodeCollector, LightTexture.FULL_BRIGHT);
matrices.popPose();
}
}
// ---
public record DisplayHitResult(BraidDisplay display, Vector2dc point) {}
}
================================================
FILE: src/main/java/io/wispforest/owo/braid/display/DisplayQuad.java
================================================
package io.wispforest.owo.braid.display;
import net.minecraft.world.phys.Vec3;
import org.jetbrains.annotations.Nullable;
import org.joml.Vector2d;
import org.joml.Vector2dc;
public final class DisplayQuad {
public final Vec3 pos;
public final Vec3 top;
public final Vec3 left;
public final Vec3 normal;
public DisplayQuad(Vec3 pos, Vec3 top, Vec3 left) {
this.pos = pos;
this.top = top;
this.left = left;
this.normal = this.left.cross(this.top);
}
public Vec3 unproject(Vector2dc point) {
return this.pos.add(this.top.scale(point.x())).add(this.left.scale(point.y()));
}
public @Nullable HitTestResult hitTest(Vec3 origin, Vec3 direction) {
var t = this.pos.subtract(origin).dot(this.normal) / direction.dot(this.normal);
if (t < 0) return null;
var candidatePoint = origin.add(direction.scale(t)).subtract(this.pos);
var widthSquared = this.top.lengthSqr();
var heightSquared = this.left.lengthSqr();
var point = new Vector2d(
candidatePoint.dot(this.top) / widthSquared,
candidatePoint.dot(this.left) / heightSquared
);
return point.x > 0 && point.x < 1 && point.y > 0 && point.y < 1
? new HitTestResult(point, t)
: null;
}
public record HitTestResult(Vector2dc point, double t) {}
}
================================================
FILE: src/main/java/io/wispforest/owo/braid/framework/BuildContext.java
================================================
package io.wispforest.owo.braid.framework;
import io.wispforest.owo.braid.framework.instance.WidgetInstance;
import org.jetbrains.annotations.Nullable;
public interface BuildContext {
@Nullable T getAncestor(Class ancestorClass, Object inheritedKey);
default @Nullable T getAncestor(Class ancestorClass) {
return this.getAncestor(ancestorClass, ancestorClass);
}
@Nullable T dependOnAncestor(Class ancestorClass, Object inheritedKey, @Nullable Object dependency);
default @Nullable T dependOnAncestor(Class ancestorClass, Object inheritedKey) {
return this.dependOnAncestor(ancestorClass, inheritedKey, null);
}
default @Nullable T dependOnAncestor(Class ancestorClass) {
return this.dependOnAncestor(ancestorClass, ancestorClass);
}
/// To prevent excessive IDE warnings, the return type of this
/// getter is not annotated `@Nullable` even though if it is called
/// before this context has been laid out, it will (correctly)
/// return null
WidgetInstance> instance();
}
================================================
FILE: src/main/java/io/wispforest/owo/braid/framework/instance/CustomWidgetTransform.java
================================================
package io.wispforest.owo.braid.framework.instance;
import org.jetbrains.annotations.Nullable;
import org.joml.*;
public class CustomWidgetTransform extends WidgetTransform {
protected @Nullable Matrix3x2f toParent;
protected @Nullable Matrix3x2f toWidget;
private boolean applyAtCenter = true;
private Matrix3x2f matrix = new Matrix3x2f();
public void setMatrix(Matrix3x2f matrix) {
this.setState(() -> this.matrix = matrix);
}
public Matrix3x2f matrix() {
return this.matrix;
}
public void setApplyAtCenter(boolean applyToCenter) {
this.setState(() -> this.applyAtCenter = applyToCenter);
}
public boolean applyAtCenter() {
return this.applyAtCenter;
}
protected Matrix3x2fc toParent() {
if (this.toParent == null) {
if (this.applyAtCenter) {
this.toParent = new Matrix3x2f()
.translate((float) (this.x + this.width / 2), (float) (this.y + this.height / 2))
.mul(this.matrix)
.translate((float) (-this.width / 2), (float) (-this.height / 2));
} else {
this.toParent = new Matrix3x2f()
.translate((float) this.x, (float) this.y)
.mul(this.matrix);
}
}
return this.toParent;
}
protected Matrix3x2fc toWidget() {
if (this.toWidget == null) {
this.toWidget = new Matrix3x2f(this.toParent()).invert();
}
return this.toWidget;
}
@Override
public void transformToParent(Matrix3x2f mat) {
mat.mul(this.toParent());
}
@Override
public void transformToParent(Matrix3x2fStack matrices) {
matrices.mul(this.toParent());
}
@Override
public void transformToWidget(Matrix3x2f mat) {
mat.mul(this.toWidget());
}
@Override
public void transformToWidget(Matrix3x2fStack matrices) {
matrices.mul(this.toWidget());
}
@Override
public void toParentCoordinates(Vector2d vec) {
var vec2f = new Vector2f(vec);
this.toParent().transformPosition(vec2f);
vec.set(vec2f.x, vec2f.y);
}
@Override
public void toWidgetCoordinates(Vector2d vec) {
var vec2f = new Vector2f(vec);
this.toWidget().transformPosition(vec2f);
vec.set(vec2f.x, vec2f.y);
}
@Override
public void recompute() {
super.recompute();
this.toParent = null;
this.toWidget = null;
}
}
================================================
FILE: src/main/java/io/wispforest/owo/braid/framework/instance/Hit.java
================================================
package io.wispforest.owo.braid.framework.instance;
public record Hit(WidgetInstance> instance, double x, double y) {}
================================================
FILE: src/main/java/io/wispforest/owo/braid/framework/instance/HitTestState.java
================================================
package io.wispforest.owo.braid.framework.instance;
import com.google.common.collect.FluentIterable;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Iterator;
import java.util.function.Predicate;
public class HitTestState {
private final Deque hits = new ArrayDeque<>();
public boolean anyHit() {
return !this.hits.isEmpty();
}
public Hit firstHit() {
return this.hits.getFirst();
}
public Iterable trace() {
return this.hits;
}
public Iterable occludedTrace() {
return new Iterable<>() {
@Override
public @NotNull Iterator iterator() {
var inner = HitTestState.this.hits.iterator();
return new Iterator<>() {
private boolean encounteredBoundary = false;
@Override
public boolean hasNext() {
return inner.hasNext() && !this.encounteredBoundary;
}
@Override
public Hit next() {
var next = inner.next();
if ((next.instance().flags & WidgetInstance.FLAG_HIT_TEST_BOUNDARY) != 0) {
this.encounteredBoundary = true;
}
return next;
}
};
}
};
}
public @Nullable Hit firstWhere(Predicate predicate) {
return FluentIterable.from(this.occludedTrace()).firstMatch(predicate::test).orNull();
}
public void addHit(WidgetInstance> instance, double x, double y) {
this.hits.addFirst(new Hit(instance, x, y));
}
}
================================================
FILE: src/main/java/io/wispforest/owo/braid/framework/instance/InspectorProperty.java
================================================
package io.wispforest.owo.braid.framework.instance;
import net.minecraft.network.chat.Component;
public record InspectorProperty(Component name, Component value) {}
================================================
FILE: src/main/java/io/wispforest/owo/braid/framework/instance/InstanceHost.java
================================================
package io.wispforest.owo.braid.framework.instance;
import io.wispforest.owo.braid.widgets.basic.LayoutBuilder;
import net.minecraft.client.Minecraft;
import org.joml.Vector2dc;
public interface InstanceHost {
Minecraft client();
/// Schedule a [WidgetInstance#layout] invocation for `instance`,
/// to be executed during the next layout pass.
///
/// This function must generally not be called during a layout pass
/// unless [#notifySubtreeRebuild] has been invoked first since
/// otherwise we run the risk of laying out some instances twice
void scheduleLayout(WidgetInstance> instance);
/// Notify the layout scheduler that a widget or proxy subtree
/// of the current element is (likely) about to rebuild and
/// subsequently [#scheduleLayout] may be invoked during the
/// current layout pass
///
/// This is used to implement the [LayoutBuilder] mechanism
void notifySubtreeRebuild();
void schedulePostLayoutCallback(Runnable callback);
Vector2dc cursorPosition();
}
================================================
FILE: src/main/java/io/wispforest/owo/braid/framework/instance/LeafWidgetInstance.java
================================================
package io.wispforest.owo.braid.framework.instance;
import io.wispforest.owo.braid.framework.widget.InstanceWidget;
public abstract class LeafWidgetInstance extends WidgetInstance {
public LeafWidgetInstance(T widget) {
super(widget);
}
@Override
public void visitChildren(Visitor visitor) {}
}
================================================
FILE: src/main/java/io/wispforest/owo/braid/framework/instance/MouseListener.java
================================================
package io.wispforest.owo.braid.framework.instance;
import io.wispforest.owo.braid.core.KeyModifiers;
import io.wispforest.owo.braid.core.cursor.CursorStyle;
import org.jetbrains.annotations.Nullable;
public interface MouseListener {
default @Nullable CursorStyle cursorStyleAt(double x, double y) {
return null;
}
default boolean onMouseDown(double x, double y, int button, KeyModifiers modifiers) {
return false;
}
default boolean onMouseUp(double x, double y, int button, KeyModifiers modifiers) {
return false;
}
default void onMouseEnter() {}
default void onMouseMove(double toX, double toY) {}
default void onMouseExit() {}
default void onMouseDragStart(int button, KeyModifiers modifiers) {}
default void onMouseDrag(double x, double y, double dx, double dy) {}
default void onMouseDragEnd() {}
default boolean onMouseScroll(double x, double y, double horizontal, double vertical) {
return false;
}
}
================================================
FILE: src/main/java/io/wispforest/owo/braid/framework/instance/MultiChildWidgetInstance.java
================================================
package io.wispforest.owo.braid.framework.instance;
import io.wispforest.owo.braid.core.BraidGraphics;
import io.wispforest.owo.braid.core.BraidUtils;
import io.wispforest.owo.braid.framework.widget.MultiChildInstanceWidget;
import java.util.ArrayList;
import java.util.List;
import java.util.OptionalDouble;
public abstract class MultiChildWidgetInstance extends WidgetInstance {
public List> children = new ArrayList<>();
public MultiChildWidgetInstance(T widget) {
super(widget);
}
@Override
public void draw(BraidGraphics graphics) {
for (var child : this.children) {
this.drawChild(graphics, child);
}
}
@Override
public void visitChildren(Visitor visitor) {
for (var child : this.children) {
visitor.visit(child);
}
}
public void insertChild(int index, WidgetInstance> child) {
this.children.set(index, this.adopt(child));
this.markNeedsLayout();
}
// ---
protected OptionalDouble computeFirstBaselineOffset() {
for (var child : this.children) {
var childBaseline = child.getBaselineOffset();
if (childBaseline.isEmpty()) continue;
return OptionalDouble.of(childBaseline.getAsDouble() + child.transform.y);
}
return OptionalDouble.empty();
}
protected OptionalDouble computeHighestBaselineOffset() {
return BraidUtils.fold(this.children, null, (acc, child) -> {
var childBaseline = child.getBaselineOffset();
if (childBaseline.isEmpty()) return acc;
return baselineMin(acc, OptionalDouble.of(childBaseline.getAsDouble() + child.transform.y));
});
}
private static OptionalDouble baselineMin(OptionalDouble a, OptionalDouble b) {
if (a.isEmpty()) return b;
if (b.isEmpty()) return a;
return a.getAsDouble() <= b.getAsDouble() ? a : b;
}
}
================================================
FILE: src/main/java/io/wispforest/owo/braid/framework/instance/OptionalChildWidgetInstance.java
================================================
package io.wispforest.owo.braid.framework.instance;
import com.google.common.base.Preconditions;
import io.wispforest.owo.braid.core.BraidGraphics;
import io.wispforest.owo.braid.core.Constraints;
import io.wispforest.owo.braid.framework.widget.InstanceWidget;
import org.jetbrains.annotations.Nullable;
import java.util.OptionalDouble;
public abstract class OptionalChildWidgetInstance extends WidgetInstance {
protected @Nullable WidgetInstance> child;
public OptionalChildWidgetInstance(T widget) {
super(widget);
}
@Override
public void draw(BraidGraphics graphics) {
if (this.child != null) {
this.drawChild(graphics, this.child);
}
}
@Override
public void visitChildren(Visitor visitor) {
if (this.child != null) {
visitor.visit(this.child);
}
}
public WidgetInstance> child() {
Preconditions.checkNotNull(this.child, "tried to retrieve child of SingleChildWidgetInstance before it was set");
return this.child;
}
public void setChild(@Nullable WidgetInstance> value) {
if (value == this.child) return;
this.child = this.adopt(value);
this.markNeedsLayout();
}
public static abstract class ShrinkWrap extends OptionalChildWidgetInstance {
public ShrinkWrap(T widget) {
super(widget);
}
@Override
protected void doLayout(Constraints constraints) {
this.sizeToChild(constraints, this.child);
}
@Override
protected double measureIntrinsicWidth(double height) {
return this.child != null ? this.child.getIntrinsicWidth(height) : 0;
}
@Override
protected double measureIntrinsicHeight(double width) {
return this.child != null ? this.child.getIntrinsicHeight(width) : 0;
}
@Override
protected OptionalDouble measureBaselineOffset() {
return this.child != null ? this.child.getBaselineOffset() : OptionalDouble.empty();
}
}
}
================================================
FILE: src/main/java/io/wispforest/owo/braid/framework/instance/SingleChildWidgetInstance.java
================================================
package io.wispforest.owo.braid.framework.instance;
import com.google.common.base.Preconditions;
import io.wispforest.owo.braid.core.BraidGraphics;
import io.wispforest.owo.braid.core.Constraints;
import io.wispforest.owo.braid.framework.widget.InstanceWidget;
import java.util.OptionalDouble;
public abstract class SingleChildWidgetInstance extends WidgetInstance {
protected WidgetInstance> child;
public SingleChildWidgetInstance(T widget) {
super(widget);
}
@Override
public void draw(BraidGraphics graphics) {
this.drawChild(graphics, this.child);
}
@Override
public void visitChildren(Visitor visitor) {
visitor.visit(this.child);
}
public WidgetInstance> child() {
Preconditions.checkNotNull(this.child, "tried to retrieve child of SingleChildWidgetInstance before it was set");
return this.child;
}
public void setChild(WidgetInstance> value) {
if (value == this.child) return;
this.child = this.adopt(value);
this.markNeedsLayout();
}
public static abstract class ShrinkWrap extends SingleChildWidgetInstance {
public ShrinkWrap(T widget) {
super(widget);
}
@Override
protected void doLayout(Constraints constraints) {
this.sizeToChild(constraints, this.child);
}
@Override
protected double measureIntrinsicWidth(double height) {
return this.child.getIntrinsicWidth(height);
}
@Override
protected double measureIntrinsicHeight(double width) {
return this.child.getIntrinsicHeight(width);
}
@Override
protected OptionalDouble measureBaselineOffset() {
return this.child.getBaselineOffset();
}
}
}
================================================
FILE: src/main/java/io/wispforest/owo/braid/framework/instance/TooltipProvider.java
================================================
package io.wispforest.owo.braid.framework.instance;
import net.minecraft.client.gui.screens.inventory.tooltip.ClientTooltipComponent;
import net.minecraft.network.chat.Style;
import org.jetbrains.annotations.Nullable;
import java.util.List;
public interface TooltipProvider {
@Nullable List getTooltipComponentsAt(double x, double y);
@Nullable
default Style getStyleAt(double x, double y) {
return null;
}
}
================================================
FILE: src/main/java/io/wispforest/owo/braid/framework/instance/WidgetInstance.java
================================================
package io.wispforest.owo.braid.framework.instance;
import com.google.common.base.Preconditions;
import io.wispforest.owo.Owo;
import io.wispforest.owo.braid.core.BraidGraphics;
import io.wispforest.owo.braid.core.Constraints;
import io.wispforest.owo.braid.core.LayoutAxis;
import io.wispforest.owo.braid.core.Size;
import io.wispforest.owo.braid.framework.widget.InstanceWidget;
import io.wispforest.owo.ui.core.Color;
import io.wispforest.owo.ui.util.NinePatchTexture;
import it.unimi.dsi.fastutil.objects.Object2DoubleMap;
import it.unimi.dsi.fastutil.objects.Object2DoubleOpenHashMap;
import net.minecraft.world.phys.AABB;
import org.jetbrains.annotations.MustBeInvokedByOverriders;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.joml.Matrix3x2f;
import org.joml.Vector2d;
import org.joml.Vector2f;
import java.util.*;
public abstract class WidgetInstance implements Comparable> {
public static final int FLAG_HIT_TEST_BOUNDARY = 0b1;
public final WidgetTransform transform = this.createTransform();
public @Nullable Object parentData;
public int flags = 0;
private int depth = 0;
private InstanceHost host;
private WidgetInstance> parent;
protected T widget;
// ---
public boolean debugHighlighted = false;
public boolean debugDrawVisualizers = false;
public boolean debugParentHasDependency() {
//noinspection OptionalAssignedToNull
return !this.intrinsicSizeCache.isEmpty() || this.baselineOffsetCache != null;
}
// ---
private @Nullable Constraints constraints;
private boolean needsLayout = false;
private @Nullable WidgetInstance> relayoutBoundary;
public WidgetInstance(T widget) {
this.widget = widget;
}
protected WidgetTransform createTransform() {
return new WidgetTransform();
}
// ---
public final Size layout(Constraints constraints) {
if (!this.needsLayout && Objects.equals(constraints, this.constraints)) {
return this.transform.toSize();
}
this.constraints = constraints;
this.relayoutBoundary = constraints.isTight() || this.parent == null ? this : this.parent.relayoutBoundary;
this.doLayout(constraints);
this.needsLayout = false;
return this.transform.toSize();
}
protected abstract void doLayout(Constraints constraints);
protected abstract double measureIntrinsicWidth(double height);
protected abstract double measureIntrinsicHeight(double width);
private final Object2DoubleMap intrinsicSizeCache = new Object2DoubleOpenHashMap<>();
public double getIntrinsicWidth(double height) {
return this.intrinsicSizeCache.computeIfAbsent(new IntrinsicCacheKey(LayoutAxis.HORIZONTAL, height), ($) -> this.measureIntrinsicWidth(height));
}
public double getIntrinsicHeight(double width) {
return this.intrinsicSizeCache.computeIfAbsent(new IntrinsicCacheKey(LayoutAxis.VERTICAL, width), ($) -> this.measureIntrinsicHeight(width));
}
protected abstract OptionalDouble measureBaselineOffset();
private @Nullable OptionalDouble baselineOffsetCache;
public OptionalDouble getBaselineOffset() {
//noinspection OptionalAssignedToNull
if (this.baselineOffsetCache != null) return this.baselineOffsetCache;
return this.baselineOffsetCache = this.measureBaselineOffset();
}
// ---
public abstract void draw(BraidGraphics graphics);
public abstract void visitChildren(Visitor visitor);
// ---
public void attachHost(InstanceHost host) {
this.host = host;
var callback = POST_ATTACH_CALLBACKS.remove(this);
if (callback != null) callback.run();
this.visitChildren(child -> child.attachHost(host));
}
protected > W adopt(W child) {
if (child == null || ((WidgetInstance>) child).parent == this) return child;
child.setDepth(this.depth + 1);
((WidgetInstance>) child).parent = this;
if (this.host != null) {
child.attachHost(this.host);
}
return child;
}
// ---
public List debugListInspectorProperties() {
return List.of();
}
public boolean debugHasVisualizers() {
return false;
}
protected void debugDrawVisualizers(BraidGraphics graphics) {}
// ---
protected void drawChild(BraidGraphics ctx, WidgetInstance> child) {
ctx.push();
child.transform.transformToParent(ctx.pose());
child.draw(ctx);
if (child.debugHasVisualizers() && child.debugDrawVisualizers) {
child.debugDrawVisualizers(ctx);
}
if (child.debugHighlighted) {
NinePatchTexture.draw(
Owo.id("braid_debug_highlighted"),
ctx,
0, 0, (int) child.transform.width(), (int) child.transform.height(),
Color.ofRgb(0x00FFD1)
);
}
ctx.pop();
}
protected void sizeToChild(Constraints constraints, @Nullable WidgetInstance> child) {
if (child == null) {
this.transform.setSize(constraints.minSize());
} else {
var childSize = child.layout(constraints);
this.transform.setSize(childSize);
}
}
public void clearLayoutCache(boolean recursive) {
this.needsLayout = true;
if (recursive) {
this.visitChildren(child -> child.clearLayoutCache(true));
}
}
@SuppressWarnings("OptionalAssignedToNull")
public void markNeedsLayout() {
this.needsLayout = true;
var parentHasDependency = !this.intrinsicSizeCache.isEmpty() || this.baselineOffsetCache != null;
this.intrinsicSizeCache.clear();
this.baselineOffsetCache = null;
if (!parentHasDependency && this.isRelayoutBoundary()) {
if (this.host != null) this.host.scheduleLayout(this);
} else {
if (this.parent != null) this.parent.markNeedsLayout();
}
}
private boolean debugDisposed = false;
@MustBeInvokedByOverriders
public void dispose() {
Preconditions.checkState(!this.debugDisposed, "tried to dispose a widget instance twice");
this.debugDisposed = true;
this.parent = null;
}
// ---
public List> ancestors() {
var result = new ArrayList>();
var ancestor = this.parent;
while (ancestor != null) {
result.add(ancestor);
ancestor = ancestor.parent;
}
return result;
}
public void hitTest(double x, double y, HitTestState state) {
if (this.hitTestSelf(x, y)) {
state.addHit(this, x, y);
}
var coordinates = new Vector2d();
this.visitChildren(child -> {
coordinates.set(x, y);
child.transform.toWidgetCoordinates(coordinates);
child.hitTest(coordinates.x, coordinates.y, state);
});
}
protected boolean hitTestSelf(double x, double y) {
return x >= 0 && x < this.transform.width && y >= 0 && y < this.transform.height;
}
public Matrix3x2f computeGlobalTransform() {
return this.computeTransformFrom(null);
}
public Matrix3x2f computeTransformFrom(@Nullable WidgetInstance> ancestor) {
var result = new Matrix3x2f();
this.transform.transformToWidget(result);
for (var step : this.ancestors()) {
if (step == ancestor) break;
step.transform.transformToWidget(result);
}
return result;
}
public AABB computeGlobalBounds() {
var global = this.parent != null ? this.parent.computeGlobalTransform().invert() : new Matrix3x2f();
var min = global.transformPosition(new Vector2f((float) this.transform.x, (float) this.transform.y));
var max = global.transformPosition(new Vector2f((float) (this.transform.x + this.transform.width), (float) (this.transform.y + this.transform.height)));
return new AABB(min.x, min.y, 0, max.x, max.y, 0);
}
public Vector2d computeGlobalPosition() {
var global = this.parent != null ? this.parent.computeGlobalTransform().invert() : new Matrix3x2f();
var pos = global.transformPosition(new Vector2f((float) this.transform.x, (float) this.transform.y));
return new Vector2d(pos.x, pos.y);
}
// ---
public @Nullable Constraints constraints() {
return this.constraints;
}
public int depth() {
return this.depth;
}
public void setDepth(int depth) {
if (this.depth == depth) return;
this.depth = depth;
this.visitChildren(child -> child.setDepth(this.depth + 1));
}
/// To prevent excessive IDE warnings, the return type of this
/// getter is not annotated `@Nullable` even though if it is called
/// before this instance is adopted it will (correctly) return null
public InstanceHost host() {
return this.host;
}
public boolean needsLayout() {
return this.needsLayout;
}
public boolean isRelayoutBoundary() {
return this.relayoutBoundary == this;
}
public boolean hasParent() {
return this.parent != null;
}
public void setWidget(T widget) {
this.widget = widget;
}
public T widget() {
return this.widget;
}
public WidgetInstance> parent() {
return this.parent;
}
// ---
private static final WeakHashMap, Runnable> POST_ATTACH_CALLBACKS = new WeakHashMap<>();
public static void addPostAttachCallback(WidgetInstance> instance, Runnable callback) {
POST_ATTACH_CALLBACKS.put(instance, callback);
}
// ---
@Override
public int compareTo(@NotNull WidgetInstance> o) {
return Integer.compare(this.depth, o.depth);
}
// ---
@FunctionalInterface
public interface Visitor {
void visit(WidgetInstance> child);
}
}
record IntrinsicCacheKey(LayoutAxis axis, double crossExtent) {}
//enum Visitors implements WidgetInstance.Visitor {
// MARK_NEEDS_LAYOUT(WidgetInstance::markNeedsLayout);
//
// private final WidgetInstance.Visitor delegate;
//
// Visitors(WidgetInstance.Visitor delegate) {
// this.delegate = delegate;
// }
//
// @Override
// public void visit(WidgetInstance> child) {
// this.delegate.visit(child);
// }
//}
================================================
FILE: src/main/java/io/wispforest/owo/braid/framework/instance/WidgetTransform.java
================================================
package io.wispforest.owo.braid.framework.instance;
import io.wispforest.owo.Owo;
import io.wispforest.owo.braid.core.LayoutAxis;
import io.wispforest.owo.braid.core.Size;
import org.joml.Matrix3x2f;
import org.joml.Matrix3x2fStack;
import org.joml.Vector2d;
public class WidgetTransform {
protected double x = 0, y = 0;
protected double width = 0, height = 0;
public void setX(double x) {
setState(() -> this.x = x);
}
public double x() {
return this.x;
}
public void setY(double y) {
setState(() -> this.y = y);
}
public double y() {
return this.y;
}
public void setWidth(double width) {
setState(() -> {
if (Double.isInfinite(width)) {
this.width = 69420;
Owo.LOGGER.error("A widget transform received infinite width, clamping to 69420. This should never happen");
} else {
this.width = width;
}
});
}
public double width() {
return this.width;
}
public void setHeight(double height) {
setState(() -> {
if (Double.isInfinite(height)) {
this.height = 69420;
Owo.LOGGER.error("A widget transform received infinite height, clamping to 69420. This should never happen");
} else {
this.height = height;
}
});
}
public double height() {
return this.height;
}
public void setSize(Size size) {
setState(() -> {
this.width = size.width();
this.height = size.height();
});
}
public Size toSize() {
return Size.of(this.width, this.height);
}
public void transformToParent(Matrix3x2f mat) {
mat.translate((float) this.x, (float) this.y);
}
public void transformToParent(Matrix3x2fStack matrices) {
matrices.translate((float) this.x, (float) this.y);
}
public void transformToWidget(Matrix3x2f mat) {
mat.translate((float) -this.x, (float) -this.y);
}
public void transformToWidget(Matrix3x2fStack matrices) {
matrices.translate((float) -this.x, (float) -this.y);
}
public void toParentCoordinates(Vector2d vec) {
vec.add(this.x, this.y);
}
public void toWidgetCoordinates(Vector2d vec) {
vec.sub(this.x, this.y);
}
public void setExtent(LayoutAxis axis, double value) {
switch (axis) {
case HORIZONTAL -> setWidth(value);
case VERTICAL -> setHeight(value);
}
}
public double getExtent(LayoutAxis axis) {
return switch (axis) {
case HORIZONTAL -> width();
case VERTICAL -> height();
};
}
public void setCoordinate(LayoutAxis axis, double value) {
switch (axis) {
case HORIZONTAL -> setX(value);
case VERTICAL -> setY(value);
}
}
public double getCoordinate(LayoutAxis axis) {
return switch (axis) {
case HORIZONTAL -> x();
case VERTICAL -> y();
};
}
protected void setState(Runnable action) {
action.run();
this.recompute();
}
public void recompute() {}
}
================================================
FILE: src/main/java/io/wispforest/owo/braid/framework/proxy/BuildScope.java
================================================
package io.wispforest.owo.braid.framework.proxy;
import io.wispforest.owo.Owo;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
public class BuildScope {
private final List dirtyProxies = new ArrayList<>();
private boolean resortProxies = true;
private final @Nullable Runnable scheduleRebuild;
public BuildScope(@Nullable Runnable scheduleRebuild) {
this.scheduleRebuild = scheduleRebuild;
}
public BuildScope() {
this(null);
}
// ---
public void scheduleRebuild(WidgetProxy proxy) {
this.dirtyProxies.add(proxy);
this.resortProxies = true;
if (this.scheduleRebuild != null) {
this.scheduleRebuild.run();
}
}
public boolean rebuildDirtyProxies() {
if (this.dirtyProxies.isEmpty()) return false;
this.dirtyProxies.sort(Comparator.naturalOrder());
for (var idx = 0; idx < this.dirtyProxies.size(); idx = this.nextDirtyIndex(idx)) {
this.dirtyProxies.get(idx).rebuild();
}
if (Owo.DEBUG && this.dirtyProxies.stream().anyMatch(BuildScope::isMissed)) {
throw new IllegalStateException(
"missed the following dirty proxies: ["
+ this.dirtyProxies.stream().filter(BuildScope::isMissed).map(Objects::toString).collect(Collectors.joining(", "))
+ "]"
);
}
this.dirtyProxies.clear();
return true;
}
private int nextDirtyIndex(int idx) {
if (!this.resortProxies) return idx + 1;
this.dirtyProxies.sort(Comparator.naturalOrder());
this.resortProxies = false;
idx++;
while (idx > 0 && this.dirtyProxies.get(idx - 1).needsRebuild()) {
idx--;
}
return idx;
}
// ---
private static boolean isMissed(WidgetProxy proxy) {
return proxy.needsRebuild && proxy.lifecycle == WidgetProxy.Lifecycle.LIVE;
}
}
================================================
FILE: src/main/java/io/wispforest/owo/braid/framework/proxy/ComposedProxy.java
================================================
package io.wispforest.owo.braid.framework.proxy;
import io.wispforest.owo.braid.framework.instance.WidgetInstance;
import io.wispforest.owo.braid.framework.widget.Widget;
import org.jetbrains.annotations.Nullable;
public abstract non-sealed class ComposedProxy extends WidgetProxy {
protected @Nullable WidgetProxy child;
public ComposedProxy(Widget widget) {
super(widget);
}
public WidgetProxy child() {
return this.child;
}
@Override
public void visitChildren(Visitor visitor) {
if (this.child != null) visitor.visit(this.child);
}
// ---
private WidgetInstance> descendantInstance;
@Override
public @Nullable WidgetInstance> instance() {
return this.descendantInstance;
}
@Override
public void notifyDescendantInstance(@Nullable WidgetInstance> instance, @Nullable Object slot) {
this.descendantInstance = instance;
}
}
================================================
FILE: src/main/java/io/wispforest/owo/braid/framework/proxy/InheritedProxy.java
================================================
package io.wispforest.owo.braid.framework.proxy;
import io.wispforest.owo.braid.framework.widget.InheritedWidget;
import io.wispforest.owo.braid.framework.widget.Widget;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
public class InheritedProxy extends ComposedProxy {
private final List dependents = new ArrayList<>();
public InheritedProxy(InheritedWidget widget) {
super(widget);
}
public void addDependency(WidgetProxy dependent, @Nullable Object dependency) {
this.dependents.add(dependent);
}
public void removeDependent(WidgetProxy dependent) {
this.dependents.remove(dependent);
}
protected boolean mustRebuildDependent(WidgetProxy dependent) {
return true;
}
public void notifyDependent(WidgetProxy dependent) {
dependent.notifyDependenciesChanged();
}
@Override
public void mount(WidgetProxy parent, @Nullable Object slot) {
super.mount(parent, slot);
this.inheritedProxies = this.inheritedProxies != null ? new HashMap<>(this.inheritedProxies) : new HashMap<>();
this.inheritedProxies.put(((InheritedWidget) this.widget()).inheritedKey(), this);
this.rebuild();
}
@Override
public void updateWidget(Widget newWidget) {
var shouldUpdate = ((InheritedWidget) this.widget()).mustRebuildDependents((InheritedWidget) newWidget);
super.updateWidget(newWidget);
this.rebuild(true);
if (shouldUpdate) {
for (var dependent : this.dependents) {
if (!this.mustRebuildDependent(dependent)) continue;
this.notifyDependent(dependent);
}
}
}
@Override
protected void doRebuild() {
super.doRebuild();
this.child = this.refreshChild(this.child, ((InheritedWidget) this.widget()).child, this.slot());
}
}
================================================
FILE: src/main/java/io/wispforest/owo/braid/framework/proxy/InstanceWidgetProxy.java
================================================
package io.wispforest.owo.braid.framework.proxy;
import com.google.common.base.Preconditions;
import io.wispforest.owo.braid.framework.instance.WidgetInstance;
import io.wispforest.owo.braid.framework.widget.InstanceWidget;
import io.wispforest.owo.braid.framework.widget.Widget;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
public abstract non-sealed class InstanceWidgetProxy extends WidgetProxy {
protected final WidgetInstance instance;
private final List ancestorsUntilNextInstanceProxy = new ArrayList<>();
protected InstanceWidgetProxy(InstanceWidget widget) {
super(widget);
//noinspection unchecked
this.instance = (WidgetInstance) widget.instantiate();
Preconditions.checkNotNull(this.instance, "Widget#instantiate must return a non-null instance");
}
@Override
public WidgetInstance extends InstanceWidget> instance() {
return this.instance;
}
@Override
public void mount(WidgetProxy parent, @Nullable Object slot) {
super.mount(parent, slot);
var ancestor = parent;
while (!(ancestor instanceof InstanceWidgetProxy)) {
this.ancestorsUntilNextInstanceProxy.add(ancestor);
ancestor = ancestor.parent();
}
this.ancestorsUntilNextInstanceProxy.add(ancestor);
this.rebuild();
this.notifyAncestors();
}
@Override
public void updateSlot(@Nullable Object newSlot) {
super.updateSlot(newSlot);
this.notifyAncestors();
}
@Override
public void unmount() {
super.unmount();
this.instance.dispose();
this.ancestorsUntilNextInstanceProxy.clear();
}
@Override
public void updateWidget(Widget newWidget) {
super.updateWidget(newWidget);
this.instance.setWidget((InstanceWidget) newWidget);
}
private void notifyAncestors() {
for (var listener : this.ancestorsUntilNextInstanceProxy) {
listener.notifyDescendantInstance(this.instance, this.slot());
}
}
}
================================================
FILE: src/main/java/io/wispforest/owo/braid/framework/proxy/LeafInstanceWidgetProxy.java
================================================
package io.wispforest.owo.braid.framework.proxy;
import com.google.common.base.Preconditions;
import io.wispforest.owo.braid.framework.instance.WidgetInstance;
import io.wispforest.owo.braid.framework.widget.LeafInstanceWidget;
import org.jetbrains.annotations.Nullable;
public class LeafInstanceWidgetProxy extends InstanceWidgetProxy {
public LeafInstanceWidgetProxy(LeafInstanceWidget widget) {
super(widget);
}
@Override
public void visitChildren(Visitor visitor) {}
@Override
public void notifyDescendantInstance(@Nullable WidgetInstance> instance, @Nullable Object slot) {
Preconditions.checkState(false, "a leaf proxy cannot have descendant instances");
}
}
================================================
FILE: src/main/java/io/wispforest/owo/braid/framework/proxy/MultiChildInstanceWidgetProxy.java
================================================
package io.wispforest.owo.braid.framework.proxy;
import com.google.common.base.Preconditions;
import io.wispforest.owo.braid.framework.instance.MultiChildWidgetInstance;
import io.wispforest.owo.braid.framework.instance.WidgetInstance;
import io.wispforest.owo.braid.framework.widget.InstanceWidget;
import io.wispforest.owo.braid.framework.widget.Key;
import io.wispforest.owo.braid.framework.widget.MultiChildInstanceWidget;
import io.wispforest.owo.braid.framework.widget.Widget;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class MultiChildInstanceWidgetProxy extends InstanceWidgetProxy {
public List children = new ArrayList<>();
public List> childInstances = new ArrayList<>();
public MultiChildInstanceWidgetProxy(MultiChildInstanceWidget widget) {
super(widget);
}
@Override
public MultiChildWidgetInstance extends InstanceWidget> instance() {
//noinspection unchecked
return (MultiChildWidgetInstance extends InstanceWidget>) super.instance();
}
@Override
public void visitChildren(Visitor visitor) {
for (var child : children) {
visitor.visit(child);
}
}
@Override
public void updateWidget(Widget newWidget) {
super.updateWidget(newWidget);
rebuild(true);
}
@Override
public void doRebuild() {
super.doRebuild();
var newWidgets = ((MultiChildInstanceWidget) this.widget()).children;
var newChildrenTop = 0;
var oldChildrenTop = 0;
var newChildrenBottom = newWidgets.size() - 1;
var oldChildrenBottom = this.children.size() - 1;
var newChildren = Stream.generate(() -> null).limit(newWidgets.size()).collect(Collectors.toList());
// we already set up the new child instance list, so that any
// notifyDescendantInstance invocations caused by the below
// refreshChild calls always index into the correct list
this.childInstances = Stream.>generate(() -> null).limit(newChildren.size()).collect(Collectors.toList());
copyInto(this.childInstances, 0, this.instance().children, 0, Math.min(this.childInstances.size(), this.instance().children.size()));
if (this.instance().children.size() > this.childInstances.size()) {
this.instance().markNeedsLayout();
}
this.instance().children = this.childInstances;
// sync from the top
while ((oldChildrenTop <= oldChildrenBottom) && (newChildrenTop <= newChildrenBottom)) {
var oldChild = this.children.get(oldChildrenTop);
var newWidget = newWidgets.get(newChildrenTop);
if (!Widget.canUpdate(oldChild.widget(), newWidget)) {
break;
}
newChildren.set(newChildrenTop, this.refreshChild(oldChild, newWidget, newChildrenTop));
Preconditions.checkNotNull(this.childInstances.get(newChildrenTop));
oldChildrenTop++;
newChildrenTop++;
}
// scan from the bottom
while ((oldChildrenTop <= oldChildrenBottom) && (newChildrenTop <= newChildrenBottom)) {
var oldChild = this.children.get(oldChildrenTop);
var newWidget = newWidgets.get(newChildrenTop);
if (!Widget.canUpdate(oldChild.widget(), newWidget)) {
break;
}
oldChildrenTop++;
newChildrenTop++;
}
// scan middle, store keyed and disposed un-keyed
var hasOldChildren = oldChildrenTop <= oldChildrenBottom;
Map keyedOldChildren = null;
if (hasOldChildren) {
keyedOldChildren = new HashMap<>();
while (oldChildrenTop <= oldChildrenBottom) {
var oldChild = this.children.get(oldChildrenTop);
var key = oldChild.widget().key();
if (key != null) {
keyedOldChildren.put(key, oldChild);
} else {
oldChild.unmount();
}
oldChildrenTop++;
}
}
// sync middle, updating keyed
while (newChildrenTop <= newChildrenBottom) {
WidgetProxy oldChild = null;
var newWidget = newWidgets.get(newChildrenTop);
if (hasOldChildren) {
var key = newWidget.key();
if (key != null) {
oldChild = keyedOldChildren.get(key);
if (oldChild != null) {
if (Widget.canUpdate(oldChild.widget(), newWidget)) {
keyedOldChildren.remove(key);
} else {
oldChild = null;
}
}
}
}
newChildren.set(newChildrenTop, this.refreshChild(oldChild, newWidget, newChildrenTop));
Preconditions.checkNotNull(this.childInstances.get(newChildrenTop));
newChildrenTop++;
}
newChildrenBottom = newWidgets.size() - 1;
oldChildrenBottom = this.children.size() - 1;
while ((oldChildrenTop <= oldChildrenBottom) && (newChildrenTop <= newChildrenBottom)) {
var oldChild = this.children.get(oldChildrenTop);
var newWidget = newWidgets.get(newChildrenTop);
newChildren.set(newChildrenTop, this.refreshChild(oldChild, newWidget, newChildrenTop));
Preconditions.checkNotNull(this.childInstances.get(newChildrenTop));
oldChildrenTop++;
newChildrenTop++;
}
// dispose keyed proxies that were not reused
if (hasOldChildren && !keyedOldChildren.isEmpty()) {
for (var proxy : keyedOldChildren.values()) {
proxy.unmount();
}
}
// finally, install new children
this.children = newChildren;
}
@Override
public void notifyDescendantInstance(@Nullable WidgetInstance> instance, @Nullable Object slot) {
this.instance().insertChild(((Integer) slot).intValue(), instance);
}
@SuppressWarnings("SameParameterValue")
private static void copyInto(List target, int at, List source, int from, int to) {
var copyCount = to - from;
for (var i = 0; i < copyCount; i++) {
target.set(at + i, source.get(from + i));
}
}
}
================================================
FILE: src/main/java/io/wispforest/owo/braid/framework/proxy/OptionalChildInstanceWidgetProxy.java
================================================
package io.wispforest.owo.braid.framework.proxy;
import io.wispforest.owo.braid.framework.instance.OptionalChildWidgetInstance;
import io.wispforest.owo.braid.framework.instance.WidgetInstance;
import io.wispforest.owo.braid.framework.widget.InstanceWidget;
import io.wispforest.owo.braid.framework.widget.OptionalChildInstanceWidget;
import io.wispforest.owo.braid.framework.widget.Widget;
import org.jetbrains.annotations.Nullable;
public class OptionalChildInstanceWidgetProxy extends InstanceWidgetProxy {
protected @Nullable WidgetProxy child;
public OptionalChildInstanceWidgetProxy(OptionalChildInstanceWidget widget) {
super(widget);
}
@Override
public OptionalChildWidgetInstance extends InstanceWidget> instance() {
return (OptionalChildWidgetInstance extends InstanceWidget>) super.instance();
}
@Override
public void updateWidget(Widget newWidget) {
super.updateWidget(newWidget);
this.rebuild(true);
}
@Override
protected void doRebuild() {
super.doRebuild();
this.child = this.refreshChild(this.child, ((OptionalChildInstanceWidget) this.widget()).child, null);
if (((OptionalChildInstanceWidget) this.widget()).child == null) {
this.instance().setChild(null);
}
}
@Override
public void notifyDescendantInstance(@Nullable WidgetInstance> instance, @Nullable Object slot) {
this.instance().setChild(instance);
}
@Override
public void visitChildren(Visitor visitor) {
if (this.child != null) {
visitor.visit(this.child);
}
}
}
================================================
FILE: src/main/java/io/wispforest/owo/braid/framework/proxy/ProxyHost.java
================================================
package io.wispforest.owo.braid.framework.proxy;
import net.minecraft.client.Minecraft;
import java.time.Duration;
public interface ProxyHost {
Minecraft client();
void scheduleAnimationCallback(AnimationCallback callback);
long scheduleDelayedCallback(Duration delay, Runnable callback);
void cancelDelayedCallback(long id);
void schedulePostLayoutCallback(Runnable callback);
interface AnimationCallback {
void run(Duration delta);
}
}
================================================
FILE: src/main/java/io/wispforest/owo/braid/framework/proxy/SingleChildInstanceWidgetProxy.java
================================================
package io.wispforest.owo.braid.framework.proxy;
import io.wispforest.owo.braid.framework.instance.SingleChildWidgetInstance;
import io.wispforest.owo.braid.framework.instance.WidgetInstance;
import io.wispforest.owo.braid.framework.widget.InstanceWidget;
import io.wispforest.owo.braid.framework.widget.SingleChildInstanceWidget;
import io.wispforest.owo.braid.framework.widget.Widget;
import org.jetbrains.annotations.Nullable;
public class SingleChildInstanceWidgetProxy extends InstanceWidgetProxy {
protected WidgetProxy child;
public SingleChildInstanceWidgetProxy(SingleChildInstanceWidget widget) {
super(widget);
}
@Override
public SingleChildWidgetInstance extends InstanceWidget> instance() {
return (SingleChildWidgetInstance extends InstanceWidget>) super.instance();
}
@Override
public void updateWidget(Widget newWidget) {
super.updateWidget(newWidget);
this.rebuild(true);
}
@Override
protected void doRebuild() {
super.doRebuild();
this.child = this.refreshChild(this.child, ((SingleChildInstanceWidget) this.widget()).child, null);
}
@Override
public void notifyDescendantInstance(@Nullable WidgetInstance> instance, @Nullable Object slot) {
this.instance().setChild(instance);
}
@Override
public void visitChildren(Visitor visitor) {
if (this.child != null) {
visitor.visit(this.child);
}
}
}
================================================
FILE: src/main/java/io/wispforest/owo/braid/framework/proxy/StatefulProxy.java
================================================
package io.wispforest.owo.braid.framework.proxy;
import io.wispforest.owo.braid.framework.widget.StatefulWidget;
import io.wispforest.owo.braid.framework.widget.Widget;
import org.jetbrains.annotations.Nullable;
public class StatefulProxy extends ComposedProxy {
private final WidgetState state;
private boolean dependenciesChanged = false;
public StatefulProxy(StatefulWidget widget) {
super(widget);
//noinspection unchecked
this.state = (WidgetState) widget.createState();
this.state.widget = (StatefulWidget) this.widget();
this.state.owner = this;
}
public WidgetState> state() {
return this.state;
}
@Override
public void mount(WidgetProxy parent, @Nullable Object slot) {
super.mount(parent, slot);
this.state.init();
this.rebuild();
}
@Override
public void notifyDependenciesChanged() {
super.notifyDependenciesChanged();
this.dependenciesChanged = true;
}
@Override
public void unmount() {
super.unmount();
this.state.dispose();
}
@Override
public void updateWidget(Widget newWidget) {
super.updateWidget(newWidget);
var oldWidget = this.state.widget;
this.state.widget = (StatefulWidget) newWidget;
this.state.didUpdateWidget(oldWidget);
this.rebuild(true);
}
@Override
protected void doRebuild() {
if (this.dependenciesChanged) {
this.state.notifyDependenciesChanged();
this.dependenciesChanged = false;
}
var newWidget = this.state.build(this);
super.doRebuild();
this.child = this.refreshChild(this.child, newWidget, this.slot());
}
}
================================================
FILE: src/main/java/io/wispforest/owo/braid/framework/proxy/StatelessProxy.java
================================================
package io.wispforest.owo.braid.framework.proxy;
import io.wispforest.owo.braid.framework.widget.StatelessWidget;
import io.wispforest.owo.braid.framework.widget.Widget;
import org.jetbrains.annotations.Nullable;
public class StatelessProxy extends ComposedProxy {
public StatelessProxy(StatelessWidget widget) {
super(widget);
}
@Override
public void mount(WidgetProxy parent, @Nullable Object slot) {
super.mount(parent, slot);
this.rebuild();
}
@Override
public void updateWidget(Widget newWidget) {
super.updateWidget(newWidget);
this.rebuild(true);
}
@Override
protected void doRebuild() {
var newWidget = ((StatelessWidget) this.widget()).build(this);
super.doRebuild();
this.child = this.refreshChild(this.child, newWidget, this.slot());
}
}
================================================
FILE: src/main/java/io/wispforest/owo/braid/framework/proxy/WidgetProxy.java
================================================
package io.wispforest.owo.braid.framework.proxy;
import com.google.common.base.Preconditions;
import io.wispforest.owo.braid.framework.BuildContext;
import io.wispforest.owo.braid.framework.instance.WidgetInstance;
import io.wispforest.owo.braid.framework.widget.Widget;
import org.jetbrains.annotations.MustBeInvokedByOverriders;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
public abstract sealed class WidgetProxy implements BuildContext, Comparable permits ComposedProxy, InstanceWidgetProxy {
private Widget widget;
private @Nullable WidgetProxy parent;
private BuildScope parentBuildScope;
private int depth = -1;
private @Nullable ProxyHost host;
private @Nullable Object slot;
public Lifecycle lifecycle = Lifecycle.INITIAL;
protected boolean needsRebuild = true;
protected Map