Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@ org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled
javaVersion=25
mcVersion=26.2
group=dev.slne.surf.api
version=3.41.0
version=3.42.0
relocationPrefix=dev.slne.surf.api.libs
snapshot=false
36 changes: 32 additions & 4 deletions surf-api-core/surf-api-core/api/surf-api-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -7244,22 +7244,50 @@ public abstract class dev/slne/surf/api/core/inventory/framework/internal/Pagina
public static fun values ()[Ldev/slne/surf/api/core/inventory/framework/internal/PaginationButtonGlyphs;
}

public final class dev/slne/surf/api/core/inventory/framework/internal/RenderedViewTitle {
public fun <init> (Ljava/lang/String;I)V
public final fun component1 ()Ljava/lang/String;
public final fun component2 ()I
public final fun copy (Ljava/lang/String;I)Ldev/slne/surf/api/core/inventory/framework/internal/RenderedViewTitle;
public static synthetic fun copy$default (Ldev/slne/surf/api/core/inventory/framework/internal/RenderedViewTitle;Ljava/lang/String;IILjava/lang/Object;)Ldev/slne/surf/api/core/inventory/framework/internal/RenderedViewTitle;
public fun equals (Ljava/lang/Object;)Z
public final fun getText ()Ljava/lang/String;
public final fun getWidth ()I
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}

public final class dev/slne/surf/api/core/inventory/framework/internal/ShiftGlyphs {
public static final field INSTANCE Ldev/slne/surf/api/core/inventory/framework/internal/ShiftGlyphs;
public final fun renderShift (I)Ljava/lang/String;
}

public final class dev/slne/surf/api/core/inventory/framework/internal/TextAlignmentMath {
public static final field INSTANCE Ldev/slne/surf/api/core/inventory/framework/internal/TextAlignmentMath;
public final fun centerAlignedShift (Ljava/lang/String;IIIII)I
public final fun centerAlignedShift (IIII)I
public final fun charWidth (IILit/unimi/dsi/fastutil/ints/Int2IntMap;)I
public static synthetic fun charWidth$default (Ldev/slne/surf/api/core/inventory/framework/internal/TextAlignmentMath;IILit/unimi/dsi/fastutil/ints/Int2IntMap;ILjava/lang/Object;)I
public final fun leftAlignedShift (II)I
public final fun rightAlignedShift (Ljava/lang/String;IIIII)I
public final fun textWidth (Ljava/lang/String;II)I
public final fun rightAlignedShift (IIII)I
public final fun textWidth (Ljava/lang/String;IILit/unimi/dsi/fastutil/ints/Int2IntMap;)I
public static synthetic fun textWidth$default (Ldev/slne/surf/api/core/inventory/framework/internal/TextAlignmentMath;Ljava/lang/String;IILit/unimi/dsi/fastutil/ints/Int2IntMap;ILjava/lang/Object;)I
}

public final class dev/slne/surf/api/core/inventory/framework/internal/ViewButtonSession {
public fun <init> (Ljava/util/List;Ljava/lang/Object;)V
public final fun advance (I)Z
public final fun getCurrent ()Ljava/lang/Object;
public final fun getCurrentIndex ()I
public final fun getHasChanged ()Z
public final fun getInitial ()Ljava/lang/Object;
public final fun getStates ()Ljava/util/List;
public final fun select (Ljava/lang/Object;)Z
}

public final class dev/slne/surf/api/core/inventory/framework/internal/ViewContainerRenderingKt {
public static final fun appendShiftedComponent (Ldev/slne/surf/api/core/messages/builder/SurfComponentBuilder;IILkotlin/jvm/functions/Function1;)V
public static final fun formatViewTitle (Ljava/lang/String;IZ)Ljava/lang/String;
public static final fun renderViewTitle (Ljava/lang/String;IIZLit/unimi/dsi/fastutil/ints/Int2IntMap;)Ldev/slne/surf/api/core/inventory/framework/internal/RenderedViewTitle;
public static synthetic fun renderViewTitle$default (Ljava/lang/String;IIZLit/unimi/dsi/fastutil/ints/Int2IntMap;ILjava/lang/Object;)Ldev/slne/surf/api/core/inventory/framework/internal/RenderedViewTitle;
}

public final class dev/slne/surf/api/core/invoker/HiddenInvokerUtil {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package dev.slne.surf.api.core.inventory.framework.internal

import dev.slne.surf.api.shared.api.util.InternalSurfApi
import it.unimi.dsi.fastutil.ints.Int2IntMap
import it.unimi.dsi.fastutil.ints.Int2IntMaps

/**
* Shared pixel math behind the per-platform `TextAlignment` enums.
Expand All @@ -14,22 +16,55 @@ import dev.slne.surf.api.shared.api.util.InternalSurfApi
@InternalSurfApi
object TextAlignmentMath {

/**
* Returns the rendered pixel width of the glyph for [codePoint].
*
* Looks [codePoint] up in [charWidths] and falls back to [charSize] when the font renders
* it at the default width.
*
* @param codePoint the Unicode code point to measure
* @param charSize the width in pixels of a single character in the title font
* @param charWidths per-code-point width overrides for glyphs that are not [charSize] wide
*/
fun charWidth(
codePoint: Int,
charSize: Int,
charWidths: Int2IntMap = Int2IntMaps.EMPTY_MAP,
): Int = if (charWidths.containsKey(codePoint)) charWidths.get(codePoint) else charSize

/**
* Computes the total rendered pixel width of [text].
*
* Uses the formula: `text.length * charSize + (text.length - 1) * charSpacing`.
* Returns `0` for an empty string.
* Sums the width of every glyph (see [charWidth]) and adds [charSpacing] between each pair of
* adjacent glyphs. Returns `0` for an empty string.
*
* Measurement is done per Unicode **code point**, not per `Char`, so surrogate pairs count as
* the single glyph they render as.
*
* @param text the string to measure
* @param text the string to measure, exactly as it is rendered
* @param charSize the width in pixels of a single character
* @param charSpacing the inter-character spacing in pixels
* @param charWidths per-code-point width overrides for glyphs that are not [charSize] wide
* @return the total pixel width of the text
*/
fun textWidth(text: String, charSize: Int, charSpacing: Int): Int {
if (text.isEmpty()) return 0
val n = text.length
fun textWidth(
text: String,
charSize: Int,
charSpacing: Int,
charWidths: Int2IntMap = Int2IntMaps.EMPTY_MAP,
): Int {
val glyphs = text.codePointCount(0, text.length)
if (glyphs == 0) return 0

var width = (glyphs - 1) * charSpacing
var index = 0
while (index < text.length) {
val codePoint = text.codePointAt(index)
width += charWidth(codePoint, charSize, charWidths)
index += Character.charCount(codePoint)
}

return (n * charSize) + ((n - 1) * charSpacing)
return width
}

/**
Expand All @@ -41,48 +76,46 @@ object TextAlignmentMath {
fun leftAlignedShift(leftShift: Int, padding: Int): Int = leftShift + padding

/**
* Pixel shift that places [text] at the right edge of the container area.
* Pixel shift that places a run of [textWidth] pixels at the right edge of the container area.
*
* @param text the string whose width is taken into account
* @param textWidth the rendered pixel width of the text, as returned by [textWidth]
* @param leftShift the base pixel offset of the container area
* @param padding horizontal padding applied on each side within the container
* @param containerWidth the total usable pixel width of the container area
* @param charSize the width in pixels of a single character
* @param charSpacing the inter-character spacing in pixels
*/
fun rightAlignedShift(
text: String,
textWidth: Int,
leftShift: Int,
padding: Int,
containerWidth: Int,
charSize: Int,
charSpacing: Int,
): Int {
val usableWidth = containerWidth - (padding * 2)
val freeSpace = usableWidth - textWidth(text, charSize, charSpacing)
val freeSpace = usableWidth - textWidth

return leftShift + freeSpace + 1 + padding
}

/**
* Pixel shift that centers [text] horizontally within the container area.
* Pixel shift that centers a run of [textWidth] pixels horizontally within the container area.
*
* The free space is halved with [Math.floorDiv] so that a title wider than the container (which
* yields a negative free space) overflows evenly instead of being biased to one side by
* truncation towards zero.
*
* @param text the string whose width is taken into account
* @param textWidth the rendered pixel width of the text, as returned by [textWidth]
* @param leftShift the base pixel offset of the container area
* @param padding horizontal padding applied on each side within the container
* @param containerWidth the total usable pixel width of the container area
* @param charSize the width in pixels of a single character
* @param charSpacing the inter-character spacing in pixels
*/
fun centerAlignedShift(
text: String,
textWidth: Int,
leftShift: Int,
padding: Int,
containerWidth: Int,
charSize: Int,
charSpacing: Int,
): Int {
val usableWidth = containerWidth - (padding * 2)
val freeSpace = usableWidth - textWidth(text, charSize, charSpacing)
return leftShift + (freeSpace / 2) + 1 + padding
val freeSpace = usableWidth - textWidth

return leftShift + Math.floorDiv(freeSpace, 2) + 1 + padding
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package dev.slne.surf.api.core.inventory.framework.internal

import dev.slne.surf.api.shared.api.util.InternalSurfApi

/**
* Per-viewer state of a stateful inventory button.
*
* A session owns the ordered [states] a button can cycle through, the [initial] value it started
* out with, and the value it currently shows. It is created once per view session (per player) and
* mutated in place whenever the button is clicked, which is what allows the view to tell on close
* whether the button was actually used — see [hasChanged].
*
* All of the cycling logic lives here so the per-platform button implementations cannot drift
* apart. This is internal infrastructure — use the platform `toggleButton` / `statefulButton` /
* `tripleButton` DSL instead.
*
* @param T the type identifying one button state
* @property states the ordered states the button cycles through; must be non-empty and is not
* copied, so callers must pass an immutable list
* @property initial the state the button started out with for this viewer
*/
@InternalSurfApi
class ViewButtonSession<T>(
val states: List<T>,
val initial: T,
) {
/** Index into [states] of the currently shown state. */
var currentIndex: Int = states.indexOf(initial)
private set

init {
require(states.isNotEmpty()) { "A stateful button must declare at least one state" }
require(currentIndex >= 0) {
"The initial value '$initial' is not one of the button's declared states $states"
}
}

/** The currently shown state. */
val current: T get() = states[currentIndex]

/**
* Whether the button is currently showing a state other than the one it started out with.
*
* Note that this compares against [initial] rather than counting clicks, so cycling all the way
* back to the starting state counts as *unchanged*.
*/
val hasChanged: Boolean get() = current != initial

/**
* Moves [step] states forward (or backward for a negative [step]), wrapping around at both
* ends of [states].
*
* @param step how many states to move; `0` and buttons with a single state never move
* @return `true` if the current state actually changed
*/
fun advance(step: Int): Boolean {
if (step == 0 || states.size < 2) return false

val next = Math.floorMod(currentIndex + step, states.size)
if (next == currentIndex) return false

currentIndex = next
return true
}

/**
* Jumps directly to [value].
*
* @param value the state to show; must be one of [states]
* @return `true` if the current state actually changed
* @throws IllegalArgumentException if [value] is not one of [states]
*/
fun select(value: T): Boolean {
val index = states.indexOf(value)
require(index >= 0) {
"The value '$value' is not one of the button's declared states $states"
}

if (index == currentIndex) return false

currentIndex = index
return true
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package dev.slne.surf.api.core.inventory.framework.internal

import dev.slne.surf.api.core.messages.builder.SurfComponentBuilder
import dev.slne.surf.api.shared.api.util.InternalSurfApi
import it.unimi.dsi.fastutil.ints.Int2IntMap
import it.unimi.dsi.fastutil.ints.Int2IntMaps

/**
* Appends a single container component to this builder, surrounded by the shift glyphs that
Expand All @@ -12,6 +14,10 @@ import dev.slne.surf.api.shared.api.util.InternalSurfApi
* 2. Emit [renderComponent].
* 3. Shift the cursor back by `-(textureWidth + positionalShift)` pixels.
*
* Step 3 only returns the cursor to the container origin if [textureWidth] matches the number of
* pixels [renderComponent] actually advanced it by. Components that cannot guarantee that must
* report `hasExactWidth = false` so the platform `ViewContainer` renders them last.
*
* Shared by the per-platform `ViewContainer.render()` implementations. This is internal
* infrastructure — use the platform DSL instead.
*
Expand All @@ -38,27 +44,61 @@ fun SurfComponentBuilder.appendShiftedComponent(
}

/**
* Builds the rendered inventory title string: every character is uppercased and inter-character
* spacing glyphs are inserted between them.
* An inventory title that has been laid out for rendering.
*
* @property text the glyph-interleaved string to emit
* @property width the exact number of pixels [text] advances the render cursor by
*/
@InternalSurfApi
data class RenderedViewTitle(val text: String, val width: Int)

/**
* Lays out the rendered inventory title: every character is uppercased and inter-character
* spacing glyphs are inserted between them. Unless the title is right-aligned, a leading spacing
* glyph is prepended as well.
*
* Unless the title is right-aligned, a leading spacing glyph is prepended as well.
* The returned [RenderedViewTitle.width] is measured on the string that is actually emitted, so it
* accounts for the leading spacing glyph and for uppercasing that changes the glyph count (`ß`
* uppercases to `SS`). Text is walked per Unicode **code point**, so surrogate pairs are neither
* split apart nor counted twice.
*
* Shared by the per-platform `ViewContainerTitleComponent` implementations. This is internal
* infrastructure — use the platform DSL instead.
*
* @param title the plain-text inventory title
* @param charSize the pixel width of a single character in the title font
* @param charSpacing the pixel spacing to insert between each character
* @param alignRight `true` when the title is right-aligned
* @return the glyph-interleaved title string
* @param charWidths per-code-point width overrides for glyphs that are not [charSize] wide
* @return the string to render together with its exact pixel width
*/
@InternalSurfApi
fun formatViewTitle(title: String, charSpacing: Int, alignRight: Boolean): String {
val shifted = title.map { it.uppercase() }
.joinToString(ShiftGlyphs.renderShift(charSpacing))
fun renderViewTitle(
title: String,
charSize: Int,
charSpacing: Int,
alignRight: Boolean,
charWidths: Int2IntMap = Int2IntMaps.EMPTY_MAP,
): RenderedViewTitle {
val uppercased = title.uppercase()
if (uppercased.isEmpty()) return RenderedViewTitle("", 0)

val spacing = ShiftGlyphs.renderShift(charSpacing)
val text = buildString {
if (!alignRight) append(spacing)

var index = 0
while (index < uppercased.length) {
if (index > 0) append(spacing)

return if (alignRight) {
shifted
} else {
shifted.prependIndent(ShiftGlyphs.renderShift(charSpacing))
val codePoint = uppercased.codePointAt(index)
appendCodePoint(codePoint)
index += Character.charCount(codePoint)
}
}

val width = TextAlignmentMath.textWidth(uppercased, charSize, charSpacing, charWidths) +
if (alignRight) 0 else charSpacing

return RenderedViewTitle(text, width)
}
Loading