lattice/docs/architecture.md
2026-07-10 01:43:19 -04:00

5.1 KiB

Architecture

Lattice replaces Knit and Vexel with one library. This document records the major decisions and what changed relative to the two parents.

Module layout

Single Gradle module; boundaries are packages with a strict dependency direction (lower layers never import higher ones):

event/       EventBus, EventCall, lifecycle/tick/render events
client/      Minecraft wrappers: Client, Player, Chat, Clipboard, Desktop, Loader
input/       GLFW-backed Keys/Mouse/Keyboard, InputCode abstraction
render/      Renderer interface, NVGRenderer, Font/Image/Color/Gradient, Resolution, GLState
scheduler/   TickScheduler (game ticks), TimeScheduler (wall clock)
animation/   Animation core, manager, element extensions, presets
ui/          Element, Box, Window, UIScreen, layout enums, element events
ui/component Container, Rectangle, Text, SvgImage, Tooltip
ui/widget    Button, CheckBox, ColorPicker, Dropdown, Keybind, NumberInput, Slider, Switch, TextInput
ui/theme     Theme tokens
text/        Texts + TextBuilder (Component DSL)
command/     Commodore command DSL over Brigadier
util/        Number/String helpers
mixins/      (Java) Minecraft, GameRenderer, GlStateManager

A multi-module split was considered and rejected: the whole library is ~10k lines and every layer targets the same Minecraft version; module plumbing would outweigh the benefit.

Key decisions

One event bus. Knit and Vexel each created their own EventBus instance, so subscribers had to know which library fired an event. Lattice.eventBus is the only bus. The unused class-hierarchy dispatch path (checkHierarchy) was removed — subscribers register on concrete event classes.

One screen class. Knit's KnitScreen adapted vanilla Screen input and Vexel's VexelScreen layered a Window plus render-event wiring on top. UIScreen does both jobs in one class half the size.

Box deduplicates scrolling. Vexel's Container and Rectangle each carried a full copy of the scroll/scrollbar/hover/padding logic (~300 lines duplicated). Box owns padding, scrolling, scrollbar drawing and the scroll-adjusted input handling; Container is Box with no drawing and Rectangle is Box plus painting. The layout engine reads padding uniformly through Box instead of type-switching on two unrelated classes.

Int ARGB everywhere. The renderer API mixed Int ARGB, java.awt.Color, and Vexel's HSB Color class. The Renderer interface now takes Int ARGB only; the HSB Color remains in render/ as a utility (used by ColorPicker internals and available to clients).

Theme tokens. Widgets previously hardcoded their palette. Widget constructor defaults now read Theme (accent, surfaces, borders, text, track/scrollbar, animation durations), initialized to the exact Vexel palette so the rendered result is unchanged. Aether's theme system inspired the shape.

Fabric-only, current-version-only. Knit carried multi-loader preprocessor blocks (Forge/NeoForge) and multi-version conditionals that this port could never exercise. All of it is gone; client.Loader is a thin Fabric wrapper.

Deleted dead code

  • World rendering layer: RenderContext was all no-ops and MixinWorldRenderer threw unconditionally in the 26.1.2 port. Gone, along with WorldRenderEvent (14 event types that could never fire).
  • MixinClientCommonNetworkHandler: empty mixin.
  • TickEvent.Server and the server scheduler: nothing ever posted or ticked them.
  • Duplicate mixin sources: both parents shipped identical .java files in src/main/java and src/main/kotlin, which broke sourcesJar on Gradle 9.
  • Renderer.createImage(id) parameter: ignored by the implementation.
  • scrollbarIgnorePadding/scrollbarCustomPadding: duplicated scrollbarPadding.

Fixes

  • Linux natives: Vexel shipped only natives-macos-arm64 for NanoVG. Lattice ships linux, linux-arm64, windows, macos, macos-arm64 (as Aether does).
  • Framebuffer reflection cached: NVGRenderer.beginFrame walked class hierarchies reflectively every frame to find directStateAccess/getFbo; the lookup now resolves once.
  • Render hook is lazy: the GameRenderer mixin skips the whole NanoVG frame when nothing subscribes to RenderEvent.Gui.
  • Mouse.Scaled.y used the width-based scale factor (copy-paste bug in Knit).
  • TickScheduler actually runs: Knit's was never wired to a tick source; Lattice registers it on TickEvent.Start on first use.
  • TimeScheduler.repeat honored initialDelayMillis only in one overload; the rewrite has one code path.
  • Animation preset leak: slideIn/bounceScale stored original geometry in global maps keyed by hashCode(), never cleaned; the bookkeeping now lives on the element and dies with it.
  • SvgImage releases its NVG image on destroy and no longer generates a random UUID cache key per recolor.

Threading model

The event bus uses CopyOnWriteArrayList/ConcurrentHashMap; posting is safe from any thread, but UI elements must only be touched on the render thread. TimeScheduler runs on a background pool (use it + display() / TickScheduler.post to hop back).