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

106 lines
5.1 KiB
Markdown

# 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).