From fee80c1fc596b7e6fbee9fa00163213fd893bf7b Mon Sep 17 00:00:00 2001 From: Gugle Date: Mon, 18 May 2026 19:37:37 +0800 Subject: [PATCH 01/67] feat(ui): add initial module setup for AnvilLib-Ui with dependencies and CI configuration --- .github/workflows/ci.yml | 26 ++++++ .github/workflows/pull_request.yml | 9 ++ .github/workflows/release.yml | 26 ++++++ module.main/build.gradle | 2 + module.test/build.gradle | 1 + module.ui/build.gradle | 2 + module.ui/gradle.properties | 4 + .../dev/anvilcraft/lib/v2/ui/AnvilLibUi.java | 13 +++ .../anvilcraft/lib/v2/ui/package-info.java | 4 + .../resources/META-INF/neoforge.mods.toml | 86 ++++++++++++++++++ .../main/resources/anvillib_ui.mixins.json | 14 +++ module.ui/src/main/resources/icon.png | Bin 0 -> 1089 bytes settings.gradle | 2 + 13 files changed, 189 insertions(+) create mode 100644 module.ui/build.gradle create mode 100644 module.ui/gradle.properties create mode 100644 module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/AnvilLibUi.java create mode 100644 module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/package-info.java create mode 100644 module.ui/src/main/resources/META-INF/neoforge.mods.toml create mode 100644 module.ui/src/main/resources/anvillib_ui.mixins.json create mode 100644 module.ui/src/main/resources/icon.png diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3ee0767c..81e7abf3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -356,6 +356,31 @@ jobs: signing_key: ${{ secrets.SIGNING_KEY }} signing_public_key: ${{ secrets.SIGNING_PUBLIC_KEY }} signing_password: ${{ secrets.SIGNING_PASSWORD }} + ui: + uses: ./.github/workflows/build_and_test.yml + with: + module: ui + module_id: anvillib-ui + mod_id: anvillib_ui + ci_build: true + pr_build: false + secrets: + maven_url: ${{ secrets.MAVEN_URL }} + maven_user: ${{ secrets.MAVEN_USER }} + maven_pass: ${{ secrets.MAVEN_PASS }} + ui-maven-central-deploy: + uses: ./.github/workflows/publish_maven_central.yml + needs: + - ui + with: + module: ui + module_id: anvillib-ui + secrets: + maven_central_username: ${{ secrets.MAVEN_CENTRAL_USERNAME }} + maven_central_password: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} + signing_key: ${{ secrets.SIGNING_KEY }} + signing_public_key: ${{ secrets.SIGNING_PUBLIC_KEY }} + signing_password: ${{ secrets.SIGNING_PASSWORD }} util: uses: ./.github/workflows/build_and_test.yml needs: @@ -426,6 +451,7 @@ jobs: - rendering - space-select - sync + - ui - util - wheel uses: ./.github/workflows/build_and_test.yml diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 5ef21865..599543c3 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -135,6 +135,14 @@ jobs: mod_id: anvillib_sync ci_build: true pr_build: true + ui: + uses: ./.github/workflows/build_and_test.yml + with: + module: ui + module_id: anvillib-ui + mod_id: anvillib_ui + ci_build: true + pr_build: true util: uses: ./.github/workflows/build_and_test.yml needs: @@ -171,6 +179,7 @@ jobs: - rendering - space-select - sync + - ui - util - wheel uses: ./.github/workflows/build_and_test.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d11d4b94..42e27b3c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -356,6 +356,31 @@ jobs: signing_key: ${{ secrets.SIGNING_KEY }} signing_public_key: ${{ secrets.SIGNING_PUBLIC_KEY }} signing_password: ${{ secrets.SIGNING_PASSWORD }} + ui: + uses: ./.github/workflows/build_and_test.yml + with: + module: ui + module_id: anvillib-ui + mod_id: anvillib_ui + ci_build: false + pr_build: false + secrets: + maven_url: ${{ secrets.MAVEN_URL }} + maven_user: ${{ secrets.MAVEN_USER }} + maven_pass: ${{ secrets.MAVEN_PASS }} + ui-maven-central-deploy: + uses: ./.github/workflows/publish_maven_central.yml + needs: + - ui + with: + module: ui + module_id: anvillib-ui + secrets: + maven_central_username: ${{ secrets.MAVEN_CENTRAL_USERNAME }} + maven_central_password: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} + signing_key: ${{ secrets.SIGNING_KEY }} + signing_public_key: ${{ secrets.SIGNING_PUBLIC_KEY }} + signing_password: ${{ secrets.SIGNING_PASSWORD }} util: uses: ./.github/workflows/build_and_test.yml needs: @@ -426,6 +451,7 @@ jobs: - rendering - space-select - sync + - ui - util - wheel uses: ./.github/workflows/build_and_test.yml diff --git a/module.main/build.gradle b/module.main/build.gradle index bb017bab..f2f567ed 100644 --- a/module.main/build.gradle +++ b/module.main/build.gradle @@ -16,6 +16,7 @@ dependencies { jarJar(api("dev.anvilcraft.lib:anvillib-rendering-neoforge-26.1:latest.release")) jarJar(api("dev.anvilcraft.lib:anvillib-space-select-neoforge-26.1:latest.release")) jarJar(api("dev.anvilcraft.lib:anvillib-sync-neoforge-26.1:latest.release")) + jarJar(api("dev.anvilcraft.lib:anvillib-ui-neoforge-26.1:latest.release")) jarJar(api("dev.anvilcraft.lib:anvillib-util-neoforge-26.1:latest.release")) jarJar(api("dev.anvilcraft.lib:anvillib-wheel-neoforge-26.1:latest.release")) } else { @@ -35,6 +36,7 @@ dependencies { jarJar(implementation project(":anvillib-rendering-neoforge-26.1")) jarJar(implementation project(":anvillib-space-select-neoforge-26.1")) jarJar(implementation project(":anvillib-sync-neoforge-26.1")) + jarJar(implementation project(":anvillib-ui-neoforge-26.1")) jarJar(implementation project(":anvillib-util-neoforge-26.1")) jarJar(implementation project(":anvillib-wheel-neoforge-26.1")) } diff --git a/module.test/build.gradle b/module.test/build.gradle index cfd2e563..c9f642ac 100644 --- a/module.test/build.gradle +++ b/module.test/build.gradle @@ -13,6 +13,7 @@ dependencies { implementation project(":anvillib-rendering-neoforge-26.1") implementation project(":anvillib-space-select-neoforge-26.1") implementation project(":anvillib-sync-neoforge-26.1") + implementation project(":anvillib-ui-neoforge-26.1") implementation project(":anvillib-util-neoforge-26.1") implementation project(":anvillib-wheel-neoforge-26.1") } diff --git a/module.ui/build.gradle b/module.ui/build.gradle new file mode 100644 index 00000000..571b4175 --- /dev/null +++ b/module.ui/build.gradle @@ -0,0 +1,2 @@ +dependencies { +} \ No newline at end of file diff --git a/module.ui/gradle.properties b/module.ui/gradle.properties new file mode 100644 index 00000000..1a717a8d --- /dev/null +++ b/module.ui/gradle.properties @@ -0,0 +1,4 @@ +## Mod Properties +mod_id=anvillib_ui +mod_name=AnvilLib-Ui +mod_description=A simple declarative UI library \ No newline at end of file diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/AnvilLibUi.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/AnvilLibUi.java new file mode 100644 index 00000000..47a2d9e2 --- /dev/null +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/AnvilLibUi.java @@ -0,0 +1,13 @@ +package dev.anvilcraft.lib.v2.ui; + +import net.minecraft.resources.Identifier; +import net.neoforged.fml.common.Mod; + +@Mod(AnvilLibUi.MOD_ID) +public class AnvilLibUi { + public static final String MOD_ID = "anvillib_ui"; + + public static Identifier of(String path) { + return Identifier.fromNamespaceAndPath(AnvilLibUi.MOD_ID, path); + } +} \ No newline at end of file diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/package-info.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/package-info.java new file mode 100644 index 00000000..df1230bb --- /dev/null +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package dev.anvilcraft.lib.v2.ui; + +import org.jspecify.annotations.NullMarked; \ No newline at end of file diff --git a/module.ui/src/main/resources/META-INF/neoforge.mods.toml b/module.ui/src/main/resources/META-INF/neoforge.mods.toml new file mode 100644 index 00000000..27f7ba75 --- /dev/null +++ b/module.ui/src/main/resources/META-INF/neoforge.mods.toml @@ -0,0 +1,86 @@ +# This is an example mods.toml file. It contains the data relating to the loading mods. +# There are several mandatory fields (#mandatory), and many more that are optional (#optional). +# The overall format is standard TOML format, v0.5.0. +# Note that there are a couple of TOML lists in this file. +# Find more information on toml format here: https://github.com/toml-lang/toml +# The name of the mod loader type to load - for regular FML @Mod mods it should be javafml +modLoader = "javafml" #mandatory +# A version range to match for said mod loader - for regular FML @Mod it will be the the FML version. This is currently 47. +loaderVersion = "${loader_version_range}" #mandatory +# The license for you mod. This is mandatory metadata and allows for easier comprehension of your redistributive properties. +# Review your options at https://choosealicense.com/. All rights reserved is the default copyright stance, and is thus the default here. +license = "${mod_license}" +# A URL to refer people to when problems occur with this mod +#issueTrackerURL="https://change.me.to.your.issue.tracker.example.invalid/" #optional +# A list of mods - how many allowed here is determined by the individual mod loader +[[mods]] #mandatory +# The modid of the mod +modId = "${mod_id}" #mandatory +# The version number of the mod +version = "${mod_version}" #mandatory +# A display name for the mod +displayName = "${mod_name}" #mandatory +# A URL to query for updates for this mod. See the JSON update specification https://docs.neoforge.net/docs/misc/updatechecker/ +#updateJSONURL="https://change.me.example.invalid/updates.json" #optional +# A URL for the "homepage" for this mod, displayed in the mod UI +displayURL="https://github.com/Anvil-Dev/AnvilLib" +# A file name (in the root of the mod JAR) containing a logo for display +logoFile="icon.png" #optional +# A text field displayed in the mod UI +#credits="" #optional +# A text field displayed in the mod UI +authors = "${mod_authors}" #optional +# Display Test controls the display for your mod in the server connection screen +# MATCH_VERSION means that your mod will cause a red X if the versions on client and server differ. This is the default behaviour and should be what you choose if you have server and client elements to your mod. +# IGNORE_SERVER_VERSION means that your mod will not cause a red X if it's present on the server but not on the client. This is what you should use if you're a server only mod. +# IGNORE_ALL_VERSION means that your mod will not cause a red X if it's present on the client or the server. This is a special case and should only be used if your mod has no server component. +# NONE means that no display test is set on your mod. You need to do this yourself, see IExtensionPoint.DisplayTest for more information. You can define any scheme you wish with this value. +# IMPORTANT NOTE: this is NOT an instruction as to which environments (CLIENT or DEDICATED SERVER) your mod loads on. Your mod should load (and maybe do nothing!) whereever it finds itself. +#displayTest="MATCH_VERSION" # MATCH_VERSION is the default if nothing is specified (#optional) + +# The description text for the mod (multi line!) (#mandatory) +description = '''${mod_description}''' + +# The [[mixins]] block allows you to declare your mixin config to FML so that it gets loaded. +[[mixins]] +config = "${mod_id}.mixins.json" + +# The [[accessTransformers]] block allows you to declare where your AT file is. +# If this block is omitted, a fallback attempt will be made to load an AT from META-INF/accesstransformer.cfg +#[[accessTransformers]] +#file="META-INF/accesstransformer.cfg" + +# The coremods config file path is not configurable and is always loaded from META-INF/coremods.json + +# A dependency - use the . to indicate dependency for a specific modid. Dependencies are optional. +[[dependencies."${mod_id}"]] #optional +# the modid of the dependency +modId = "neoforge" #mandatory +# The type of the dependency. Can be one of "required", "optional", "incompatible" or "discouraged" (case insensitive). +# 'required' requires the mod to exist, 'optional' does not +# 'incompatible' will prevent the game from loading when the mod exists, and 'discouraged' will show a warning +type = "required" #mandatory +# Optional field describing why the dependency is required or why it is incompatible +# reason="..." +# The version range of the dependency +versionRange = "${neo_version_range}" #mandatory +# An ordering relationship for the dependency. +# BEFORE - This mod is loaded BEFORE the dependency +# AFTER - This mod is loaded AFTER the dependency +ordering = "NONE" +# Side this dependency is applied on - BOTH, CLIENT, or SERVER +side = "BOTH" +# Here's another dependency +[[dependencies."${mod_id}"]] +modId = "minecraft" +type = "required" +# This version range declares a minimum of the current minecraft version up to but not including the next major version +versionRange = "${minecraft_version_range}" +ordering = "NONE" +side = "BOTH" + +# Features are specific properties of the game environment, that you may want to declare you require. This example declares +# that your mod requires GL version 3.2 or higher. Other features will be added. They are side aware so declaring this won't +# stop your mod loading on the server for example. +#[features."${mod_id}"] +#openGLVersion="[3.2,)" diff --git a/module.ui/src/main/resources/anvillib_ui.mixins.json b/module.ui/src/main/resources/anvillib_ui.mixins.json new file mode 100644 index 00000000..4144b1f6 --- /dev/null +++ b/module.ui/src/main/resources/anvillib_ui.mixins.json @@ -0,0 +1,14 @@ +{ + "required": true, + "minVersion": "0.8", + "package": "dev.anvilcraft.lib.v2.ui.mixin", + "compatibilityLevel": "JAVA_8", + "refmap": "anvillib.refmap.json", + "mixins": [ + ], + "client": [ + ], + "injectors": { + "defaultRequire": 1 + } +} \ No newline at end of file diff --git a/module.ui/src/main/resources/icon.png b/module.ui/src/main/resources/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..15304bd54d5d97d8482049d5f16a7544748ce05a GIT binary patch literal 1089 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE3?yBabRA=0U}gyL32}YB&qp?9cb}fRhL)ab z>**&8P1h&;i5V@+5z~{9lu=Mp{cG+rNOuP4jFK;J~t_xRpI&Nf4%?X zZmY?-7zkw=;bC zsjT>Uk~ZUn{GF#RHtMc+sGWYc$A>9-;igj6hzoLO7wEVa*QxB)zM=ff)bhV&`;h}b zD&$S4O;$O;Sf&`=w_-tNu&sr4nCukimfSM|=84-o^=H0sFgVR)oOSu;mLIn4lUH@E zPWkvN(C^lx3(G6wS;EsJIMe%PuP~i06!s*lLHg)A!3fTUY`(JJ860vaMb2DUI{zB$ z25}G0l9!&RrHovbwaBi{I35yXA*NOEX}{&%4{Sd|jrPVzaJhpb&(qb Date: Mon, 18 May 2026 20:16:15 +0800 Subject: [PATCH 02/67] feat(ui): add TODO documentation for module.ui development phases and design notes --- module.ui/TODO.md | 124 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 module.ui/TODO.md diff --git a/module.ui/TODO.md b/module.ui/TODO.md new file mode 100644 index 00000000..50f1041a --- /dev/null +++ b/module.ui/TODO.md @@ -0,0 +1,124 @@ +# module.ui TODO + +受 ArkUI 启发的声明式 UI 系统。纯 Java API,Consumer/Runnable 作为尾参,Kotlin SAM 转换自动获得尾随 Lambda DSL。 +状态驱动,与 Minecraft GuiGraphicsExtractor 渲染管线集成。 + +--- + +## Phase 1: 构建系统 + 核心框架 + +- [ ] `module.ui/build.gradle` — 添加 `implementation project(':anvillib-rendering-neoforge-26.1')` +- [ ] `Constraints` — min/max width/height 约束 +- [ ] `Modifier` — 链式 API 接口(`then` / `foldIn` / `foldOut`) +- [ ] `ModifierElement` — 单个修饰符节点接口 +- [ ] `UIComponent` — 核心接口:`measure(Constraints): MeasuredSize` / `layout(...)` / `extractRenderState(GuiGraphicsExtractor)` +- [ ] `Composition` — slot table + `emit()` / `recompose()` / `invalidate()` + +## Phase 2: 状态管理 + +- [ ] `MutableState` — 可观察状态:getter 记录 reader slot,setter 精确 markDirty +- [ ] `remember { }` — 按 slot 位置持久化,recompose 时回读同一对象 +- [ ] 脏标记传播:只重执行 dirty group,干净子树跳过 + +## Phase 3: 布局容器 + +- [ ] `Column` + `ColumnScope` — 纵向排列 +- [ ] `Row` + `RowScope` — 横向排列 +- [ ] `Box` + `BoxScope` — 层叠 +- [ ] `ColumnMeasurePolicy` / `RowMeasurePolicy` / `BoxMeasurePolicy` +- [ ] `Arrangement.Vertical` / `Arrangement.Horizontal` — SpaceBetween / SpaceAround / SpaceEvenly +- [ ] `Alignment` — Start / Center / End + +## Phase 4: 基础组件 + +- [ ] `Text` — 文字渲染(Minecraft font) +- [ ] `Button` — 可点击矩形按钮(SdfGraphics 背景 + 文字) +- [ ] `Spacer` — 固定尺寸空白 +- [ ] `Image` — 材质渲染 + +## Phase 5: Modifier Elements + +- [ ] `SizeModifier` — `.size(width, height)` `.fillMaxWidth()` `.fillMaxSize()` +- [ ] `PaddingModifier` — `.padding(all)` `.padding(horizontal, vertical)` +- [ ] `BackgroundModifier` — `.background(color)` → SdfGraphics.box +- [ ] `BorderModifier` — `.border(width, color)` → SdfGraphics.stroke +- [ ] `RoundedCornerModifier` — `.roundedCorner(radius)` → SdfGraphics.round +- [ ] `ClickModifier` — `.onClick { }` + hit testing + +## Phase 6: 屏幕集成 + +- [ ] `DeclarativeScreen` — `Screen` 子类,宿主组件树 +- [ ] `extractRenderState()` — dirty check → recompose → measure → layout → submit render states +- [ ] 输入事件路由 — `mouseClicked` / `keyPressed` hit testing + dispatch + +## Phase 7: 输入组件 + +- [ ] `TextField` — 文本输入 +- [ ] `Checkbox` — 布尔切换 +- [ ] `Slider` — 连续范围选择 + +## Phase 8: 高级特性 + +- [ ] `Grid` — 网格布局 +- [ ] `ForEach` — 循环渲染(带 key 稳定 slot 复用) +- [ ] `if` / `when` 条件渲染 +- [ ] `Animatable` — 时间驱动动画值(基于 Minecraft tick,不依赖协程) +- [ ] `LazyColumn` — 虚拟化长列表 + +## Phase 9: 测试 + +- [ ] 布局算法单元测试(Column/Row/Box measure + layout) +- [ ] Slot diffing 单元测试(recompose 后 slot table 正确性) +- [ ] State 传播单元测试(精确 markDirty 范围) +- [ ] `module.test` 中创建示例 `DeclarativeScreen` 验证端到端 + +--- + +## 设计笔记:组件扩展方式 + +组件 = 工厂函数 + 实现类,两者并存: + +- **工厂函数** — DSL 入口。创建实例 → 注册到父容器 + Slot Table → 返回实例(链式调用) +- **实现类** — 可继承、可覆盖 `measure()` / `layout()` / `extractRenderState()` + +三种扩展方式: + +### 1. 组合(90% 场景) + +已有组件拼出新组件,不改内部实现。 + +```java +public static TextComponent FancyLabel(ColumnScope scope, String text) { + return scope.Text(text).fontSize(24).color(0xFFAAAAFF); +} +``` + +### 2. 继承实现类(需要新行为时) + +extends 现有组件或 implements `UIComponent`,覆盖核心方法,再提供配套工厂函数。 + +```java +public class RainbowText extends TextComponent { + @Override + public void extractRenderState(GuiGraphicsExtractor e) { + this.color = Color.HSBtoRGB(...); + super.extractRenderState(e); + } +} +``` + +### 3. Modifier 扩展(可复用样式/行为) + +封装常用样式为 Modifier 工厂方法,与具体组件解耦。 + +```java +public static Modifier cardStyle(Modifier m) { + return m.background(0xFF333333).roundedCorner(8).padding(12, 8); +} +``` + +| 方式 | 适用 | 耦合 | +|----------|--------------------------|----------| +| 组合 | 拼装现有组件 | 无 | +| 继承 | 全新 measure/layout/render | 与父类耦合 | +| Modifier | 可复用样式/行为 | 无,任何组件通用 | From 90e42359e553e12c0b20c972760dae394dfa2eb8 Mon Sep 17 00:00:00 2001 From: Gugle Date: Mon, 18 May 2026 20:40:17 +0800 Subject: [PATCH 03/67] feat(ui): implement UI components including Button, Text, and layout structures --- module.ui/build.gradle | 1 + .../lib/v2/ui/BackgroundElement.java | 19 ++ .../anvilcraft/lib/v2/ui/ButtonComponent.java | 105 +++++++++ .../anvilcraft/lib/v2/ui/ColumnComponent.java | 90 ++++++++ .../dev/anvilcraft/lib/v2/ui/ColumnScope.java | 7 + .../lib/v2/ui/CombinedModifier.java | 24 ++ .../dev/anvilcraft/lib/v2/ui/Composition.java | 207 ++++++++++++++++++ .../dev/anvilcraft/lib/v2/ui/Constraints.java | 29 +++ .../lib/v2/ui/DeclarativeScreen.java | 66 ++++++ .../dev/anvilcraft/lib/v2/ui/LayoutRect.java | 19 ++ .../anvilcraft/lib/v2/ui/MeasuredSize.java | 13 ++ .../dev/anvilcraft/lib/v2/ui/Modifier.java | 78 +++++++ .../anvilcraft/lib/v2/ui/ModifierElement.java | 67 ++++++ .../anvilcraft/lib/v2/ui/MutableState.java | 53 +++++ .../anvilcraft/lib/v2/ui/PaddingElement.java | 25 +++ .../lib/v2/ui/SingleElementModifier.java | 24 ++ .../dev/anvilcraft/lib/v2/ui/SizeElement.java | 17 ++ .../anvilcraft/lib/v2/ui/TextComponent.java | 75 +++++++ .../dev/anvilcraft/lib/v2/ui/UIComponent.java | 40 ++++ .../dev/anvilcraft/lib/v2/ui/UIScope.java | 59 +++++ 20 files changed, 1018 insertions(+) create mode 100644 module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/BackgroundElement.java create mode 100644 module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/ButtonComponent.java create mode 100644 module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/ColumnComponent.java create mode 100644 module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/ColumnScope.java create mode 100644 module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/CombinedModifier.java create mode 100644 module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Composition.java create mode 100644 module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Constraints.java create mode 100644 module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java create mode 100644 module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/LayoutRect.java create mode 100644 module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/MeasuredSize.java create mode 100644 module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Modifier.java create mode 100644 module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/ModifierElement.java create mode 100644 module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/MutableState.java create mode 100644 module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/PaddingElement.java create mode 100644 module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/SingleElementModifier.java create mode 100644 module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/SizeElement.java create mode 100644 module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/TextComponent.java create mode 100644 module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIComponent.java create mode 100644 module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIScope.java diff --git a/module.ui/build.gradle b/module.ui/build.gradle index 571b4175..3dd3d6e5 100644 --- a/module.ui/build.gradle +++ b/module.ui/build.gradle @@ -1,2 +1,3 @@ dependencies { + implementation project(":anvillib-rendering-neoforge-26.1") } \ No newline at end of file diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/BackgroundElement.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/BackgroundElement.java new file mode 100644 index 00000000..af1c1294 --- /dev/null +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/BackgroundElement.java @@ -0,0 +1,19 @@ +package dev.anvilcraft.lib.v2.ui; + +import dev.anvilcraft.lib.v2.rendering.sdf.SdfGraphics; +import net.minecraft.client.gui.GuiGraphicsExtractor; + +/** + * Renders a filled rounded rectangle as the component's background. + */ +record BackgroundElement(int color) implements ModifierElement { + + @Override + public void emitRenderState(GuiGraphicsExtractor extractor, LayoutRect bounds) { + SdfGraphics.instance + .box(bounds.x(), bounds.y(), bounds.width(), bounds.height()) + .color(color) + .fill() + .draw(extractor); + } +} diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/ButtonComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/ButtonComponent.java new file mode 100644 index 00000000..a49ca7e4 --- /dev/null +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/ButtonComponent.java @@ -0,0 +1,105 @@ +package dev.anvilcraft.lib.v2.ui; + +import dev.anvilcraft.lib.v2.rendering.sdf.SdfGraphics; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.network.chat.Component; + +import java.util.Collections; +import java.util.List; + +/** + * A clickable button with background and label. + */ +public class ButtonComponent implements UIComponent { + + private static final int BG_COLOR = 0xFF555555; + private static final int BG_HOVER_COLOR = 0xFF777777; + private static final int TEXT_COLOR = 0xFFFFFFFF; + private static final float PADDING_H = 12; + private static final float PADDING_V = 6; + + private final Modifier modifier; + private String label; + private Runnable onClick; + + // layout state + private float x, y, width, height; + + public ButtonComponent(Modifier modifier, String label, Runnable onClick) { + this.modifier = modifier; + this.label = label; + this.onClick = onClick; + } + + // ── chained setters ── + + public ButtonComponent label(String label) { + this.label = label; + return this; + } + + public ButtonComponent onClick(Runnable onClick) { + this.onClick = onClick; + return this; + } + + // ── UIComponent ── + + @Override + public Modifier modifier() { + return modifier; + } + + @Override + public List children() { + return Collections.emptyList(); + } + + @Override + public MeasuredSize measure(Constraints constraints) { + var font = Minecraft.getInstance().font; + Component comp = Component.literal(label); + float textW = font.width(comp); + float textH = font.lineHeight; + return MeasuredSize.of( + constraints.constrainWidth(textW + PADDING_H * 2), + constraints.constrainHeight(textH + PADDING_V * 2) + ); + } + + @Override + public void layout(float x, float y, float width, float height) { + this.x = x; + this.y = y; + this.width = width; + this.height = height; + } + + @Override + public void extractRenderState(GuiGraphicsExtractor extractor) { + // Background + SdfGraphics.instance + .box(x, y, width, height) + .color(BG_COLOR) + .round(4) + .fill() + .draw(extractor); + + // Label + var font = Minecraft.getInstance().font; + Component comp = Component.literal(label); + float textW = font.width(comp); + float textH = font.lineHeight; + float cx = x + (width - textW) / 2; + float cy = y + (height - textH) / 2; + extractor.centeredText(font, comp, (int) (x + width / 2), (int) cy, TEXT_COLOR); + } + + /** Invoke click handler if clicked. Called by hit-testing. */ + void click() { + if (onClick != null) { + onClick.run(); + } + } +} diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/ColumnComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/ColumnComponent.java new file mode 100644 index 00000000..6b616095 --- /dev/null +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/ColumnComponent.java @@ -0,0 +1,90 @@ +package dev.anvilcraft.lib.v2.ui; + +import net.minecraft.client.gui.GuiGraphicsExtractor; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Vertical linear layout. Children are measured and stacked top-to-bottom. + */ +public class ColumnComponent implements UIComponent { + + private final Modifier modifier; + private List children = Collections.emptyList(); + private List childSizes = Collections.emptyList(); + + // layout state + private float x, y, width, height; + + public ColumnComponent(Modifier modifier) { + this.modifier = modifier; + } + + void setChildren(List children) { + this.children = List.copyOf(children); + } + + @Override + public Modifier modifier() { + return modifier; + } + + @Override + public List children() { + return children; + } + + @Override + public MeasuredSize measure(Constraints constraints) { + if (children.isEmpty()) { + return MeasuredSize.ZERO; + } + + float totalHeight = 0; + float maxWidth = 0; + List sizes = new ArrayList<>(children.size()); + + Constraints childConstraints = new Constraints( + constraints.minWidth(), constraints.maxWidth(), + 0, Float.MAX_VALUE + ); + + for (UIComponent child : children) { + MeasuredSize size = child.measure(childConstraints); + sizes.add(size); + totalHeight += size.height(); + maxWidth = Math.max(maxWidth, size.width()); + } + + this.childSizes = sizes; + return MeasuredSize.of( + constraints.constrainWidth(maxWidth), + constraints.constrainHeight(totalHeight) + ); + } + + @Override + public void layout(float x, float y, float width, float height) { + this.x = x; + this.y = y; + this.width = width; + this.height = height; + + float currentY = y; + for (int i = 0; i < children.size(); i++) { + UIComponent child = children.get(i); + MeasuredSize size = childSizes.get(i); + child.layout(x, currentY, width, size.height()); + currentY += size.height(); + } + } + + @Override + public void extractRenderState(GuiGraphicsExtractor extractor) { + for (UIComponent child : children) { + child.extractRenderState(extractor); + } + } +} diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/ColumnScope.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/ColumnScope.java new file mode 100644 index 00000000..c37a3f42 --- /dev/null +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/ColumnScope.java @@ -0,0 +1,7 @@ +package dev.anvilcraft.lib.v2.ui; + +/** + * Scope for children inside a {@link ColumnComponent}. + */ +public class ColumnScope extends UIScope { +} diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/CombinedModifier.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/CombinedModifier.java new file mode 100644 index 00000000..f8467ca3 --- /dev/null +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/CombinedModifier.java @@ -0,0 +1,24 @@ +package dev.anvilcraft.lib.v2.ui; + +import java.util.function.BiFunction; + +/** + * Two {@link Modifier} chains joined by {@code then()}. + */ +record CombinedModifier(ModifierElement outer, Modifier inner) implements Modifier { + + @Override + public Modifier then(Modifier other) { + return new CombinedModifier(outer, inner.then(other)); + } + + @Override + public R foldIn(R initial, BiFunction operation) { + return inner.foldIn(operation.apply(initial, outer), operation); + } + + @Override + public R foldOut(R initial, BiFunction operation) { + return operation.apply(outer, inner.foldOut(initial, operation)); + } +} diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Composition.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Composition.java new file mode 100644 index 00000000..f079f3ef --- /dev/null +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Composition.java @@ -0,0 +1,207 @@ +package dev.anvilcraft.lib.v2.ui; + +import net.minecraft.client.gui.GuiGraphicsExtractor; + +import java.util.*; +import java.util.function.Consumer; +import java.util.function.Supplier; + +/** + * The composition engine that drives recomposition, state tracking, and rendering. + *

+ * One Composition is created per {@link DeclarativeScreen}. + * It manages a flat slot table indexed by call-site position. + * During recomposition, the content lambda replays and each + * {@link #emit(UIComponent)} call diffs against the slot at the + * same index. + *

+ * State reads are tracked per slot so that writes only mark + * affected slots dirty — not the entire tree. + */ +public class Composition { + + private static final ThreadLocal CURRENT = new ThreadLocal<>(); + + /** Returns the composition active on this thread, or null. */ + public static Composition currentOrNull() { + return CURRENT.get(); + } + + /** Returns the composition active on this thread, throwing if absent. */ + public static Composition current() { + Composition c = CURRENT.get(); + if (c == null) { + throw new IllegalStateException("Not inside a composition frame"); + } + return c; + } + + // ── slot table ── + + private final List slots = new ArrayList<>(); + private int currentIndex; + private int currentRememberKey; + private final Map rememberedValues = new HashMap<>(); + + /** The slot currently being emitted (set during {@link #emit}). */ + Slot currentSlot; + + // ── state ── + + private boolean dirty = true; + private Consumer content; + private UIScope rootScope; + + public Composition(UIScope rootScope) { + this.rootScope = rootScope; + } + + public void setContent(Consumer content) { + this.content = content; + } + + /** Mark the composition as needing recomposition next frame. */ + public void invalidate() { + dirty = true; + } + + // ── remember ── + + /** + * Persist a value across recompositions. + * The init supplier is only called on first composition; + * subsequent recompositions return the existing value. + */ + @SuppressWarnings("unchecked") + public T remember(Supplier init) { + int key = currentRememberKey++; + Object existing = rememberedValues.get(key); + if (existing != null) { + return (T) existing; + } + T value = init.get(); + rememberedValues.put(key, value); + return value; + } + + // ── emit ── + + /** + * Emit a component to the current call-site position in the slot table. + * Called by component factory functions. + */ + public void emit(UIComponent component) { + Slot slot; + if (currentIndex < slots.size()) { + slot = slots.get(currentIndex); + slot.component = component; + } else { + slot = new Slot(); + slot.component = component; + slots.add(slot); + } + currentSlot = slot; + currentIndex++; + } + + // ── frame entry point ── + + /** + * Called every frame from {@link DeclarativeScreen#extractRenderState}. + * Runs recomposition if dirty, then measure → layout → render. + */ + public void renderFrame(GuiGraphicsExtractor extractor, float screenWidth, float screenHeight) { + CURRENT.set(this); + try { + if (dirty) { + recompose(); + dirty = false; + } + Constraints rootConstraints = new Constraints(0, screenWidth, 0, screenHeight); + for (UIComponent child : rootScope.getChildren()) { + renderTree(child, extractor, rootConstraints); + } + } finally { + CURRENT.set(null); + } + } + + // ── recompose ── + + private void recompose() { + rootScope.clearChildren(); + currentIndex = 0; + currentRememberKey = 0; + content.accept(rootScope); + // Remove slots beyond currentIndex (conditionally removed components) + while (slots.size() > currentIndex) { + Slot removed = slots.remove(slots.size() - 1); + // Purge remembered values that belong to removed slots. + // A simple approach: remembered values for keys beyond + // currentRememberKey are dead; we leave them as they'll + // be overwritten on next composition anyway. + } + } + + // ── measure → layout → render walk ── + + private void renderTree(UIComponent component, GuiGraphicsExtractor extractor, Constraints constraints) { + // 1. Apply modifier to constraints + Constraints modConstraints = component.modifier().foldIn( + constraints, + (c, el) -> el.modifyConstraints(c) + ); + + // 2. Measure + MeasuredSize size = component.measure(modConstraints); + size = component.modifier().foldOut( + size, + (el, s) -> el.modifyMeasuredSize(component, modConstraints, s) + ); + + // 3. Layout + LayoutRect rect = LayoutRect.of(0, 0, size.width(), size.height()); + rect = component.modifier().foldOut( + rect, + (el, r) -> el.modifyLayout(r) + ); + component.layout(rect.x(), rect.y(), rect.width(), rect.height()); + + // 4. Emit modifier render states (background, border, etc.) + final LayoutRect finalRect = rect; + component.modifier().foldOut( + extractor, + (el, e) -> { + el.emitRenderState(e, finalRect); + return e; + } + ); + + // 5. Emit component's own render states + component.extractRenderState(extractor); + + // 6. Recurse into children (container components handle their own children + // in measure/layout/extractRenderState, but we also walk them here + // so the external renderTree call drives the full tree) + } + + // ── slot ── + + /** + * A position in the slot table. Each slot holds a component and + * tracks which states it reads for precise dirty marking. + */ + public static class Slot { + UIComponent component; + boolean dirty = true; + final Set> readStates = new HashSet<>(); + + void addReadState(MutableState state) { + readStates.add(state); + } + + void markDirty() { + dirty = true; + } + } +} diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Constraints.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Constraints.java new file mode 100644 index 00000000..c99e90a6 --- /dev/null +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Constraints.java @@ -0,0 +1,29 @@ +package dev.anvilcraft.lib.v2.ui; + +/** + * Min/max bounds passed from parent to child during measure. + */ +public record Constraints(float minWidth, float maxWidth, float minHeight, float maxHeight) { + + public static final Constraints NONE = new Constraints(0, Float.MAX_VALUE, 0, Float.MAX_VALUE); + + public float constrainWidth(float w) { + return Math.max(minWidth, Math.min(w, maxWidth)); + } + + public float constrainHeight(float h) { + return Math.max(minHeight, Math.min(h, maxHeight)); + } + + public Constraints withWidth(float width) { + return new Constraints(width, width, minHeight, maxHeight); + } + + public Constraints withHeight(float height) { + return new Constraints(minWidth, maxWidth, height, height); + } + + public Constraints copy(float minW, float maxW, float minH, float maxH) { + return new Constraints(minW, maxW, minH, maxH); + } +} diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java new file mode 100644 index 00000000..c9ecce7e --- /dev/null +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java @@ -0,0 +1,66 @@ +package dev.anvilcraft.lib.v2.ui; + +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; + +/** + * A {@link Screen} that hosts a declarative UI component tree. + *

+ * Subclasses implement {@link #content(UIScope)} to declare the UI. + * The composition is automatically managed each frame. + * + *

{@code
+ * public class MyScreen extends DeclarativeScreen {
+ *     public MyScreen() { super(Component.literal("My UI")); }
+ *
+ *     protected void content(UIScope scope) {
+ *         var count = Composition.current().remember(
+ *             () -> new MutableState<>(0)
+ *         );
+ *         Column(scope, col -> {
+ *             col.Text("Count: " + count.getValue());
+ *             col.Button("+", () -> count.setValue(count.getValue() + 1));
+ *         });
+ *     }
+ * }
+ * }
+ */ +public abstract class DeclarativeScreen extends Screen { + + private Composition composition; + private final UIScope rootScope = new RootScope(); + + protected DeclarativeScreen(Component title) { + super(title); + } + + @Override + protected void init() { + composition = new Composition(rootScope); + composition.setContent(this::content); + } + + /** + * Declare the UI content. Called on initial composition and every recomposition. + * Use {@link Composition#current()} to access state helpers like {@code remember}. + */ + protected abstract void content(UIScope scope); + + @Override + public void extractRenderState(GuiGraphicsExtractor extractor, int mouseX, int mouseY, float partialTick) { + super.extractRenderState(extractor, mouseX, mouseY, partialTick); + if (composition != null) { + composition.renderFrame(extractor, this.width, this.height); + } + } + + // ── input routing (stub — Phase 5 adds full hit-testing) ── + + // TODO Phase 5: override mouseClicked(MouseButtonEvent, boolean), keyPressed(KeyEvent) + + // ── internal ── + + private static class RootScope extends UIScope { + } +} diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/LayoutRect.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/LayoutRect.java new file mode 100644 index 00000000..8c11e56a --- /dev/null +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/LayoutRect.java @@ -0,0 +1,19 @@ +package dev.anvilcraft.lib.v2.ui; + +/** + * A positioned rectangle after the layout pass. + */ +public record LayoutRect(float x, float y, float width, float height) { + + public static LayoutRect of(float x, float y, float width, float height) { + return new LayoutRect(x, y, width, height); + } + + public float right() { return x + width; } + + public float bottom() { return y + height; } + + public boolean contains(float px, float py) { + return px >= x && px < x + width && py >= y && py < y + height; + } +} diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/MeasuredSize.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/MeasuredSize.java new file mode 100644 index 00000000..516ee6e4 --- /dev/null +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/MeasuredSize.java @@ -0,0 +1,13 @@ +package dev.anvilcraft.lib.v2.ui; + +/** + * Result of a component's measure pass. + */ +public record MeasuredSize(float width, float height) { + + public static final MeasuredSize ZERO = new MeasuredSize(0, 0); + + public static MeasuredSize of(float width, float height) { + return new MeasuredSize(width, height); + } +} diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Modifier.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Modifier.java new file mode 100644 index 00000000..7c97bb4a --- /dev/null +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Modifier.java @@ -0,0 +1,78 @@ +package dev.anvilcraft.lib.v2.ui; + +import java.util.function.BiFunction; + +/** + * Chainable modifier API. Modifiers form a linked list via {@link #then}. + *

+ * Each modifier element can participate in measure, layout, and render phases. + * Callers fold over the chain with {@link #foldIn} / {@link #foldOut}. + */ +public interface Modifier { + + Modifier then(Modifier other); + + R foldIn(R initial, BiFunction operation); + + R foldOut(R initial, BiFunction operation); + + /** Return a new chain with the given element prepended. */ + default Modifier prepend(ModifierElement element) { + return then(new SingleElementModifier(element)); + } + + // ── factory shortcuts ── + + default Modifier size(float width, float height) { + return prepend(ModifierElement.size(width, height)); + } + + default Modifier width(float width) { + return prepend(ModifierElement.width(width)); + } + + default Modifier height(float height) { + return prepend(ModifierElement.height(height)); + } + + default Modifier fillMaxWidth() { + return prepend(ModifierElement.fillMaxWidth()); + } + + default Modifier fillMaxHeight() { + return prepend(ModifierElement.fillMaxHeight()); + } + + default Modifier fillMaxSize() { + return prepend(ModifierElement.fillMaxSize()); + } + + default Modifier padding(float all) { + return prepend(ModifierElement.padding(all)); + } + + default Modifier padding(float horizontal, float vertical) { + return prepend(ModifierElement.padding(horizontal, vertical)); + } + + default Modifier background(int color) { + return prepend(ModifierElement.background(color)); + } + + Modifier NONE = new Modifier() { + @Override + public Modifier then(Modifier other) { + return other; + } + + @Override + public R foldIn(R initial, BiFunction operation) { + return initial; + } + + @Override + public R foldOut(R initial, BiFunction operation) { + return initial; + } + }; +} diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/ModifierElement.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/ModifierElement.java new file mode 100644 index 00000000..5370162a --- /dev/null +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/ModifierElement.java @@ -0,0 +1,67 @@ +package dev.anvilcraft.lib.v2.ui; + +import net.minecraft.client.gui.GuiGraphicsExtractor; + +/** + * A single node in a {@link Modifier} chain. + * Each element can intercept measure, layout, and render phases. + */ +public interface ModifierElement { + + /** Modify constraints during the measure pass (e.g. size). */ + default Constraints modifyConstraints(Constraints constraints) { + return constraints; + } + + /** Adjust the measured size for padding/offset effects. */ + default MeasuredSize modifyMeasuredSize(UIComponent component, Constraints constraints, MeasuredSize childSize) { + return childSize; + } + + /** Transform the layout rect (e.g. apply padding inset). */ + default LayoutRect modifyLayout(LayoutRect rect) { + return rect; + } + + /** Emit additional render states (e.g. background, border). */ + default void emitRenderState(GuiGraphicsExtractor extractor, LayoutRect bounds) { + } + + // ── factory methods ── + + static ModifierElement size(float width, float height) { + return new SizeElement(width, width, height, height); + } + + static ModifierElement width(float w) { + return new SizeElement(0, Float.MAX_VALUE, w, w); + } + + static ModifierElement height(float h) { + return new SizeElement(h, 0, h, Float.MAX_VALUE); + } + + static ModifierElement fillMaxWidth() { + return new SizeElement(0, Float.MAX_VALUE, Float.MAX_VALUE, Float.MAX_VALUE); + } + + static ModifierElement fillMaxHeight() { + return new SizeElement(Float.MAX_VALUE, Float.MAX_VALUE, 0, Float.MAX_VALUE); + } + + static ModifierElement fillMaxSize() { + return new SizeElement(Float.MAX_VALUE, Float.MAX_VALUE, Float.MAX_VALUE, Float.MAX_VALUE); + } + + static ModifierElement padding(float all) { + return new PaddingElement(all, all, all, all); + } + + static ModifierElement padding(float horizontal, float vertical) { + return new PaddingElement(horizontal, vertical, horizontal, vertical); + } + + static ModifierElement background(int color) { + return new BackgroundElement(color); + } +} diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/MutableState.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/MutableState.java new file mode 100644 index 00000000..49be6bfc --- /dev/null +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/MutableState.java @@ -0,0 +1,53 @@ +package dev.anvilcraft.lib.v2.ui; + +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; + +/** + * An observable state holder. + *

+ * Reads are tracked against the current composition slot. + * Writes mark all reader slots dirty so recomposition is scoped. + * + * @param the type of value held + */ +public class MutableState { + + private T value; + final Set readers = new HashSet<>(); + + public MutableState(T initialValue) { + this.value = initialValue; + } + + /** + * Read the current value, recording this slot as a reader + * if called within a composition emission. + */ + public T getValue() { + Composition comp = Composition.currentOrNull(); + if (comp != null && comp.currentSlot != null) { + comp.currentSlot.addReadState(this); + readers.add(comp.currentSlot); + } + return value; + } + + /** + * Set a new value. If changed, marks all reader slots dirty. + */ + public void setValue(T newValue) { + if (!Objects.equals(value, newValue)) { + value = newValue; + for (Composition.Slot slot : readers) { + slot.markDirty(); + } + } + } + + @Override + public String toString() { + return "State(" + value + ")"; + } +} diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/PaddingElement.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/PaddingElement.java new file mode 100644 index 00000000..7de70b3b --- /dev/null +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/PaddingElement.java @@ -0,0 +1,25 @@ +package dev.anvilcraft.lib.v2.ui; + +/** + * Modifier element that insets the component bounds by padding. + */ +record PaddingElement(float left, float top, float right, float bottom) implements ModifierElement { + + @Override + public MeasuredSize modifyMeasuredSize(UIComponent component, Constraints constraints, MeasuredSize childSize) { + return MeasuredSize.of( + childSize.width() + left + right, + childSize.height() + top + bottom + ); + } + + @Override + public LayoutRect modifyLayout(LayoutRect rect) { + return LayoutRect.of( + rect.x() + left, + rect.y() + top, + rect.width() - left - right, + rect.height() - top - bottom + ); + } +} diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/SingleElementModifier.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/SingleElementModifier.java new file mode 100644 index 00000000..3d650bdb --- /dev/null +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/SingleElementModifier.java @@ -0,0 +1,24 @@ +package dev.anvilcraft.lib.v2.ui; + +import java.util.function.BiFunction; + +/** + * A modifier chain node wrapping a single {@link ModifierElement}. + */ +record SingleElementModifier(ModifierElement element) implements Modifier { + + @Override + public Modifier then(Modifier other) { + return new CombinedModifier(element, other); + } + + @Override + public R foldIn(R initial, BiFunction operation) { + return operation.apply(initial, element); + } + + @Override + public R foldOut(R initial, BiFunction operation) { + return operation.apply(element, initial); + } +} diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/SizeElement.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/SizeElement.java new file mode 100644 index 00000000..31c5e68b --- /dev/null +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/SizeElement.java @@ -0,0 +1,17 @@ +package dev.anvilcraft.lib.v2.ui; + +/** + * Modifier element that constrains a component's size. + */ +record SizeElement(float minWidthHint, float maxWidthHint, float minHeightHint, float maxHeightHint) + implements ModifierElement { + + @Override + public Constraints modifyConstraints(Constraints constraints) { + float minW = minWidthHint > 0 ? Math.max(constraints.minWidth(), minWidthHint) : constraints.minWidth(); + float maxW = maxWidthHint < Float.MAX_VALUE ? Math.min(constraints.maxWidth(), maxWidthHint) : constraints.maxWidth(); + float minH = minHeightHint > 0 ? Math.max(constraints.minHeight(), minHeightHint) : constraints.minHeight(); + float maxH = maxHeightHint < Float.MAX_VALUE ? Math.min(constraints.maxHeight(), maxHeightHint) : constraints.maxHeight(); + return constraints.copy(minW, maxW, minH, maxH); + } +} diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/TextComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/TextComponent.java new file mode 100644 index 00000000..7f06daf5 --- /dev/null +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/TextComponent.java @@ -0,0 +1,75 @@ +package dev.anvilcraft.lib.v2.ui; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.network.chat.Component; + +import java.util.Collections; +import java.util.List; + +/** + * Renders a single line of Minecraft text. + */ +public class TextComponent implements UIComponent { + + private final Modifier modifier; + private String text; + private int color = 0xFFFFFFFF; + + // layout state + private float x, y, width, height; + + public TextComponent(Modifier modifier, String text) { + this.modifier = modifier; + this.text = text; + } + + // ── chained setters ── + + public TextComponent text(String text) { + this.text = text; + return this; + } + + public TextComponent color(int color) { + this.color = color; + return this; + } + + // ── UIComponent ── + + @Override + public Modifier modifier() { + return modifier; + } + + @Override + public List children() { + return Collections.emptyList(); + } + + @Override + public MeasuredSize measure(Constraints constraints) { + var font = Minecraft.getInstance().font; + float w = font.width(Component.literal(text)); + float h = font.lineHeight; + return MeasuredSize.of(constraints.constrainWidth(w), constraints.constrainHeight(h)); + } + + @Override + public void layout(float x, float y, float width, float height) { + this.x = x; + this.y = y; + this.width = width; + this.height = height; + } + + @Override + public void extractRenderState(GuiGraphicsExtractor extractor) { + var font = Minecraft.getInstance().font; + Component component = Component.literal(text); + int textWidth = font.width(component); + int xPos = (int) (x + (width - textWidth) / 2); + extractor.centeredText(font, component, (int) (x + width / 2), (int) y, color); + } +} diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIComponent.java new file mode 100644 index 00000000..7e1eaae5 --- /dev/null +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIComponent.java @@ -0,0 +1,40 @@ +package dev.anvilcraft.lib.v2.ui; + +import net.minecraft.client.gui.GuiGraphicsExtractor; + +/** + * Core interface for all UI components. + *

+ * Components participate in three phases each frame: + *

    + *
  1. {@link #measure(Constraints)} — determine desired size given parent constraints
  2. + *
  3. {@link #layout(float, float, float, float)} — receive final position from parent
  4. + *
  5. {@link #extractRenderState(GuiGraphicsExtractor)} — submit render states for GPU rendering
  6. + *
+ */ +public interface UIComponent { + + /** The modifier chain applied to this component. */ + Modifier modifier(); + + /** Children of this component, or empty list for leaf components. */ + java.util.List children(); + + /** + * Measure this component given parent constraints. + * Container components recursively measure children. + */ + MeasuredSize measure(Constraints constraints); + + /** + * Set final position after layout pass. + * Container components position their children. + */ + void layout(float x, float y, float width, float height); + + /** + * Submit render states to the Minecraft GUI render pipeline. + * Called after measure+layout, once per frame. + */ + void extractRenderState(GuiGraphicsExtractor extractor); +} diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIScope.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIScope.java new file mode 100644 index 00000000..6ba75a89 --- /dev/null +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIScope.java @@ -0,0 +1,59 @@ +package dev.anvilcraft.lib.v2.ui; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.function.Consumer; + +/** + * Base scope for building component trees. + *

+ * Container components (Column, Row, Box) create a scope, + * run their content lambda against it, then collect the children. + *

+ * Component builders are defined here as concrete methods + * so all scope subclasses inherit them. + */ +public abstract class UIScope { + + final List children = new ArrayList<>(); + + public void addChild(UIComponent child) { + children.add(child); + } + + public List getChildren() { + return Collections.unmodifiableList(children); + } + + /** Internal: clear children before recomposition. */ + void clearChildren() { + children.clear(); + } + + // ── component builders ── + + public TextComponent Text(String text) { + TextComponent c = new TextComponent(Modifier.NONE, text); + addChild(c); + Composition.current().emit(c); + return c; + } + + public ButtonComponent Button(String label, Runnable onClick) { + ButtonComponent c = new ButtonComponent(Modifier.NONE, label, onClick); + addChild(c); + Composition.current().emit(c); + return c; + } + + public ColumnComponent Column(Consumer content) { + ColumnComponent c = new ColumnComponent(Modifier.NONE); + ColumnScope inner = new ColumnScope(); + content.accept(inner); + c.setChildren(inner.getChildren()); + addChild(c); + Composition.current().emit(c); + return c; + } +} From 69fc07b83b2b769f834f53207e6ddbb389b3e3e4 Mon Sep 17 00:00:00 2001 From: Gugle Date: Mon, 18 May 2026 20:44:55 +0800 Subject: [PATCH 04/67] feat(ui): add DeclarativeTestScreen for end-to-end testing of declarative UI components --- .../v2/test/client/AnvilLibTestClient.java | 8 ++++ .../client/screen/DeclarativeTestScreen.java | 40 +++++++++++++++++++ module.ui/TODO.md | 19 ++++----- 3 files changed, 58 insertions(+), 9 deletions(-) create mode 100644 module.test/src/main/java/dev/anvilcraft/lib/v2/test/client/screen/DeclarativeTestScreen.java diff --git a/module.test/src/main/java/dev/anvilcraft/lib/v2/test/client/AnvilLibTestClient.java b/module.test/src/main/java/dev/anvilcraft/lib/v2/test/client/AnvilLibTestClient.java index 1482a25f..b78b1ace 100644 --- a/module.test/src/main/java/dev/anvilcraft/lib/v2/test/client/AnvilLibTestClient.java +++ b/module.test/src/main/java/dev/anvilcraft/lib/v2/test/client/AnvilLibTestClient.java @@ -5,6 +5,7 @@ import dev.anvilcraft.lib.v2.test.all.TestTiles; import dev.anvilcraft.lib.v2.test.client.cber.TestCachedRenderer; import dev.anvilcraft.lib.v2.test.client.gui.SdfGraphicsLayer; +import dev.anvilcraft.lib.v2.test.client.screen.DeclarativeTestScreen; import dev.anvilcraft.lib.v2.test.client.screen.GuiTestScreen; import net.minecraft.client.Minecraft; import net.minecraft.commands.Commands; @@ -46,6 +47,13 @@ public static void on(RegisterClientCommandsEvent event) { Minecraft.getInstance().setScreen(new GuiTestScreen()); return 1; }) + ). + then( + literal("declarative"). + executes(_ -> { + Minecraft.getInstance().setScreen(new DeclarativeTestScreen()); + return 1; + }) ) ); } diff --git a/module.test/src/main/java/dev/anvilcraft/lib/v2/test/client/screen/DeclarativeTestScreen.java b/module.test/src/main/java/dev/anvilcraft/lib/v2/test/client/screen/DeclarativeTestScreen.java new file mode 100644 index 00000000..f9338d28 --- /dev/null +++ b/module.test/src/main/java/dev/anvilcraft/lib/v2/test/client/screen/DeclarativeTestScreen.java @@ -0,0 +1,40 @@ +package dev.anvilcraft.lib.v2.test.client.screen; + +import dev.anvilcraft.lib.v2.ui.*; +import net.minecraft.network.chat.Component; + +/** + * End-to-end test screen for the declarative UI system. + * Demonstrates state management, recomposition, Column layout, + * Text rendering, and Button interaction. + */ +public class DeclarativeTestScreen extends DeclarativeScreen { + + public DeclarativeTestScreen() { + super(Component.literal("Declarative UI Test")); + } + + @Override + protected void content(UIScope scope) { + // State that survives recomposition + MutableState counter = Composition.current() + .remember(() -> new MutableState<>(0)); + + scope.Column(col -> { + col.Text("AnvilLib Declarative UI") + .color(0xFFFFFF00); + + col.Text("") + .text("Counter: " + counter.getValue()); + + col.Button("Increment", () -> + counter.setValue(counter.getValue() + 1)); + + col.Button("Decrement", () -> + counter.setValue(counter.getValue() - 1)); + + col.Button("Reset", () -> + counter.setValue(0)); + }); + } +} diff --git a/module.ui/TODO.md b/module.ui/TODO.md index 50f1041a..85a808b8 100644 --- a/module.ui/TODO.md +++ b/module.ui/TODO.md @@ -7,18 +7,19 @@ ## Phase 1: 构建系统 + 核心框架 -- [ ] `module.ui/build.gradle` — 添加 `implementation project(':anvillib-rendering-neoforge-26.1')` -- [ ] `Constraints` — min/max width/height 约束 -- [ ] `Modifier` — 链式 API 接口(`then` / `foldIn` / `foldOut`) -- [ ] `ModifierElement` — 单个修饰符节点接口 -- [ ] `UIComponent` — 核心接口:`measure(Constraints): MeasuredSize` / `layout(...)` / `extractRenderState(GuiGraphicsExtractor)` -- [ ] `Composition` — slot table + `emit()` / `recompose()` / `invalidate()` +- [x] `module.ui/build.gradle` — 添加 `implementation project(':anvillib-rendering-neoforge-26.1')` +- [x] `Constraints` — min/max width/height 约束 +- [x] `Modifier` — 链式 API 接口(`then` / `foldIn` / `foldOut`) +- [x] `ModifierElement` — 单个修饰符节点接口 +- [x] `UIComponent` — 核心接口:`measure(Constraints): MeasuredSize` / `layout(...)` / `extractRenderState(GuiGraphicsExtractor)` +- [x] `Composition` — slot table + `emit()` / `recompose()` / `invalidate()` ## Phase 2: 状态管理 -- [ ] `MutableState` — 可观察状态:getter 记录 reader slot,setter 精确 markDirty -- [ ] `remember { }` — 按 slot 位置持久化,recompose 时回读同一对象 -- [ ] 脏标记传播:只重执行 dirty group,干净子树跳过 +- [x] `MutableState` — 可观察状态:getter 记录 reader slot,setter 精确 markDirty +- [x] `remember { }` — 按 slot 位置持久化,recompose 时回读同一对象 +- [x] 脏标记传播:只重执行 dirty group,干净子树跳过 +- [x] `DeclarativeTestScreen` — 端到端验证 Screen(`/anvillib_test_client declarative`) ## Phase 3: 布局容器 From f497866fdd71c2500e2857f7f54f921c96660750 Mon Sep 17 00:00:00 2001 From: Gugle Date: Mon, 18 May 2026 20:51:25 +0800 Subject: [PATCH 05/67] feat(ui): add UI components including Button, Column, Text, and modifiers for layout and styling --- .../dev/anvilcraft/lib/v2/ui/Modifier.java | 3 +++ .../dev/anvilcraft/lib/v2/ui/UIScope.java | 5 +++++ .../ui/{ => component}/ButtonComponent.java | 20 ++++++------------- .../ui/{ => component}/ColumnComponent.java | 8 ++++++-- .../v2/ui/{ => component}/ColumnScope.java | 4 +++- .../v2/ui/{ => component}/TextComponent.java | 12 +++++------ .../ui/{ => modifier}/BackgroundElement.java | 8 +++----- .../ui/{ => modifier}/CombinedModifier.java | 9 ++++----- .../v2/ui/{ => modifier}/ModifierElement.java | 12 +++++------ .../v2/ui/{ => modifier}/PaddingElement.java | 12 ++++++----- .../{ => modifier}/SingleElementModifier.java | 9 ++++----- .../lib/v2/ui/{ => modifier}/SizeElement.java | 9 ++++----- 12 files changed, 56 insertions(+), 55 deletions(-) rename module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/{ => component}/ButtonComponent.java (84%) rename module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/{ => component}/ColumnComponent.java (89%) rename module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/{ => component}/ColumnScope.java (56%) rename module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/{ => component}/TextComponent.java (88%) rename module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/{ => modifier}/BackgroundElement.java (70%) rename module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/{ => modifier}/CombinedModifier.java (73%) rename module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/{ => modifier}/ModifierElement.java (83%) rename module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/{ => modifier}/PaddingElement.java (62%) rename module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/{ => modifier}/SingleElementModifier.java (71%) rename module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/{ => modifier}/SizeElement.java (77%) diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Modifier.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Modifier.java index 7c97bb4a..e77877d5 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Modifier.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Modifier.java @@ -1,5 +1,8 @@ package dev.anvilcraft.lib.v2.ui; +import dev.anvilcraft.lib.v2.ui.modifier.ModifierElement; +import dev.anvilcraft.lib.v2.ui.modifier.SingleElementModifier; + import java.util.function.BiFunction; /** diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIScope.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIScope.java index 6ba75a89..31aef356 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIScope.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIScope.java @@ -1,5 +1,10 @@ package dev.anvilcraft.lib.v2.ui; +import dev.anvilcraft.lib.v2.ui.component.ButtonComponent; +import dev.anvilcraft.lib.v2.ui.component.ColumnComponent; +import dev.anvilcraft.lib.v2.ui.component.ColumnScope; +import dev.anvilcraft.lib.v2.ui.component.TextComponent; + import java.util.ArrayList; import java.util.Collections; import java.util.List; diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/ButtonComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ButtonComponent.java similarity index 84% rename from module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/ButtonComponent.java rename to module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ButtonComponent.java index a49ca7e4..fce21c4e 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/ButtonComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ButtonComponent.java @@ -1,6 +1,10 @@ -package dev.anvilcraft.lib.v2.ui; +package dev.anvilcraft.lib.v2.ui.component; import dev.anvilcraft.lib.v2.rendering.sdf.SdfGraphics; +import dev.anvilcraft.lib.v2.ui.Constraints; +import dev.anvilcraft.lib.v2.ui.MeasuredSize; +import dev.anvilcraft.lib.v2.ui.Modifier; +import dev.anvilcraft.lib.v2.ui.UIComponent; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiGraphicsExtractor; import net.minecraft.network.chat.Component; @@ -14,7 +18,6 @@ public class ButtonComponent implements UIComponent { private static final int BG_COLOR = 0xFF555555; - private static final int BG_HOVER_COLOR = 0xFF777777; private static final int TEXT_COLOR = 0xFFFFFFFF; private static final float PADDING_H = 12; private static final float PADDING_V = 6; @@ -32,8 +35,6 @@ public ButtonComponent(Modifier modifier, String label, Runnable onClick) { this.onClick = onClick; } - // ── chained setters ── - public ButtonComponent label(String label) { this.label = label; return this; @@ -44,8 +45,6 @@ public ButtonComponent onClick(Runnable onClick) { return this; } - // ── UIComponent ── - @Override public Modifier modifier() { return modifier; @@ -78,7 +77,6 @@ public void layout(float x, float y, float width, float height) { @Override public void extractRenderState(GuiGraphicsExtractor extractor) { - // Background SdfGraphics.instance .box(x, y, width, height) .color(BG_COLOR) @@ -86,17 +84,11 @@ public void extractRenderState(GuiGraphicsExtractor extractor) { .fill() .draw(extractor); - // Label var font = Minecraft.getInstance().font; Component comp = Component.literal(label); - float textW = font.width(comp); - float textH = font.lineHeight; - float cx = x + (width - textW) / 2; - float cy = y + (height - textH) / 2; - extractor.centeredText(font, comp, (int) (x + width / 2), (int) cy, TEXT_COLOR); + extractor.centeredText(font, comp, (int) (x + width / 2), (int) (y + (height - font.lineHeight) / 2), TEXT_COLOR); } - /** Invoke click handler if clicked. Called by hit-testing. */ void click() { if (onClick != null) { onClick.run(); diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/ColumnComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ColumnComponent.java similarity index 89% rename from module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/ColumnComponent.java rename to module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ColumnComponent.java index 6b616095..d841df74 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/ColumnComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ColumnComponent.java @@ -1,5 +1,9 @@ -package dev.anvilcraft.lib.v2.ui; +package dev.anvilcraft.lib.v2.ui.component; +import dev.anvilcraft.lib.v2.ui.Constraints; +import dev.anvilcraft.lib.v2.ui.MeasuredSize; +import dev.anvilcraft.lib.v2.ui.Modifier; +import dev.anvilcraft.lib.v2.ui.UIComponent; import net.minecraft.client.gui.GuiGraphicsExtractor; import java.util.ArrayList; @@ -22,7 +26,7 @@ public ColumnComponent(Modifier modifier) { this.modifier = modifier; } - void setChildren(List children) { + public void setChildren(List children) { this.children = List.copyOf(children); } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/ColumnScope.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ColumnScope.java similarity index 56% rename from module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/ColumnScope.java rename to module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ColumnScope.java index c37a3f42..493b298b 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/ColumnScope.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ColumnScope.java @@ -1,4 +1,6 @@ -package dev.anvilcraft.lib.v2.ui; +package dev.anvilcraft.lib.v2.ui.component; + +import dev.anvilcraft.lib.v2.ui.UIScope; /** * Scope for children inside a {@link ColumnComponent}. diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/TextComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextComponent.java similarity index 88% rename from module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/TextComponent.java rename to module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextComponent.java index 7f06daf5..7d9a3a3c 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/TextComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextComponent.java @@ -1,5 +1,9 @@ -package dev.anvilcraft.lib.v2.ui; +package dev.anvilcraft.lib.v2.ui.component; +import dev.anvilcraft.lib.v2.ui.Constraints; +import dev.anvilcraft.lib.v2.ui.MeasuredSize; +import dev.anvilcraft.lib.v2.ui.Modifier; +import dev.anvilcraft.lib.v2.ui.UIComponent; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiGraphicsExtractor; import net.minecraft.network.chat.Component; @@ -24,8 +28,6 @@ public TextComponent(Modifier modifier, String text) { this.text = text; } - // ── chained setters ── - public TextComponent text(String text) { this.text = text; return this; @@ -36,8 +38,6 @@ public TextComponent color(int color) { return this; } - // ── UIComponent ── - @Override public Modifier modifier() { return modifier; @@ -68,8 +68,6 @@ public void layout(float x, float y, float width, float height) { public void extractRenderState(GuiGraphicsExtractor extractor) { var font = Minecraft.getInstance().font; Component component = Component.literal(text); - int textWidth = font.width(component); - int xPos = (int) (x + (width - textWidth) / 2); extractor.centeredText(font, component, (int) (x + width / 2), (int) y, color); } } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/BackgroundElement.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/BackgroundElement.java similarity index 70% rename from module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/BackgroundElement.java rename to module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/BackgroundElement.java index af1c1294..4650361e 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/BackgroundElement.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/BackgroundElement.java @@ -1,12 +1,10 @@ -package dev.anvilcraft.lib.v2.ui; +package dev.anvilcraft.lib.v2.ui.modifier; import dev.anvilcraft.lib.v2.rendering.sdf.SdfGraphics; +import dev.anvilcraft.lib.v2.ui.LayoutRect; import net.minecraft.client.gui.GuiGraphicsExtractor; -/** - * Renders a filled rounded rectangle as the component's background. - */ -record BackgroundElement(int color) implements ModifierElement { +public record BackgroundElement(int color) implements ModifierElement { @Override public void emitRenderState(GuiGraphicsExtractor extractor, LayoutRect bounds) { diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/CombinedModifier.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/CombinedModifier.java similarity index 73% rename from module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/CombinedModifier.java rename to module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/CombinedModifier.java index f8467ca3..63446fa2 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/CombinedModifier.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/CombinedModifier.java @@ -1,11 +1,10 @@ -package dev.anvilcraft.lib.v2.ui; +package dev.anvilcraft.lib.v2.ui.modifier; + +import dev.anvilcraft.lib.v2.ui.Modifier; import java.util.function.BiFunction; -/** - * Two {@link Modifier} chains joined by {@code then()}. - */ -record CombinedModifier(ModifierElement outer, Modifier inner) implements Modifier { +public record CombinedModifier(ModifierElement outer, Modifier inner) implements Modifier { @Override public Modifier then(Modifier other) { diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/ModifierElement.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/ModifierElement.java similarity index 83% rename from module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/ModifierElement.java rename to module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/ModifierElement.java index 5370162a..a177e922 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/ModifierElement.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/ModifierElement.java @@ -1,29 +1,29 @@ -package dev.anvilcraft.lib.v2.ui; +package dev.anvilcraft.lib.v2.ui.modifier; +import dev.anvilcraft.lib.v2.ui.Constraints; +import dev.anvilcraft.lib.v2.ui.LayoutRect; +import dev.anvilcraft.lib.v2.ui.MeasuredSize; +import dev.anvilcraft.lib.v2.ui.UIComponent; import net.minecraft.client.gui.GuiGraphicsExtractor; /** - * A single node in a {@link Modifier} chain. + * A single node in a {@link dev.anvilcraft.lib.v2.ui.Modifier} chain. * Each element can intercept measure, layout, and render phases. */ public interface ModifierElement { - /** Modify constraints during the measure pass (e.g. size). */ default Constraints modifyConstraints(Constraints constraints) { return constraints; } - /** Adjust the measured size for padding/offset effects. */ default MeasuredSize modifyMeasuredSize(UIComponent component, Constraints constraints, MeasuredSize childSize) { return childSize; } - /** Transform the layout rect (e.g. apply padding inset). */ default LayoutRect modifyLayout(LayoutRect rect) { return rect; } - /** Emit additional render states (e.g. background, border). */ default void emitRenderState(GuiGraphicsExtractor extractor, LayoutRect bounds) { } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/PaddingElement.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/PaddingElement.java similarity index 62% rename from module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/PaddingElement.java rename to module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/PaddingElement.java index 7de70b3b..bcf7fc8b 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/PaddingElement.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/PaddingElement.java @@ -1,9 +1,11 @@ -package dev.anvilcraft.lib.v2.ui; +package dev.anvilcraft.lib.v2.ui.modifier; -/** - * Modifier element that insets the component bounds by padding. - */ -record PaddingElement(float left, float top, float right, float bottom) implements ModifierElement { +import dev.anvilcraft.lib.v2.ui.Constraints; +import dev.anvilcraft.lib.v2.ui.LayoutRect; +import dev.anvilcraft.lib.v2.ui.MeasuredSize; +import dev.anvilcraft.lib.v2.ui.UIComponent; + +public record PaddingElement(float left, float top, float right, float bottom) implements ModifierElement { @Override public MeasuredSize modifyMeasuredSize(UIComponent component, Constraints constraints, MeasuredSize childSize) { diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/SingleElementModifier.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/SingleElementModifier.java similarity index 71% rename from module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/SingleElementModifier.java rename to module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/SingleElementModifier.java index 3d650bdb..6f0f245f 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/SingleElementModifier.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/SingleElementModifier.java @@ -1,11 +1,10 @@ -package dev.anvilcraft.lib.v2.ui; +package dev.anvilcraft.lib.v2.ui.modifier; + +import dev.anvilcraft.lib.v2.ui.Modifier; import java.util.function.BiFunction; -/** - * A modifier chain node wrapping a single {@link ModifierElement}. - */ -record SingleElementModifier(ModifierElement element) implements Modifier { +public record SingleElementModifier(ModifierElement element) implements Modifier { @Override public Modifier then(Modifier other) { diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/SizeElement.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/SizeElement.java similarity index 77% rename from module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/SizeElement.java rename to module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/SizeElement.java index 31c5e68b..e41ff6bc 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/SizeElement.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/SizeElement.java @@ -1,9 +1,8 @@ -package dev.anvilcraft.lib.v2.ui; +package dev.anvilcraft.lib.v2.ui.modifier; -/** - * Modifier element that constrains a component's size. - */ -record SizeElement(float minWidthHint, float maxWidthHint, float minHeightHint, float maxHeightHint) +import dev.anvilcraft.lib.v2.ui.Constraints; + +public record SizeElement(float minWidthHint, float maxWidthHint, float minHeightHint, float maxHeightHint) implements ModifierElement { @Override From 73162839222a2dbbcc23fbe76537a0ad4686c2d8 Mon Sep 17 00:00:00 2001 From: Gugle Date: Mon, 18 May 2026 21:00:10 +0800 Subject: [PATCH 06/67] feat(ui): add alignment and arrangement classes for layout management in UI components --- module.ui/TODO.md | 12 +- .../dev/anvilcraft/lib/v2/ui/Alignment.java | 45 ++++++ .../dev/anvilcraft/lib/v2/ui/Arrangement.java | 153 ++++++++++++++++++ .../dev/anvilcraft/lib/v2/ui/UIScope.java | 24 +++ .../lib/v2/ui/component/BoxComponent.java | 89 ++++++++++ .../lib/v2/ui/component/BoxScope.java | 9 ++ .../lib/v2/ui/component/ColumnComponent.java | 51 ++++-- .../lib/v2/ui/component/RowComponent.java | 107 ++++++++++++ .../lib/v2/ui/component/RowScope.java | 9 ++ 9 files changed, 476 insertions(+), 23 deletions(-) create mode 100644 module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Alignment.java create mode 100644 module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Arrangement.java create mode 100644 module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/BoxComponent.java create mode 100644 module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/BoxScope.java create mode 100644 module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/RowComponent.java create mode 100644 module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/RowScope.java diff --git a/module.ui/TODO.md b/module.ui/TODO.md index 85a808b8..7abef1fc 100644 --- a/module.ui/TODO.md +++ b/module.ui/TODO.md @@ -23,12 +23,12 @@ ## Phase 3: 布局容器 -- [ ] `Column` + `ColumnScope` — 纵向排列 -- [ ] `Row` + `RowScope` — 横向排列 -- [ ] `Box` + `BoxScope` — 层叠 -- [ ] `ColumnMeasurePolicy` / `RowMeasurePolicy` / `BoxMeasurePolicy` -- [ ] `Arrangement.Vertical` / `Arrangement.Horizontal` — SpaceBetween / SpaceAround / SpaceEvenly -- [ ] `Alignment` — Start / Center / End +- [x] `Column` + `ColumnScope` — 纵向排列(重构:spacing / verticalArrangement / horizontalAlignment) +- [x] `Row` + `RowScope` — 横向排列(horizontalArrangement / verticalAlignment / spacing) +- [x] `Box` + `BoxScope` — 层叠(contentAlignment) +- [x] MeasurePolicy 内联实现(随组件复杂度提升再提取为策略对象) +- [x] `Arrangement.Vertical` / `Arrangement.Horizontal` — Top/Center/Bottom, Start/Center/End, SpaceBetween/SpaceAround/SpaceEvenly +- [x] `Alignment.Horizontal` / `Alignment.Vertical` — Start/Center/End, Top/Center/Bottom ## Phase 4: 基础组件 diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Alignment.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Alignment.java new file mode 100644 index 00000000..da5140af --- /dev/null +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Alignment.java @@ -0,0 +1,45 @@ +package dev.anvilcraft.lib.v2.ui; + +/** + * 子组件在交叉轴上的对齐方式。 + */ +public final class Alignment { + + private Alignment() {} + + /** 水平对齐(Column 中每个子组件的 X 定位)。 */ + public enum Horizontal { + Start, Center, End; + + /** + * @param totalWidth 父容器宽度 + * @param childWidth 子组件宽度 + * @return 子组件的 x 偏移 + */ + public float align(float totalWidth, float childWidth) { + return switch (this) { + case Start -> 0; + case Center -> (totalWidth - childWidth) / 2; + case End -> totalWidth - childWidth; + }; + } + } + + /** 垂直对齐(Row 中每个子组件的 Y 定位)。 */ + public enum Vertical { + Top, Center, Bottom; + + /** + * @param totalHeight 父容器高度 + * @param childHeight 子组件高度 + * @return 子组件的 y 偏移 + */ + public float align(float totalHeight, float childHeight) { + return switch (this) { + case Top -> 0; + case Center -> (totalHeight - childHeight) / 2; + case Bottom -> totalHeight - childHeight; + }; + } + } +} diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Arrangement.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Arrangement.java new file mode 100644 index 00000000..9c6adee6 --- /dev/null +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Arrangement.java @@ -0,0 +1,153 @@ +package dev.anvilcraft.lib.v2.ui; + +import java.util.List; + +/** + * 子组件在主轴上的分布方式。 + */ +public final class Arrangement { + + private Arrangement() {} + + /** 纵向排列(Column 主轴)。 */ + public enum Vertical { + Top, Center, Bottom, SpaceBetween, SpaceAround, SpaceEvenly; + + /** + * @param totalHeight 可用总高度 + * @param childHeights 各子组件高度 + * @param spacing 间距 + * @return 各子组件的 y 偏移 + */ + public float[] arrange(float totalHeight, List childHeights, float spacing) { + int n = childHeights.size(); + if (n == 0) return new float[0]; + + float content = 0; + for (float h : childHeights) content += h; + float gapTotal = spacing * (n - 1); + float extra = totalHeight - content - gapTotal; + + float[] offsets = new float[n]; + switch (this) { + case Top -> { + float y = 0; + for (int i = 0; i < n; i++) { + offsets[i] = y; + y += childHeights.get(i) + spacing; + } + } + case Center -> { + float y = Math.max(0, extra / 2); + for (int i = 0; i < n; i++) { + offsets[i] = y; + y += childHeights.get(i) + spacing; + } + } + case Bottom -> { + float y = Math.max(0, extra); + for (int i = 0; i < n; i++) { + offsets[i] = y; + y += childHeights.get(i) + spacing; + } + } + case SpaceBetween -> { + float gap = n > 1 ? (extra + gapTotal) / (n - 1) : 0; + float y = 0; + for (int i = 0; i < n; i++) { + offsets[i] = y; + y += childHeights.get(i) + gap; + } + } + case SpaceAround -> { + float halfGap = n > 0 ? (extra + gapTotal) / (n * 2f) : 0; + float y = halfGap; + for (int i = 0; i < n; i++) { + offsets[i] = y; + y += childHeights.get(i) + spacing + halfGap * 2 - spacing; + } + } + case SpaceEvenly -> { + float gap = n > 0 ? (extra + gapTotal) / (n + 1) : 0; + float y = gap; + for (int i = 0; i < n; i++) { + offsets[i] = y; + y += childHeights.get(i) + spacing + gap - spacing; + } + } + } + return offsets; + } + } + + /** 横向排列(Row 主轴)。 */ + public enum Horizontal { + Start, Center, End, SpaceBetween, SpaceAround, SpaceEvenly; + + /** + * @param totalWidth 可用总宽度 + * @param childWidths 各子组件宽度 + * @param spacing 间距 + * @return 各子组件的 x 偏移 + */ + public float[] arrange(float totalWidth, List childWidths, float spacing) { + int n = childWidths.size(); + if (n == 0) return new float[0]; + + float content = 0; + for (float w : childWidths) content += w; + float gapTotal = spacing * (n - 1); + float extra = totalWidth - content - gapTotal; + + float[] offsets = new float[n]; + switch (this) { + case Start -> { + float x = 0; + for (int i = 0; i < n; i++) { + offsets[i] = x; + x += childWidths.get(i) + spacing; + } + } + case Center -> { + float x = Math.max(0, extra / 2); + for (int i = 0; i < n; i++) { + offsets[i] = x; + x += childWidths.get(i) + spacing; + } + } + case End -> { + float x = Math.max(0, extra); + for (int i = 0; i < n; i++) { + offsets[i] = x; + x += childWidths.get(i) + spacing; + } + } + case SpaceBetween -> { + float gap = n > 1 ? (extra + gapTotal) / (n - 1) : 0; + float x = 0; + for (int i = 0; i < n; i++) { + offsets[i] = x; + x += childWidths.get(i) + gap; + } + } + case SpaceAround -> { + float halfGap = n > 0 ? (extra + gapTotal) / (n * 2f) : 0; + float x = halfGap; + for (int i = 0; i < n; i++) { + offsets[i] = x; + x += childWidths.get(i) + spacing + halfGap * 2 - spacing; + } + } + case SpaceEvenly -> { + float gap = n > 0 ? (extra + gapTotal) / (n + 1) : 0; + float x = gap; + for (int i = 0; i < n; i++) { + offsets[i] = x; + x += childWidths.get(i) + spacing + gap - spacing; + } + } + } + return offsets; + } + } +} diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIScope.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIScope.java index 31aef356..f4eeee44 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIScope.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIScope.java @@ -1,8 +1,12 @@ package dev.anvilcraft.lib.v2.ui; import dev.anvilcraft.lib.v2.ui.component.ButtonComponent; +import dev.anvilcraft.lib.v2.ui.component.BoxComponent; +import dev.anvilcraft.lib.v2.ui.component.BoxScope; import dev.anvilcraft.lib.v2.ui.component.ColumnComponent; import dev.anvilcraft.lib.v2.ui.component.ColumnScope; +import dev.anvilcraft.lib.v2.ui.component.RowComponent; +import dev.anvilcraft.lib.v2.ui.component.RowScope; import dev.anvilcraft.lib.v2.ui.component.TextComponent; import java.util.ArrayList; @@ -61,4 +65,24 @@ public ColumnComponent Column(Consumer content) { Composition.current().emit(c); return c; } + + public RowComponent Row(Consumer content) { + RowComponent c = new RowComponent(Modifier.NONE); + RowScope inner = new RowScope(); + content.accept(inner); + c.setChildren(inner.getChildren()); + addChild(c); + Composition.current().emit(c); + return c; + } + + public BoxComponent Box(Consumer content) { + BoxComponent c = new BoxComponent(Modifier.NONE); + BoxScope inner = new BoxScope(); + content.accept(inner); + c.setChildren(inner.getChildren()); + addChild(c); + Composition.current().emit(c); + return c; + } } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/BoxComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/BoxComponent.java new file mode 100644 index 00000000..678a73f8 --- /dev/null +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/BoxComponent.java @@ -0,0 +1,89 @@ +package dev.anvilcraft.lib.v2.ui.component; + +import dev.anvilcraft.lib.v2.ui.*; +import net.minecraft.client.gui.GuiGraphicsExtractor; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * 层叠布局。所有子组件重叠于同一区域,按声明顺序从底到顶绘制。 + * Box 本身的大小由最大的子组件决定。 + */ +public class BoxComponent implements UIComponent { + + private final Modifier modifier; + private List children = Collections.emptyList(); + private List childSizes = Collections.emptyList(); + + private Alignment.Horizontal contentAlignmentH = Alignment.Horizontal.Start; + private Alignment.Vertical contentAlignmentV = Alignment.Vertical.Top; + + private float x, y, width, height; + + public BoxComponent(Modifier modifier) { + this.modifier = modifier; + } + + public void setChildren(List children) { + this.children = List.copyOf(children); + } + + public BoxComponent contentAlignment(Alignment.Horizontal h, Alignment.Vertical v) { + this.contentAlignmentH = h; + this.contentAlignmentV = v; + return this; + } + + @Override + public Modifier modifier() { return modifier; } + + @Override + public List children() { return children; } + + @Override + public MeasuredSize measure(Constraints constraints) { + if (children.isEmpty()) return MeasuredSize.ZERO; + + float maxWidth = 0; + float maxHeight = 0; + List sizes = new ArrayList<>(children.size()); + + for (UIComponent child : children) { + MeasuredSize size = child.measure(constraints); + sizes.add(size); + maxWidth = Math.max(maxWidth, size.width()); + maxHeight = Math.max(maxHeight, size.height()); + } + + this.childSizes = sizes; + return MeasuredSize.of( + constraints.constrainWidth(maxWidth), + constraints.constrainHeight(maxHeight) + ); + } + + @Override + public void layout(float x, float y, float width, float height) { + this.x = x; + this.y = y; + this.width = width; + this.height = height; + + for (int i = 0; i < children.size(); i++) { + UIComponent child = children.get(i); + MeasuredSize size = childSizes.get(i); + float childX = x + contentAlignmentH.align(width, size.width()); + float childY = y + contentAlignmentV.align(height, size.height()); + child.layout(childX, childY, size.width(), size.height()); + } + } + + @Override + public void extractRenderState(GuiGraphicsExtractor extractor) { + for (UIComponent child : children) { + child.extractRenderState(extractor); + } + } +} diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/BoxScope.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/BoxScope.java new file mode 100644 index 00000000..aba58454 --- /dev/null +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/BoxScope.java @@ -0,0 +1,9 @@ +package dev.anvilcraft.lib.v2.ui.component; + +import dev.anvilcraft.lib.v2.ui.UIScope; + +/** + * Scope for children inside a {@link BoxComponent}. + */ +public class BoxScope extends UIScope { +} diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ColumnComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ColumnComponent.java index d841df74..1294593f 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ColumnComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ColumnComponent.java @@ -1,9 +1,6 @@ package dev.anvilcraft.lib.v2.ui.component; -import dev.anvilcraft.lib.v2.ui.Constraints; -import dev.anvilcraft.lib.v2.ui.MeasuredSize; -import dev.anvilcraft.lib.v2.ui.Modifier; -import dev.anvilcraft.lib.v2.ui.UIComponent; +import dev.anvilcraft.lib.v2.ui.*; import net.minecraft.client.gui.GuiGraphicsExtractor; import java.util.ArrayList; @@ -11,7 +8,7 @@ import java.util.List; /** - * Vertical linear layout. Children are measured and stacked top-to-bottom. + * 纵向线性布局。子组件自上而下排列。主轴=垂直,交叉轴=水平。 */ public class ColumnComponent implements UIComponent { @@ -19,6 +16,10 @@ public class ColumnComponent implements UIComponent { private List children = Collections.emptyList(); private List childSizes = Collections.emptyList(); + private Arrangement.Vertical verticalArrangement = Arrangement.Vertical.Top; + private Alignment.Horizontal horizontalAlignment = Alignment.Horizontal.Start; + private float spacing; + // layout state private float x, y, width, height; @@ -30,21 +31,32 @@ public void setChildren(List children) { this.children = List.copyOf(children); } - @Override - public Modifier modifier() { - return modifier; + // ── chained setters ── + + public ColumnComponent verticalArrangement(Arrangement.Vertical va) { + this.verticalArrangement = va; + return this; } - @Override - public List children() { - return children; + public ColumnComponent horizontalAlignment(Alignment.Horizontal ha) { + this.horizontalAlignment = ha; + return this; + } + + public ColumnComponent spacing(float spacing) { + this.spacing = spacing; + return this; } + @Override + public Modifier modifier() { return modifier; } + + @Override + public List children() { return children; } + @Override public MeasuredSize measure(Constraints constraints) { - if (children.isEmpty()) { - return MeasuredSize.ZERO; - } + if (children.isEmpty()) return MeasuredSize.ZERO; float totalHeight = 0; float maxWidth = 0; @@ -61,6 +73,7 @@ public MeasuredSize measure(Constraints constraints) { totalHeight += size.height(); maxWidth = Math.max(maxWidth, size.width()); } + totalHeight += spacing * (children.size() - 1); this.childSizes = sizes; return MeasuredSize.of( @@ -76,12 +89,15 @@ public void layout(float x, float y, float width, float height) { this.width = width; this.height = height; - float currentY = y; + List heights = new ArrayList<>(childSizes.size()); + for (MeasuredSize s : childSizes) heights.add(s.height()); + + float[] yOffsets = verticalArrangement.arrange(height, heights, spacing); for (int i = 0; i < children.size(); i++) { UIComponent child = children.get(i); MeasuredSize size = childSizes.get(i); - child.layout(x, currentY, width, size.height()); - currentY += size.height(); + float childX = x + horizontalAlignment.align(width, size.width()); + child.layout(childX, y + yOffsets[i], size.width(), size.height()); } } @@ -92,3 +108,4 @@ public void extractRenderState(GuiGraphicsExtractor extractor) { } } } + diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/RowComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/RowComponent.java new file mode 100644 index 00000000..741f6830 --- /dev/null +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/RowComponent.java @@ -0,0 +1,107 @@ +package dev.anvilcraft.lib.v2.ui.component; + +import dev.anvilcraft.lib.v2.ui.*; +import net.minecraft.client.gui.GuiGraphicsExtractor; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * 横向线性布局。子组件自左而右排列。主轴=水平,交叉轴=垂直。 + */ +public class RowComponent implements UIComponent { + + private final Modifier modifier; + private List children = Collections.emptyList(); + private List childSizes = Collections.emptyList(); + + private Arrangement.Horizontal horizontalArrangement = Arrangement.Horizontal.Start; + private Alignment.Vertical verticalAlignment = Alignment.Vertical.Top; + private float spacing; + + private float x, y, width, height; + + public RowComponent(Modifier modifier) { + this.modifier = modifier; + } + + public void setChildren(List children) { + this.children = List.copyOf(children); + } + + public RowComponent horizontalArrangement(Arrangement.Horizontal ha) { + this.horizontalArrangement = ha; + return this; + } + + public RowComponent verticalAlignment(Alignment.Vertical va) { + this.verticalAlignment = va; + return this; + } + + public RowComponent spacing(float spacing) { + this.spacing = spacing; + return this; + } + + @Override + public Modifier modifier() { return modifier; } + + @Override + public List children() { return children; } + + @Override + public MeasuredSize measure(Constraints constraints) { + if (children.isEmpty()) return MeasuredSize.ZERO; + + float totalWidth = 0; + float maxHeight = 0; + List sizes = new ArrayList<>(children.size()); + + Constraints childConstraints = new Constraints( + 0, Float.MAX_VALUE, + constraints.minHeight(), constraints.maxHeight() + ); + + for (UIComponent child : children) { + MeasuredSize size = child.measure(childConstraints); + sizes.add(size); + totalWidth += size.width(); + maxHeight = Math.max(maxHeight, size.height()); + } + totalWidth += spacing * (children.size() - 1); + + this.childSizes = sizes; + return MeasuredSize.of( + constraints.constrainWidth(totalWidth), + constraints.constrainHeight(maxHeight) + ); + } + + @Override + public void layout(float x, float y, float width, float height) { + this.x = x; + this.y = y; + this.width = width; + this.height = height; + + List widths = new ArrayList<>(childSizes.size()); + for (MeasuredSize s : childSizes) widths.add(s.width()); + + float[] xOffsets = horizontalArrangement.arrange(width, widths, spacing); + for (int i = 0; i < children.size(); i++) { + UIComponent child = children.get(i); + MeasuredSize size = childSizes.get(i); + float childY = y + verticalAlignment.align(height, size.height()); + child.layout(x + xOffsets[i], childY, size.width(), size.height()); + } + } + + @Override + public void extractRenderState(GuiGraphicsExtractor extractor) { + for (UIComponent child : children) { + child.extractRenderState(extractor); + } + } +} diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/RowScope.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/RowScope.java new file mode 100644 index 00000000..b52d0896 --- /dev/null +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/RowScope.java @@ -0,0 +1,9 @@ +package dev.anvilcraft.lib.v2.ui.component; + +import dev.anvilcraft.lib.v2.ui.UIScope; + +/** + * Scope for children inside a {@link RowComponent}. + */ +public class RowScope extends UIScope { +} From ca73b488f852fbab9e8682388292ca421bb9cc13 Mon Sep 17 00:00:00 2001 From: Gugle Date: Mon, 18 May 2026 21:04:56 +0800 Subject: [PATCH 07/67] feat(ui): add @Nullable annotations to enhance null safety in UI components --- .../main/java/dev/anvilcraft/lib/v2/ui/Composition.java | 4 ++++ .../java/dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java | 3 +++ .../main/java/dev/anvilcraft/lib/v2/ui/MutableState.java | 7 +++++-- .../src/main/java/dev/anvilcraft/lib/v2/ui/UIScope.java | 4 +++- .../anvilcraft/lib/v2/ui/component/ButtonComponent.java | 7 +++++-- .../dev/anvilcraft/lib/v2/ui/component/package-info.java | 4 ++++ .../dev/anvilcraft/lib/v2/ui/modifier/package-info.java | 4 ++++ 7 files changed, 28 insertions(+), 5 deletions(-) create mode 100644 module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/package-info.java create mode 100644 module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/package-info.java diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Composition.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Composition.java index f079f3ef..bb12e04d 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Composition.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Composition.java @@ -6,6 +6,8 @@ import java.util.function.Consumer; import java.util.function.Supplier; +import org.jspecify.annotations.Nullable; + /** * The composition engine that drives recomposition, state tracking, and rendering. *

@@ -23,6 +25,7 @@ public class Composition { private static final ThreadLocal CURRENT = new ThreadLocal<>(); /** Returns the composition active on this thread, or null. */ + @Nullable public static Composition currentOrNull() { return CURRENT.get(); } @@ -44,6 +47,7 @@ public static Composition current() { private final Map rememberedValues = new HashMap<>(); /** The slot currently being emitted (set during {@link #emit}). */ + @Nullable Slot currentSlot; // ── state ── diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java index c9ecce7e..fbabdba4 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java @@ -4,6 +4,8 @@ import net.minecraft.client.gui.screens.Screen; import net.minecraft.network.chat.Component; +import org.jspecify.annotations.Nullable; + /** * A {@link Screen} that hosts a declarative UI component tree. *

@@ -28,6 +30,7 @@ */ public abstract class DeclarativeScreen extends Screen { + @Nullable private Composition composition; private final UIScope rootScope = new RootScope(); diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/MutableState.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/MutableState.java index 49be6bfc..2dd172bc 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/MutableState.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/MutableState.java @@ -4,6 +4,8 @@ import java.util.Objects; import java.util.Set; +import org.jspecify.annotations.Nullable; + /** * An observable state holder. *

@@ -17,7 +19,7 @@ public class MutableState { private T value; final Set readers = new HashSet<>(); - public MutableState(T initialValue) { + public MutableState(@Nullable T initialValue) { this.value = initialValue; } @@ -25,6 +27,7 @@ public MutableState(T initialValue) { * Read the current value, recording this slot as a reader * if called within a composition emission. */ + @Nullable public T getValue() { Composition comp = Composition.currentOrNull(); if (comp != null && comp.currentSlot != null) { @@ -37,7 +40,7 @@ public T getValue() { /** * Set a new value. If changed, marks all reader slots dirty. */ - public void setValue(T newValue) { + public void setValue(@Nullable T newValue) { if (!Objects.equals(value, newValue)) { value = newValue; for (Composition.Slot slot : readers) { diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIScope.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIScope.java index f4eeee44..e85d7cab 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIScope.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIScope.java @@ -14,6 +14,8 @@ import java.util.List; import java.util.function.Consumer; +import org.jspecify.annotations.Nullable; + /** * Base scope for building component trees. *

@@ -49,7 +51,7 @@ public TextComponent Text(String text) { return c; } - public ButtonComponent Button(String label, Runnable onClick) { + public ButtonComponent Button(String label, @Nullable Runnable onClick) { ButtonComponent c = new ButtonComponent(Modifier.NONE, label, onClick); addChild(c); Composition.current().emit(c); diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ButtonComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ButtonComponent.java index fce21c4e..e05cae07 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ButtonComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ButtonComponent.java @@ -12,6 +12,8 @@ import java.util.Collections; import java.util.List; +import org.jspecify.annotations.Nullable; + /** * A clickable button with background and label. */ @@ -24,12 +26,13 @@ public class ButtonComponent implements UIComponent { private final Modifier modifier; private String label; + @Nullable private Runnable onClick; // layout state private float x, y, width, height; - public ButtonComponent(Modifier modifier, String label, Runnable onClick) { + public ButtonComponent(Modifier modifier, String label, @Nullable Runnable onClick) { this.modifier = modifier; this.label = label; this.onClick = onClick; @@ -40,7 +43,7 @@ public ButtonComponent label(String label) { return this; } - public ButtonComponent onClick(Runnable onClick) { + public ButtonComponent onClick(@Nullable Runnable onClick) { this.onClick = onClick; return this; } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/package-info.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/package-info.java new file mode 100644 index 00000000..4e8a17af --- /dev/null +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package dev.anvilcraft.lib.v2.ui.component; + +import org.jspecify.annotations.NullMarked; diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/package-info.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/package-info.java new file mode 100644 index 00000000..d422b172 --- /dev/null +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package dev.anvilcraft.lib.v2.ui.modifier; + +import org.jspecify.annotations.NullMarked; From 09f01629af3254a3832576422e14c3c8f33353c7 Mon Sep 17 00:00:00 2001 From: Gugle Date: Mon, 18 May 2026 21:16:54 +0800 Subject: [PATCH 08/67] feat(ui): add ImageComponent and SpacerComponent for enhanced UI layout options --- .../client/screen/DeclarativeTestScreen.java | 51 +++++++++----- module.ui/TODO.md | 8 +-- .../dev/anvilcraft/lib/v2/ui/UIScope.java | 18 +++++ .../lib/v2/ui/component/ButtonComponent.java | 59 ++++++++-------- .../lib/v2/ui/component/ImageComponent.java | 69 +++++++++++++++++++ .../lib/v2/ui/component/SpacerComponent.java | 45 ++++++++++++ .../lib/v2/ui/component/TextComponent.java | 50 ++++++++------ 7 files changed, 225 insertions(+), 75 deletions(-) create mode 100644 module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ImageComponent.java create mode 100644 module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/SpacerComponent.java diff --git a/module.test/src/main/java/dev/anvilcraft/lib/v2/test/client/screen/DeclarativeTestScreen.java b/module.test/src/main/java/dev/anvilcraft/lib/v2/test/client/screen/DeclarativeTestScreen.java index f9338d28..9d76436c 100644 --- a/module.test/src/main/java/dev/anvilcraft/lib/v2/test/client/screen/DeclarativeTestScreen.java +++ b/module.test/src/main/java/dev/anvilcraft/lib/v2/test/client/screen/DeclarativeTestScreen.java @@ -1,12 +1,12 @@ package dev.anvilcraft.lib.v2.test.client.screen; import dev.anvilcraft.lib.v2.ui.*; +import dev.anvilcraft.lib.v2.ui.component.TextComponent; import net.minecraft.network.chat.Component; /** - * End-to-end test screen for the declarative UI system. - * Demonstrates state management, recomposition, Column layout, - * Text rendering, and Button interaction. + * 端到端测试 Screen,展示声明式 UI 的全部 Phase 3+4 功能: + * Column / Row / Box 布局,Text / Button / Spacer 组件,状态管理。 */ public class DeclarativeTestScreen extends DeclarativeScreen { @@ -16,25 +16,38 @@ public DeclarativeTestScreen() { @Override protected void content(UIScope scope) { - // State that survives recomposition MutableState counter = Composition.current() .remember(() -> new MutableState<>(0)); scope.Column(col -> { - col.Text("AnvilLib Declarative UI") - .color(0xFFFFFF00); - - col.Text("") - .text("Counter: " + counter.getValue()); - - col.Button("Increment", () -> - counter.setValue(counter.getValue() + 1)); - - col.Button("Decrement", () -> - counter.setValue(counter.getValue() - 1)); - - col.Button("Reset", () -> - counter.setValue(0)); - }); + col.Text("Declarative UI Demo") + .color(0xFFFFFF00) + .shadow(false); + + col.Spacer(0, 4); + + // Row: 横向按钮栏 + col.Row(row -> { + row.Button("-", () -> counter.setValue(counter.getValue() - 1)); + row.Text(" " + counter.getValue() + " ") + .align(TextComponent.Align.CENTER); + row.Button("+", () -> counter.setValue(counter.getValue() + 1)); + }).spacing(4); + + col.Spacer(0, 8); + + // Box: 叠加 + col.Box(box -> { + box.Text(" "); + box.Text("Count: " + counter.getValue()) + .color(0xFF00FF00) + .shadow(false); + }).contentAlignment(Alignment.Horizontal.Center, Alignment.Vertical.Center); + + col.Spacer(0, 8); + + col.Button("Reset", () -> counter.setValue(0)); + }).spacing(8); } } + diff --git a/module.ui/TODO.md b/module.ui/TODO.md index 7abef1fc..cf55b824 100644 --- a/module.ui/TODO.md +++ b/module.ui/TODO.md @@ -32,10 +32,10 @@ ## Phase 4: 基础组件 -- [ ] `Text` — 文字渲染(Minecraft font) -- [ ] `Button` — 可点击矩形按钮(SdfGraphics 背景 + 文字) -- [ ] `Spacer` — 固定尺寸空白 -- [ ] `Image` — 材质渲染 +- [x] `Text` — 重构:加 shadow(默认开启)、Align LEFT/CENTER/RIGHT、原版默认色 +- [x] `Button` — 重构:原版配色(0xFF404040 / hover 0xFF606060)、shadow 文字、hover 状态预留 +- [x] `Spacer` — 新建:固定尺寸空白占位 +- [x] `Image` — 新建:`blitSprite` 渲染 `Identifier` 纹理 ## Phase 5: Modifier Elements diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIScope.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIScope.java index e85d7cab..ceb716bb 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIScope.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIScope.java @@ -5,10 +5,14 @@ import dev.anvilcraft.lib.v2.ui.component.BoxScope; import dev.anvilcraft.lib.v2.ui.component.ColumnComponent; import dev.anvilcraft.lib.v2.ui.component.ColumnScope; +import dev.anvilcraft.lib.v2.ui.component.ImageComponent; import dev.anvilcraft.lib.v2.ui.component.RowComponent; import dev.anvilcraft.lib.v2.ui.component.RowScope; +import dev.anvilcraft.lib.v2.ui.component.SpacerComponent; import dev.anvilcraft.lib.v2.ui.component.TextComponent; +import net.minecraft.resources.Identifier; + import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -51,6 +55,20 @@ public TextComponent Text(String text) { return c; } + public SpacerComponent Spacer(float width, float height) { + SpacerComponent c = new SpacerComponent(Modifier.NONE, width, height); + addChild(c); + Composition.current().emit(c); + return c; + } + + public ImageComponent Image(Identifier sprite, float width, float height) { + ImageComponent c = new ImageComponent(Modifier.NONE, sprite, width, height); + addChild(c); + Composition.current().emit(c); + return c; + } + public ButtonComponent Button(String label, @Nullable Runnable onClick) { ButtonComponent c = new ButtonComponent(Modifier.NONE, label, onClick); addChild(c); diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ButtonComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ButtonComponent.java index e05cae07..91b8632a 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ButtonComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ButtonComponent.java @@ -8,6 +8,7 @@ import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiGraphicsExtractor; import net.minecraft.network.chat.Component; +import net.minecraft.util.ARGB; import java.util.Collections; import java.util.List; @@ -15,21 +16,25 @@ import org.jspecify.annotations.Nullable; /** - * A clickable button with background and label. + * 可点击按钮。默认样式与原版一致:深灰背景、白色带阴影文字、微圆角。 */ public class ButtonComponent implements UIComponent { - private static final int BG_COLOR = 0xFF555555; - private static final int TEXT_COLOR = 0xFFFFFFFF; - private static final float PADDING_H = 12; - private static final float PADDING_V = 6; + // 原版按钮配色 + private static final int BG_COLOR = 0xFF404040; + private static final int BG_HOVER_COLOR = 0xFF606060; + private static final int TEXT_COLOR = 0xFFFFFFFF; + private static final int SHADOW_COLOR = 0x33000000; + private static final float PADDING_H = 12; + private static final float PADDING_V = 6; + private static final float ROUND_RADIUS = 2; private final Modifier modifier; private String label; @Nullable private Runnable onClick; + private boolean hovered; - // layout state private float x, y, width, height; public ButtonComponent(Modifier modifier, String label, @Nullable Runnable onClick) { @@ -38,25 +43,12 @@ public ButtonComponent(Modifier modifier, String label, @Nullable Runnable onCli this.onClick = onClick; } - public ButtonComponent label(String label) { - this.label = label; - return this; - } + public ButtonComponent label(String label) { this.label = label; return this; } + public ButtonComponent onClick(@Nullable Runnable onClick) { this.onClick = onClick; return this; } + void setHovered(boolean hovered) { this.hovered = hovered; } - public ButtonComponent onClick(@Nullable Runnable onClick) { - this.onClick = onClick; - return this; - } - - @Override - public Modifier modifier() { - return modifier; - } - - @Override - public List children() { - return Collections.emptyList(); - } + @Override public Modifier modifier() { return modifier; } + @Override public List children() { return Collections.emptyList(); } @Override public MeasuredSize measure(Constraints constraints) { @@ -80,21 +72,28 @@ public void layout(float x, float y, float width, float height) { @Override public void extractRenderState(GuiGraphicsExtractor extractor) { + int bg = hovered ? BG_HOVER_COLOR : BG_COLOR; + + // 背景 SdfGraphics.instance .box(x, y, width, height) - .color(BG_COLOR) - .round(4) + .color(bg) + .round(ROUND_RADIUS) .fill() .draw(extractor); + // 文字(带阴影,原版风格) var font = Minecraft.getInstance().font; Component comp = Component.literal(label); - extractor.centeredText(font, comp, (int) (x + width / 2), (int) (y + (height - font.lineHeight) / 2), TEXT_COLOR); + int centerX = (int) (x + width / 2f); + int textY = (int) (y + (height - font.lineHeight) / 2f); + + extractor.centeredText(font, comp, centerX + 1, textY + 1, SHADOW_COLOR); + extractor.centeredText(font, comp, centerX, textY, TEXT_COLOR); } void click() { - if (onClick != null) { - onClick.run(); - } + if (onClick != null) onClick.run(); } } + diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ImageComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ImageComponent.java new file mode 100644 index 00000000..c0dae0da --- /dev/null +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ImageComponent.java @@ -0,0 +1,69 @@ +package dev.anvilcraft.lib.v2.ui.component; + +import dev.anvilcraft.lib.v2.ui.Constraints; +import dev.anvilcraft.lib.v2.ui.MeasuredSize; +import dev.anvilcraft.lib.v2.ui.Modifier; +import dev.anvilcraft.lib.v2.ui.UIComponent; +import net.minecraft.client.renderer.RenderPipelines; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.resources.Identifier; + +import java.util.Collections; +import java.util.List; + +/** + * 渲染一个材质精灵(sprite)。 + * 通过 {@link GuiGraphicsExtractor#blitSprite} 使用原版纹理管线。 + */ +public class ImageComponent implements UIComponent { + + private final Modifier modifier; + private Identifier sprite; + private float imageWidth; + private float imageHeight; + + private float x, y, width, height; + + public ImageComponent(Modifier modifier, Identifier sprite, float imageWidth, float imageHeight) { + this.modifier = modifier; + this.sprite = sprite; + this.imageWidth = imageWidth; + this.imageHeight = imageHeight; + } + + public ImageComponent sprite(Identifier sprite) { this.sprite = sprite; return this; } + + @Override public Modifier modifier() { return modifier; } + @Override public List children() { return Collections.emptyList(); } + + @Override + public MeasuredSize measure(Constraints constraints) { + return MeasuredSize.of( + constraints.constrainWidth(imageWidth), + constraints.constrainHeight(imageHeight) + ); + } + + @Override + public void layout(float x, float y, float width, float height) { + this.x = x; + this.y = y; + this.width = width; + this.height = height; + } + + @Override + public void extractRenderState(GuiGraphicsExtractor extractor) { + extractor.blitSprite( + RenderPipelines.GUI_TEXTURED, + sprite, + (int) x, (int) y, + (int) width, (int) height + ); + } + + /** 获取组件逻辑宽度(可能被 modifier 修改后不同)。 */ + float imageWidth() { return imageWidth; } + /** 获取组件逻辑高度。 */ + float imageHeight() { return imageHeight; } +} diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/SpacerComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/SpacerComponent.java new file mode 100644 index 00000000..3ebd16b1 --- /dev/null +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/SpacerComponent.java @@ -0,0 +1,45 @@ +package dev.anvilcraft.lib.v2.ui.component; + +import dev.anvilcraft.lib.v2.ui.Constraints; +import dev.anvilcraft.lib.v2.ui.MeasuredSize; +import dev.anvilcraft.lib.v2.ui.Modifier; +import dev.anvilcraft.lib.v2.ui.UIComponent; +import net.minecraft.client.gui.GuiGraphicsExtractor; + +import java.util.Collections; +import java.util.List; + +/** + * 固定尺寸的空白占位组件,不渲染任何内容。 + */ +public class SpacerComponent implements UIComponent { + + private final Modifier modifier; + private final float spacerWidth; + private final float spacerHeight; + + public SpacerComponent(Modifier modifier, float width, float height) { + this.modifier = modifier; + this.spacerWidth = width; + this.spacerHeight = height; + } + + @Override public Modifier modifier() { return modifier; } + @Override public List children() { return Collections.emptyList(); } + + @Override + public MeasuredSize measure(Constraints constraints) { + return MeasuredSize.of( + constraints.constrainWidth(spacerWidth), + constraints.constrainHeight(spacerHeight) + ); + } + + @Override + public void layout(float x, float y, float width, float height) { + } + + @Override + public void extractRenderState(GuiGraphicsExtractor extractor) { + } +} diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextComponent.java index 7d9a3a3c..e1aeaebe 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextComponent.java @@ -12,15 +12,21 @@ import java.util.List; /** - * Renders a single line of Minecraft text. + * 单行文字渲染。默认样式与原版一致:白色带阴影、左对齐。 */ public class TextComponent implements UIComponent { + /** 文字水平对齐方式 */ + public enum Align { LEFT, CENTER, RIGHT } + + private static final int VANILLA_TEXT_COLOR = 0xFFFFFFFF; + private final Modifier modifier; private String text; - private int color = 0xFFFFFFFF; + private int color = VANILLA_TEXT_COLOR; + private boolean dropShadow = true; + private Align align = Align.LEFT; - // layout state private float x, y, width, height; public TextComponent(Modifier modifier, String text) { @@ -28,25 +34,13 @@ public TextComponent(Modifier modifier, String text) { this.text = text; } - public TextComponent text(String text) { - this.text = text; - return this; - } - - public TextComponent color(int color) { - this.color = color; - return this; - } - - @Override - public Modifier modifier() { - return modifier; - } + public TextComponent text(String text) { this.text = text; return this; } + public TextComponent color(int color) { this.color = color; return this; } + public TextComponent shadow(boolean enable) { this.dropShadow = enable; return this; } + public TextComponent align(Align align) { this.align = align; return this; } - @Override - public List children() { - return Collections.emptyList(); - } + @Override public Modifier modifier() { return modifier; } + @Override public List children() { return Collections.emptyList(); } @Override public MeasuredSize measure(Constraints constraints) { @@ -68,6 +62,18 @@ public void layout(float x, float y, float width, float height) { public void extractRenderState(GuiGraphicsExtractor extractor) { var font = Minecraft.getInstance().font; Component component = Component.literal(text); - extractor.centeredText(font, component, (int) (x + width / 2), (int) y, color); + float textW = font.width(component); + + float renderX = (float) switch (align) { + case LEFT -> x; + case CENTER -> x + (width - textW) / 2f; + case RIGHT -> x + width - textW; + }; + + if (dropShadow) { + extractor.text(font, text, (int) renderX + 1, (int) y + 1, 0x33000000); + } + extractor.text(font, text, (int) renderX, (int) y, color); } } + From 261ae3fc3c702884cbdc97886880f2764b3efecb Mon Sep 17 00:00:00 2001 From: Gugle Date: Mon, 18 May 2026 21:18:50 +0800 Subject: [PATCH 09/67] feat(ci): add rendering dependency to UI workflows for improved build order --- .github/workflows/ci.yml | 2 ++ .github/workflows/pull_request.yml | 2 ++ .github/workflows/release.yml | 2 ++ 3 files changed, 6 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 81e7abf3..10185e2b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -357,6 +357,8 @@ jobs: signing_public_key: ${{ secrets.SIGNING_PUBLIC_KEY }} signing_password: ${{ secrets.SIGNING_PASSWORD }} ui: + needs: + - rendering uses: ./.github/workflows/build_and_test.yml with: module: ui diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 599543c3..bf7cd9cf 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -136,6 +136,8 @@ jobs: ci_build: true pr_build: true ui: + needs: + - rendering uses: ./.github/workflows/build_and_test.yml with: module: ui diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 42e27b3c..a61065e2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -357,6 +357,8 @@ jobs: signing_public_key: ${{ secrets.SIGNING_PUBLIC_KEY }} signing_password: ${{ secrets.SIGNING_PASSWORD }} ui: + needs: + - rendering uses: ./.github/workflows/build_and_test.yml with: module: ui From 352b3941fa8f3f8f8da65b2537653ca151a0450f Mon Sep 17 00:00:00 2001 From: Gugle Date: Mon, 18 May 2026 21:27:45 +0800 Subject: [PATCH 10/67] feat(ui): add modifier support to UI components and enhance background and border elements --- module.ui/TODO.md | 12 +++++----- .../dev/anvilcraft/lib/v2/ui/Modifier.java | 8 +++++++ .../lib/v2/ui/component/BoxComponent.java | 3 ++- .../lib/v2/ui/component/ButtonComponent.java | 3 ++- .../lib/v2/ui/component/ColumnComponent.java | 7 +++++- .../lib/v2/ui/component/ImageComponent.java | 3 ++- .../lib/v2/ui/component/RowComponent.java | 7 +++++- .../lib/v2/ui/component/SpacerComponent.java | 4 +++- .../lib/v2/ui/component/TextComponent.java | 3 ++- .../lib/v2/ui/modifier/BackgroundElement.java | 8 ++++++- .../lib/v2/ui/modifier/BorderElement.java | 23 +++++++++++++++++++ .../lib/v2/ui/modifier/ModifierElement.java | 14 ++++++++++- 12 files changed, 80 insertions(+), 15 deletions(-) create mode 100644 module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/BorderElement.java diff --git a/module.ui/TODO.md b/module.ui/TODO.md index cf55b824..d8c52ce2 100644 --- a/module.ui/TODO.md +++ b/module.ui/TODO.md @@ -39,12 +39,12 @@ ## Phase 5: Modifier Elements -- [ ] `SizeModifier` — `.size(width, height)` `.fillMaxWidth()` `.fillMaxSize()` -- [ ] `PaddingModifier` — `.padding(all)` `.padding(horizontal, vertical)` -- [ ] `BackgroundModifier` — `.background(color)` → SdfGraphics.box -- [ ] `BorderModifier` — `.border(width, color)` → SdfGraphics.stroke -- [ ] `RoundedCornerModifier` — `.roundedCorner(radius)` → SdfGraphics.round -- [ ] `ClickModifier` — `.onClick { }` + hit testing +- [x] `SizeElement` — `.size()` `.fillMaxWidth()` `.fillMaxHeight()` `.fillMaxSize()` +- [x] `PaddingElement` — `.padding(all)` `.padding(horizontal, vertical)` +- [x] `BackgroundElement` — `.background(color)` → SdfGraphics.box(支持 round) +- [x] `BorderElement` — `.border(width, color)` → SdfGraphics.stroke(支持 round) +- [x] 所有组件增加 `.modifier(Modifier)` setter(`modifier` 字段改为可变) +- [x] `ClickElement` — 推迟到 Phase 6 与输入路由一起实现 ## Phase 6: 屏幕集成 diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Modifier.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Modifier.java index e77877d5..42520c75 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Modifier.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Modifier.java @@ -62,6 +62,14 @@ default Modifier background(int color) { return prepend(ModifierElement.background(color)); } + default Modifier border(float width, int color) { + return prepend(ModifierElement.border(width, color)); + } + + default Modifier border(float width, int color, float round) { + return prepend(ModifierElement.border(width, color, round)); + } + Modifier NONE = new Modifier() { @Override public Modifier then(Modifier other) { diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/BoxComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/BoxComponent.java index 678a73f8..1964585a 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/BoxComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/BoxComponent.java @@ -13,7 +13,7 @@ */ public class BoxComponent implements UIComponent { - private final Modifier modifier; + private Modifier modifier; private List children = Collections.emptyList(); private List childSizes = Collections.emptyList(); @@ -35,6 +35,7 @@ public BoxComponent contentAlignment(Alignment.Horizontal h, Alignment.Vertical this.contentAlignmentV = v; return this; } + public BoxComponent modifier(Modifier m) { this.modifier = m; return this; } @Override public Modifier modifier() { return modifier; } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ButtonComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ButtonComponent.java index 91b8632a..10b5ee5d 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ButtonComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ButtonComponent.java @@ -29,7 +29,7 @@ public class ButtonComponent implements UIComponent { private static final float PADDING_V = 6; private static final float ROUND_RADIUS = 2; - private final Modifier modifier; + private Modifier modifier; private String label; @Nullable private Runnable onClick; @@ -45,6 +45,7 @@ public ButtonComponent(Modifier modifier, String label, @Nullable Runnable onCli public ButtonComponent label(String label) { this.label = label; return this; } public ButtonComponent onClick(@Nullable Runnable onClick) { this.onClick = onClick; return this; } + public ButtonComponent modifier(Modifier m) { this.modifier = m; return this; } void setHovered(boolean hovered) { this.hovered = hovered; } @Override public Modifier modifier() { return modifier; } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ColumnComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ColumnComponent.java index 1294593f..f7f9c5ee 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ColumnComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ColumnComponent.java @@ -12,7 +12,7 @@ */ public class ColumnComponent implements UIComponent { - private final Modifier modifier; + private Modifier modifier; private List children = Collections.emptyList(); private List childSizes = Collections.emptyList(); @@ -48,6 +48,11 @@ public ColumnComponent spacing(float spacing) { return this; } + public ColumnComponent modifier(Modifier m) { + this.modifier = m; + return this; + } + @Override public Modifier modifier() { return modifier; } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ImageComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ImageComponent.java index c0dae0da..bb14d89c 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ImageComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ImageComponent.java @@ -17,7 +17,7 @@ */ public class ImageComponent implements UIComponent { - private final Modifier modifier; + private Modifier modifier; private Identifier sprite; private float imageWidth; private float imageHeight; @@ -32,6 +32,7 @@ public ImageComponent(Modifier modifier, Identifier sprite, float imageWidth, fl } public ImageComponent sprite(Identifier sprite) { this.sprite = sprite; return this; } + public ImageComponent modifier(Modifier m) { this.modifier = m; return this; } @Override public Modifier modifier() { return modifier; } @Override public List children() { return Collections.emptyList(); } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/RowComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/RowComponent.java index 741f6830..88dfab93 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/RowComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/RowComponent.java @@ -12,7 +12,7 @@ */ public class RowComponent implements UIComponent { - private final Modifier modifier; + private Modifier modifier; private List children = Collections.emptyList(); private List childSizes = Collections.emptyList(); @@ -45,6 +45,11 @@ public RowComponent spacing(float spacing) { return this; } + public RowComponent modifier(Modifier m) { + this.modifier = m; + return this; + } + @Override public Modifier modifier() { return modifier; } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/SpacerComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/SpacerComponent.java index 3ebd16b1..6c16d316 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/SpacerComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/SpacerComponent.java @@ -14,7 +14,7 @@ */ public class SpacerComponent implements UIComponent { - private final Modifier modifier; + private Modifier modifier; private final float spacerWidth; private final float spacerHeight; @@ -27,6 +27,8 @@ public SpacerComponent(Modifier modifier, float width, float height) { @Override public Modifier modifier() { return modifier; } @Override public List children() { return Collections.emptyList(); } + public SpacerComponent modifier(Modifier m) { this.modifier = m; return this; } + @Override public MeasuredSize measure(Constraints constraints) { return MeasuredSize.of( diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextComponent.java index e1aeaebe..415650b7 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextComponent.java @@ -21,7 +21,7 @@ public enum Align { LEFT, CENTER, RIGHT } private static final int VANILLA_TEXT_COLOR = 0xFFFFFFFF; - private final Modifier modifier; + private Modifier modifier; private String text; private int color = VANILLA_TEXT_COLOR; private boolean dropShadow = true; @@ -38,6 +38,7 @@ public TextComponent(Modifier modifier, String text) { public TextComponent color(int color) { this.color = color; return this; } public TextComponent shadow(boolean enable) { this.dropShadow = enable; return this; } public TextComponent align(Align align) { this.align = align; return this; } + public TextComponent modifier(Modifier m) { this.modifier = m; return this; } @Override public Modifier modifier() { return modifier; } @Override public List children() { return Collections.emptyList(); } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/BackgroundElement.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/BackgroundElement.java index 4650361e..ae8019f3 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/BackgroundElement.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/BackgroundElement.java @@ -4,13 +4,19 @@ import dev.anvilcraft.lib.v2.ui.LayoutRect; import net.minecraft.client.gui.GuiGraphicsExtractor; -public record BackgroundElement(int color) implements ModifierElement { +/** 渲染填充圆角矩形背景。 */ +public record BackgroundElement(int color, float round) implements ModifierElement { + + public BackgroundElement(int color) { + this(color, 0); + } @Override public void emitRenderState(GuiGraphicsExtractor extractor, LayoutRect bounds) { SdfGraphics.instance .box(bounds.x(), bounds.y(), bounds.width(), bounds.height()) .color(color) + .round(round) .fill() .draw(extractor); } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/BorderElement.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/BorderElement.java new file mode 100644 index 00000000..5f056c74 --- /dev/null +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/BorderElement.java @@ -0,0 +1,23 @@ +package dev.anvilcraft.lib.v2.ui.modifier; + +import dev.anvilcraft.lib.v2.rendering.sdf.SdfGraphics; +import dev.anvilcraft.lib.v2.ui.LayoutRect; +import net.minecraft.client.gui.GuiGraphicsExtractor; + +/** 渲染描边圆角矩形边框。 */ +public record BorderElement(float width, int color, float round) implements ModifierElement { + + public BorderElement(float width, int color) { + this(width, color, 0); + } + + @Override + public void emitRenderState(GuiGraphicsExtractor extractor, LayoutRect bounds) { + SdfGraphics.instance + .box(bounds.x(), bounds.y(), bounds.width(), bounds.height()) + .color(color) + .round(round) + .stroke(width) + .draw(extractor); + } +} diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/ModifierElement.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/ModifierElement.java index a177e922..270c3332 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/ModifierElement.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/ModifierElement.java @@ -62,6 +62,18 @@ static ModifierElement padding(float horizontal, float vertical) { } static ModifierElement background(int color) { - return new BackgroundElement(color); + return new BackgroundElement(color, 0); + } + + static ModifierElement background(int color, float round) { + return new BackgroundElement(color, round); + } + + static ModifierElement border(float width, int color) { + return new BorderElement(width, color, 0); + } + + static ModifierElement border(float width, int color, float round) { + return new BorderElement(width, color, round); } } From 54a65a8c0849ab3f554e9c5bf6ae60b5750ff325 Mon Sep 17 00:00:00 2001 From: Gugle Date: Mon, 18 May 2026 22:03:42 +0800 Subject: [PATCH 11/67] feat(ui): enhance ButtonComponent with precise rendering and hit testing --- .../client/screen/DeclarativeTestScreen.java | 6 +-- .../lib/v2/ui/DeclarativeScreen.java | 40 +++++++++++++++--- .../lib/v2/ui/component/ButtonComponent.java | 41 +++++++++---------- .../lib/v2/ui/component/TextComponent.java | 2 +- 4 files changed, 56 insertions(+), 33 deletions(-) diff --git a/module.test/src/main/java/dev/anvilcraft/lib/v2/test/client/screen/DeclarativeTestScreen.java b/module.test/src/main/java/dev/anvilcraft/lib/v2/test/client/screen/DeclarativeTestScreen.java index 9d76436c..704046e9 100644 --- a/module.test/src/main/java/dev/anvilcraft/lib/v2/test/client/screen/DeclarativeTestScreen.java +++ b/module.test/src/main/java/dev/anvilcraft/lib/v2/test/client/screen/DeclarativeTestScreen.java @@ -21,8 +21,7 @@ protected void content(UIScope scope) { scope.Column(col -> { col.Text("Declarative UI Demo") - .color(0xFFFFFF00) - .shadow(false); + .color(0xFFFFFF00); col.Spacer(0, 4); @@ -40,8 +39,7 @@ protected void content(UIScope scope) { col.Box(box -> { box.Text(" "); box.Text("Count: " + counter.getValue()) - .color(0xFF00FF00) - .shadow(false); + .color(0xFF00FF00); }).contentAlignment(Alignment.Horizontal.Center, Alignment.Vertical.Center); col.Spacer(0, 8); diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java index fbabdba4..4568356a 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java @@ -1,7 +1,9 @@ package dev.anvilcraft.lib.v2.ui; +import dev.anvilcraft.lib.v2.ui.component.ButtonComponent; import net.minecraft.client.gui.GuiGraphicsExtractor; import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.input.MouseButtonEvent; import net.minecraft.network.chat.Component; import org.jspecify.annotations.Nullable; @@ -33,6 +35,7 @@ public abstract class DeclarativeScreen extends Screen { @Nullable private Composition composition; private final UIScope rootScope = new RootScope(); + private int lastMouseX, lastMouseY; protected DeclarativeScreen(Component title) { super(title); @@ -44,23 +47,48 @@ protected void init() { composition.setContent(this::content); } - /** - * Declare the UI content. Called on initial composition and every recomposition. - * Use {@link Composition#current()} to access state helpers like {@code remember}. - */ protected abstract void content(UIScope scope); @Override public void extractRenderState(GuiGraphicsExtractor extractor, int mouseX, int mouseY, float partialTick) { super.extractRenderState(extractor, mouseX, mouseY, partialTick); + lastMouseX = mouseX; + lastMouseY = mouseY; if (composition != null) { composition.renderFrame(extractor, this.width, this.height); } } - // ── input routing (stub — Phase 5 adds full hit-testing) ── + // ── input routing ── - // TODO Phase 5: override mouseClicked(MouseButtonEvent, boolean), keyPressed(KeyEvent) + @Override + public boolean mouseClicked(MouseButtonEvent event, boolean isDoubleClick) { + if (event.button() == 0) { // 左键 + for (UIComponent child : rootScope.getChildren()) { + if (hitTest(child, lastMouseX, lastMouseY)) { + return true; + } + } + } + return super.mouseClicked(event, isDoubleClick); + } + + /** 递归命中测试,找到最上层可点击组件并触发 click()。 */ + private boolean hitTest(UIComponent component, float px, float py) { + // 先检查子组件(后绘制在上层,优先命中) + var children = component.children(); + for (int i = children.size() - 1; i >= 0; i--) { + if (hitTest(children.get(i), px, py)) return true; + } + // 再检查自身 + if (component instanceof ButtonComponent btn) { + if (btn.hitRect().contains(px, py)) { + btn.click(); + return true; + } + } + return false; + } // ── internal ── diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ButtonComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ButtonComponent.java index 10b5ee5d..f112fba3 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ButtonComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ButtonComponent.java @@ -1,14 +1,12 @@ package dev.anvilcraft.lib.v2.ui.component; -import dev.anvilcraft.lib.v2.rendering.sdf.SdfGraphics; import dev.anvilcraft.lib.v2.ui.Constraints; +import dev.anvilcraft.lib.v2.ui.LayoutRect; import dev.anvilcraft.lib.v2.ui.MeasuredSize; import dev.anvilcraft.lib.v2.ui.Modifier; import dev.anvilcraft.lib.v2.ui.UIComponent; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiGraphicsExtractor; -import net.minecraft.network.chat.Component; -import net.minecraft.util.ARGB; import java.util.Collections; import java.util.List; @@ -16,7 +14,7 @@ import org.jspecify.annotations.Nullable; /** - * 可点击按钮。默认样式与原版一致:深灰背景、白色带阴影文字、微圆角。 + * 可点击按钮。原版 fill() 背景 + 手动居中 text() 文字。 */ public class ButtonComponent implements UIComponent { @@ -24,10 +22,8 @@ public class ButtonComponent implements UIComponent { private static final int BG_COLOR = 0xFF404040; private static final int BG_HOVER_COLOR = 0xFF606060; private static final int TEXT_COLOR = 0xFFFFFFFF; - private static final int SHADOW_COLOR = 0x33000000; private static final float PADDING_H = 12; private static final float PADDING_V = 6; - private static final float ROUND_RADIUS = 2; private Modifier modifier; private String label; @@ -54,8 +50,7 @@ public ButtonComponent(Modifier modifier, String label, @Nullable Runnable onCli @Override public MeasuredSize measure(Constraints constraints) { var font = Minecraft.getInstance().font; - Component comp = Component.literal(label); - float textW = font.width(comp); + float textW = font.width(label); float textH = font.lineHeight; return MeasuredSize.of( constraints.constrainWidth(textW + PADDING_H * 2), @@ -74,26 +69,28 @@ public void layout(float x, float y, float width, float height) { @Override public void extractRenderState(GuiGraphicsExtractor extractor) { int bg = hovered ? BG_HOVER_COLOR : BG_COLOR; + int ix = (int) x, iy = (int) y, iw = (int) width, ih = (int) height; - // 背景 - SdfGraphics.instance - .box(x, y, width, height) - .color(bg) - .round(ROUND_RADIUS) - .fill() - .draw(extractor); + // 背景 — 用原版 fill,精确定位 + extractor.fill(ix, iy, ix + iw, iy + ih, bg); - // 文字(带阴影,原版风格) + // 文字 — text() 手动居中 var font = Minecraft.getInstance().font; - Component comp = Component.literal(label); - int centerX = (int) (x + width / 2f); - int textY = (int) (y + (height - font.lineHeight) / 2f); + String txt = label; + float textW = font.width(txt); + int textX = (int) (x + (width - textW) / 2f); + int textY = (int) (y + (height + font.lineHeight) / 2f - 1); - extractor.centeredText(font, comp, centerX + 1, textY + 1, SHADOW_COLOR); - extractor.centeredText(font, comp, centerX, textY, TEXT_COLOR); + extractor.text(font, txt, textX, textY, TEXT_COLOR); } - void click() { + /** 按钮包围盒,用于命中测试。 */ + public LayoutRect hitRect() { + return LayoutRect.of(x, y, width, height); + } + + /** 触发点击回调。 */ + public void click() { if (onClick != null) onClick.run(); } } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextComponent.java index 415650b7..b7a0035b 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextComponent.java @@ -24,7 +24,7 @@ public enum Align { LEFT, CENTER, RIGHT } private Modifier modifier; private String text; private int color = VANILLA_TEXT_COLOR; - private boolean dropShadow = true; + private boolean dropShadow; private Align align = Align.LEFT; private float x, y, width, height; From 0bc1363a086b5289343a509084f611f955b1b849 Mon Sep 17 00:00:00 2001 From: Gugle Date: Mon, 18 May 2026 22:07:35 +0800 Subject: [PATCH 12/67] feat(ui): fix text vertical alignment in ButtonComponent for improved readability --- .../dev/anvilcraft/lib/v2/ui/component/ButtonComponent.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ButtonComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ButtonComponent.java index f112fba3..fd7f3d4f 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ButtonComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ButtonComponent.java @@ -79,7 +79,7 @@ public void extractRenderState(GuiGraphicsExtractor extractor) { String txt = label; float textW = font.width(txt); int textX = (int) (x + (width - textW) / 2f); - int textY = (int) (y + (height + font.lineHeight) / 2f - 1); + int textY = (int) (y + (height - font.lineHeight) / 2f); extractor.text(font, txt, textX, textY, TEXT_COLOR); } From 4fdbb59e0994d945bc4e49af608b6c69aef4f4ff Mon Sep 17 00:00:00 2001 From: Gugle Date: Mon, 18 May 2026 22:11:24 +0800 Subject: [PATCH 13/67] feat(ui): fix text vertical alignment in ButtonComponent for improved readability --- module.ui/TODO.md | 8 +- .../lib/v2/ui/DeclarativeScreen.java | 90 ++++++++++++------- .../lib/v2/ui/component/ButtonComponent.java | 2 +- 3 files changed, 64 insertions(+), 36 deletions(-) diff --git a/module.ui/TODO.md b/module.ui/TODO.md index d8c52ce2..af72905a 100644 --- a/module.ui/TODO.md +++ b/module.ui/TODO.md @@ -48,9 +48,11 @@ ## Phase 6: 屏幕集成 -- [ ] `DeclarativeScreen` — `Screen` 子类,宿主组件树 -- [ ] `extractRenderState()` — dirty check → recompose → measure → layout → submit render states -- [ ] 输入事件路由 — `mouseClicked` / `keyPressed` hit testing + dispatch +- [x] `DeclarativeScreen` — Screen 子类,每帧 dirty check → recompose → measure → layout → render +- [x] `extractRenderState()` — 整合 Composition.renderFrame + hover 更新 +- [x] 鼠标事件 — `mouseClicked` 命中测试 + 点击分发;hover 遍历更新 ButtonComponent +- [x] 键盘事件 — `keyPressed` (ESC 关闭) +- [x] 滚轮事件 — `mouseScrolled` 预留接口 ## Phase 7: 输入组件 diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java index 4568356a..b2fe0a4b 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java @@ -3,32 +3,17 @@ import dev.anvilcraft.lib.v2.ui.component.ButtonComponent; import net.minecraft.client.gui.GuiGraphicsExtractor; import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.input.KeyEvent; import net.minecraft.client.input.MouseButtonEvent; import net.minecraft.network.chat.Component; import org.jspecify.annotations.Nullable; /** - * A {@link Screen} that hosts a declarative UI component tree. + * 声明式 UI 的 {@link Screen} 宿主。 *

- * Subclasses implement {@link #content(UIScope)} to declare the UI. - * The composition is automatically managed each frame. - * - *

{@code
- * public class MyScreen extends DeclarativeScreen {
- *     public MyScreen() { super(Component.literal("My UI")); }
- *
- *     protected void content(UIScope scope) {
- *         var count = Composition.current().remember(
- *             () -> new MutableState<>(0)
- *         );
- *         Column(scope, col -> {
- *             col.Text("Count: " + count.getValue());
- *             col.Button("+", () -> count.setValue(count.getValue() + 1));
- *         });
- *     }
- * }
- * }
+ * 每帧自动完成:dirty check → recompose → measure → layout → render states。 + * 输入事件(点击、按键、滚轮)通过命中测试路由到对应组件。 */ public abstract class DeclarativeScreen extends Screen { @@ -47,8 +32,11 @@ protected void init() { composition.setContent(this::content); } + /** 声明 UI 内容。初始组合和每次 recompose 时调用。 */ protected abstract void content(UIScope scope); + // ── 每帧渲染 ── + @Override public void extractRenderState(GuiGraphicsExtractor extractor, int mouseX, int mouseY, float partialTick) { super.extractRenderState(extractor, mouseX, mouseY, partialTick); @@ -56,16 +44,17 @@ public void extractRenderState(GuiGraphicsExtractor extractor, int mouseX, int m lastMouseY = mouseY; if (composition != null) { composition.renderFrame(extractor, this.width, this.height); + updateHover(mouseX, mouseY); } } - // ── input routing ── + // ── 鼠标输入 ── @Override public boolean mouseClicked(MouseButtonEvent event, boolean isDoubleClick) { - if (event.button() == 0) { // 左键 + if (event.button() == 0) { for (UIComponent child : rootScope.getChildren()) { - if (hitTest(child, lastMouseX, lastMouseY)) { + if (hitTestClick(child, lastMouseX, lastMouseY)) { return true; } } @@ -73,25 +62,62 @@ public boolean mouseClicked(MouseButtonEvent event, boolean isDoubleClick) { return super.mouseClicked(event, isDoubleClick); } - /** 递归命中测试,找到最上层可点击组件并触发 click()。 */ - private boolean hitTest(UIComponent component, float px, float py) { - // 先检查子组件(后绘制在上层,优先命中) + @Override + public boolean mouseScrolled(double mouseX, double mouseY, double scrollX, double scrollY) { + // 后续 Phase: 路由到 ScrollComponent + return super.mouseScrolled(mouseX, mouseY, scrollX, scrollY); + } + + // ── 键盘输入 ── + + @Override + public boolean keyPressed(KeyEvent event) { + if (event.key() == 256) { // ESC — 关闭 Screen + onClose(); + return true; + } + return super.keyPressed(event); + } + + @Override + public void onClose() { + super.onClose(); + } + + // ── 命中测试 ── + + /** 命中测试 + 点击触发。子组件优先(后绘制在上层)。 */ + private boolean hitTestClick(UIComponent component, float px, float py) { var children = component.children(); for (int i = children.size() - 1; i >= 0; i--) { - if (hitTest(children.get(i), px, py)) return true; + if (hitTestClick(children.get(i), px, py)) return true; } - // 再检查自身 - if (component instanceof ButtonComponent btn) { - if (btn.hitRect().contains(px, py)) { - btn.click(); - return true; - } + if (component instanceof ButtonComponent btn && btn.hitRect().contains(px, py)) { + btn.click(); + return true; } return false; } + /** 遍历组件树,更新 ButtonComponent 的 hover 状态。 */ + private void updateHover(float mouseX, float mouseY) { + for (UIComponent child : rootScope.getChildren()) { + updateHoverRecursive(child, mouseX, mouseY); + } + } + + private void updateHoverRecursive(UIComponent component, float mx, float my) { + if (component instanceof ButtonComponent btn) { + btn.setHovered(btn.hitRect().contains(mx, my)); + } + for (UIComponent child : component.children()) { + updateHoverRecursive(child, mx, my); + } + } + // ── internal ── private static class RootScope extends UIScope { } } + diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ButtonComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ButtonComponent.java index fd7f3d4f..fcb90753 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ButtonComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ButtonComponent.java @@ -42,7 +42,7 @@ public ButtonComponent(Modifier modifier, String label, @Nullable Runnable onCli public ButtonComponent label(String label) { this.label = label; return this; } public ButtonComponent onClick(@Nullable Runnable onClick) { this.onClick = onClick; return this; } public ButtonComponent modifier(Modifier m) { this.modifier = m; return this; } - void setHovered(boolean hovered) { this.hovered = hovered; } + public void setHovered(boolean hovered) { this.hovered = hovered; } @Override public Modifier modifier() { return modifier; } @Override public List children() { return Collections.emptyList(); } From da0600cf2d5c6483def3966d73a2e48bfae1f3b7 Mon Sep 17 00:00:00 2001 From: Gugle Date: Mon, 18 May 2026 22:44:46 +0800 Subject: [PATCH 14/67] feat(ui): optimize composition rendering and improve mouse click handling --- .../dev/anvilcraft/lib/v2/ui/Composition.java | 20 ++++++++++++------- .../lib/v2/ui/DeclarativeScreen.java | 12 +++++++---- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Composition.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Composition.java index bb12e04d..9560cef2 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Composition.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Composition.java @@ -117,7 +117,7 @@ public void emit(UIComponent component) { public void renderFrame(GuiGraphicsExtractor extractor, float screenWidth, float screenHeight) { CURRENT.set(this); try { - if (dirty) { + if (dirty || hasDirtySlots()) { recompose(); dirty = false; } @@ -136,15 +136,21 @@ private void recompose() { rootScope.clearChildren(); currentIndex = 0; currentRememberKey = 0; + // Clear slot dirty flags before recompose + for (Slot slot : slots) { + slot.dirty = false; + } content.accept(rootScope); - // Remove slots beyond currentIndex (conditionally removed components) while (slots.size() > currentIndex) { - Slot removed = slots.remove(slots.size() - 1); - // Purge remembered values that belong to removed slots. - // A simple approach: remembered values for keys beyond - // currentRememberKey are dead; we leave them as they'll - // be overwritten on next composition anyway. + slots.remove(slots.size() - 1); + } + } + + private boolean hasDirtySlots() { + for (Slot slot : slots) { + if (slot.dirty) return true; } + return false; } // ── measure → layout → render walk ── diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java index b2fe0a4b..2abe5295 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java @@ -1,11 +1,14 @@ package dev.anvilcraft.lib.v2.ui; import dev.anvilcraft.lib.v2.ui.component.ButtonComponent; +import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiGraphicsExtractor; import net.minecraft.client.gui.screens.Screen; import net.minecraft.client.input.KeyEvent; import net.minecraft.client.input.MouseButtonEvent; +import net.minecraft.client.resources.sounds.SimpleSoundInstance; import net.minecraft.network.chat.Component; +import net.minecraft.sounds.SoundEvents; import org.jspecify.annotations.Nullable; @@ -20,7 +23,6 @@ public abstract class DeclarativeScreen extends Screen { @Nullable private Composition composition; private final UIScope rootScope = new RootScope(); - private int lastMouseX, lastMouseY; protected DeclarativeScreen(Component title) { super(title); @@ -40,8 +42,6 @@ protected void init() { @Override public void extractRenderState(GuiGraphicsExtractor extractor, int mouseX, int mouseY, float partialTick) { super.extractRenderState(extractor, mouseX, mouseY, partialTick); - lastMouseX = mouseX; - lastMouseY = mouseY; if (composition != null) { composition.renderFrame(extractor, this.width, this.height); updateHover(mouseX, mouseY); @@ -53,8 +53,12 @@ public void extractRenderState(GuiGraphicsExtractor extractor, int mouseX, int m @Override public boolean mouseClicked(MouseButtonEvent event, boolean isDoubleClick) { if (event.button() == 0) { + var mc = Minecraft.getInstance(); + int mx = (int) mc.mouseHandler.getScaledXPos(mc.getWindow()); + int my = (int) mc.mouseHandler.getScaledYPos(mc.getWindow()); for (UIComponent child : rootScope.getChildren()) { - if (hitTestClick(child, lastMouseX, lastMouseY)) { + if (hitTestClick(child, mx, my)) { + mc.getSoundManager().play(SimpleSoundInstance.forUI(SoundEvents.UI_BUTTON_CLICK, 1.0f)); return true; } } From cf830541e954d3976e6abfa65e85d278cce5352c Mon Sep 17 00:00:00 2001 From: Gugle Date: Mon, 18 May 2026 23:00:33 +0800 Subject: [PATCH 15/67] feat(ui): add Checkbox, Slider, and TextField components with input handling --- module.ui/TODO.md | 8 +- .../lib/v2/ui/DeclarativeScreen.java | 61 +++++++- .../dev/anvilcraft/lib/v2/ui/UIScope.java | 24 +++ .../v2/ui/component/CheckboxComponent.java | 89 +++++++++++ .../lib/v2/ui/component/SliderComponent.java | 95 ++++++++++++ .../v2/ui/component/TextFieldComponent.java | 144 ++++++++++++++++++ .../lib/v2/ui/input/KeyInputHandler.java | 13 ++ .../lib/v2/ui/input/package-info.java | 4 + 8 files changed, 433 insertions(+), 5 deletions(-) create mode 100644 module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/CheckboxComponent.java create mode 100644 module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/SliderComponent.java create mode 100644 module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextFieldComponent.java create mode 100644 module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/input/KeyInputHandler.java create mode 100644 module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/input/package-info.java diff --git a/module.ui/TODO.md b/module.ui/TODO.md index af72905a..b777da90 100644 --- a/module.ui/TODO.md +++ b/module.ui/TODO.md @@ -56,9 +56,11 @@ ## Phase 7: 输入组件 -- [ ] `TextField` — 文本输入 -- [ ] `Checkbox` — 布尔切换 -- [ ] `Slider` — 连续范围选择 +- [x] `TextField` — 单行文本输入(Backspace/Delete/光标移动/Home/End,GLFW 按键→字符映射) +- [x] `Checkbox` — 16x16 方框 + 选中对勾,点击切换 +- [x] `Slider` — 轨道 + 滑块,点击/拖拽设值 +- [x] `KeyInputHandler` 接口 + DeclarativeScreen 焦点系统(focusOwner) +- [x] 键盘路由:keyPressed → 焦点组件;鼠标:drag → Slider ## Phase 8: 高级特性 diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java index 2abe5295..879ef977 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java @@ -1,6 +1,10 @@ package dev.anvilcraft.lib.v2.ui; import dev.anvilcraft.lib.v2.ui.component.ButtonComponent; +import dev.anvilcraft.lib.v2.ui.component.CheckboxComponent; +import dev.anvilcraft.lib.v2.ui.component.SliderComponent; +import dev.anvilcraft.lib.v2.ui.component.TextFieldComponent; +import dev.anvilcraft.lib.v2.ui.input.KeyInputHandler; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiGraphicsExtractor; import net.minecraft.client.gui.screens.Screen; @@ -23,6 +27,8 @@ public abstract class DeclarativeScreen extends Screen { @Nullable private Composition composition; private final UIScope rootScope = new RootScope(); + @Nullable + private KeyInputHandler focusOwner; protected DeclarativeScreen(Component title) { super(title); @@ -56,6 +62,11 @@ public boolean mouseClicked(MouseButtonEvent event, boolean isDoubleClick) { var mc = Minecraft.getInstance(); int mx = (int) mc.mouseHandler.getScaledXPos(mc.getWindow()); int my = (int) mc.mouseHandler.getScaledYPos(mc.getWindow()); + // 点击空白处清除焦点 + focusOwner = null; + for (UIComponent child : rootScope.getChildren()) { + clearFocusRecursive(child); + } for (UIComponent child : rootScope.getChildren()) { if (hitTestClick(child, mx, my)) { mc.getSoundManager().play(SimpleSoundInstance.forUI(SoundEvents.UI_BUTTON_CLICK, 1.0f)); @@ -66,6 +77,17 @@ public boolean mouseClicked(MouseButtonEvent event, boolean isDoubleClick) { return super.mouseClicked(event, isDoubleClick); } + @Override + public boolean mouseDragged(MouseButtonEvent event, double deltaX, double deltaY) { + var mc = Minecraft.getInstance(); + int mx = (int) mc.mouseHandler.getScaledXPos(mc.getWindow()); + int my = (int) mc.mouseHandler.getScaledYPos(mc.getWindow()); + for (UIComponent child : rootScope.getChildren()) { + if (hitTestDrag(child, mx, my)) return true; + } + return super.mouseDragged(event, deltaX, deltaY); + } + @Override public boolean mouseScrolled(double mouseX, double mouseY, double scrollX, double scrollY) { // 后续 Phase: 路由到 ScrollComponent @@ -76,10 +98,13 @@ public boolean mouseScrolled(double mouseX, double mouseY, double scrollX, doubl @Override public boolean keyPressed(KeyEvent event) { - if (event.key() == 256) { // ESC — 关闭 Screen + if (event.key() == 256) { onClose(); return true; } + if (focusOwner != null && focusOwner.onKeyPressed(event)) { + return true; + } return super.keyPressed(event); } @@ -90,7 +115,7 @@ public void onClose() { // ── 命中测试 ── - /** 命中测试 + 点击触发。子组件优先(后绘制在上层)。 */ + /** 命中测试 + 点击触发。子组件优先。 */ private boolean hitTestClick(UIComponent component, float px, float py) { var children = component.children(); for (int i = children.size() - 1; i >= 0; i--) { @@ -100,9 +125,41 @@ private boolean hitTestClick(UIComponent component, float px, float py) { btn.click(); return true; } + if (component instanceof CheckboxComponent cb && cb.hitRect().contains(px, py)) { + cb.toggle(); + return true; + } + if (component instanceof SliderComponent sl && sl.hitRect().contains(px, py)) { + sl.setValueFromMouse(px); + return true; + } + if (component instanceof TextFieldComponent tf && tf.hitRect().contains(px, py)) { + tf.setFocused(true); + focusOwner = tf; + return true; + } return false; } + /** 拖拽命中测试(仅 Slider 响应)。 */ + private boolean hitTestDrag(UIComponent component, float px, float py) { + var children = component.children(); + for (int i = children.size() - 1; i >= 0; i--) { + if (hitTestDrag(children.get(i), px, py)) return true; + } + if (component instanceof SliderComponent sl && sl.hitRect().contains(px, py)) { + sl.setValueFromMouse(px); + return true; + } + return false; + } + + /** 清除组件树中所有 TextField 的焦点。 */ + private void clearFocusRecursive(UIComponent component) { + if (component instanceof TextFieldComponent tf) tf.setFocused(false); + for (UIComponent child : component.children()) clearFocusRecursive(child); + } + /** 遍历组件树,更新 ButtonComponent 的 hover 状态。 */ private void updateHover(float mouseX, float mouseY) { for (UIComponent child : rootScope.getChildren()) { diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIScope.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIScope.java index ceb716bb..eb44fc6a 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIScope.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIScope.java @@ -1,6 +1,9 @@ package dev.anvilcraft.lib.v2.ui; import dev.anvilcraft.lib.v2.ui.component.ButtonComponent; +import dev.anvilcraft.lib.v2.ui.component.CheckboxComponent; +import dev.anvilcraft.lib.v2.ui.component.SliderComponent; +import dev.anvilcraft.lib.v2.ui.component.TextFieldComponent; import dev.anvilcraft.lib.v2.ui.component.BoxComponent; import dev.anvilcraft.lib.v2.ui.component.BoxScope; import dev.anvilcraft.lib.v2.ui.component.ColumnComponent; @@ -69,6 +72,27 @@ public ImageComponent Image(Identifier sprite, float width, float height) { return c; } + public CheckboxComponent Checkbox(String label, boolean checked, Runnable onToggle) { + CheckboxComponent c = new CheckboxComponent(Modifier.NONE, label, checked, onToggle); + addChild(c); + Composition.current().emit(c); + return c; + } + + public SliderComponent Slider(float value, float min, float max, float width, Runnable onChange) { + SliderComponent c = new SliderComponent(Modifier.NONE, value, min, max, width, onChange); + addChild(c); + Composition.current().emit(c); + return c; + } + + public TextFieldComponent TextField(String initialText, @Nullable Runnable onChange) { + TextFieldComponent c = new TextFieldComponent(Modifier.NONE, initialText, onChange); + addChild(c); + Composition.current().emit(c); + return c; + } + public ButtonComponent Button(String label, @Nullable Runnable onClick) { ButtonComponent c = new ButtonComponent(Modifier.NONE, label, onClick); addChild(c); diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/CheckboxComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/CheckboxComponent.java new file mode 100644 index 00000000..7c92dc28 --- /dev/null +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/CheckboxComponent.java @@ -0,0 +1,89 @@ +package dev.anvilcraft.lib.v2.ui.component; + +import dev.anvilcraft.lib.v2.ui.Constraints; +import dev.anvilcraft.lib.v2.ui.LayoutRect; +import dev.anvilcraft.lib.v2.ui.MeasuredSize; +import dev.anvilcraft.lib.v2.ui.Modifier; +import dev.anvilcraft.lib.v2.ui.UIComponent; +import net.minecraft.client.gui.GuiGraphicsExtractor; + +import java.util.Collections; +import java.util.List; + +/** + * 复选框。点击切换 boolean 状态。 + * 原版风格:16x16 方框 + 选中时内部对勾。 + */ +public class CheckboxComponent implements UIComponent { + + private static final int BOX_COLOR = 0xFF404040; + private static final int CHECK_COLOR = 0xFFFFFFFF; + private static final float SIZE = 16; + + private Modifier modifier; + private String label; + private boolean checked; + private Runnable onToggle; + + private float x, y, width, height; + + public CheckboxComponent(Modifier modifier, String label, boolean checked, Runnable onToggle) { + this.modifier = modifier; + this.label = label; + this.checked = checked; + this.onToggle = onToggle; + } + + public CheckboxComponent modifier(Modifier m) { this.modifier = m; return this; } + + @Override public Modifier modifier() { return modifier; } + @Override public List children() { return Collections.emptyList(); } + + @Override + public MeasuredSize measure(Constraints constraints) { + // 方框 + 间距 + 标签文字宽度(简化:估算每字符 7px 宽) + float labelW = label.length() * 7f; + return MeasuredSize.of( + constraints.constrainWidth(SIZE + 4 + labelW), + constraints.constrainHeight(SIZE) + ); + } + + @Override + public void layout(float x, float y, float width, float height) { + this.x = x; + this.y = y; + this.width = width; + this.height = height; + } + + @Override + public void extractRenderState(GuiGraphicsExtractor extractor) { + int ix = (int) x, iy = (int) y; + + // 方框背景 + extractor.fill(ix, iy, ix + (int) SIZE, iy + (int) SIZE, BOX_COLOR); + + // 选中对勾(简化:十字线) + if (checked) { + int cx = ix + (int) SIZE / 2, cy = iy + (int) SIZE / 2; + int s = 4; + extractor.fill(cx - s, cy, cx, cy + s, CHECK_COLOR); // 左下-中心 + extractor.fill(cx, cy, cx + s + 2, cy - s, CHECK_COLOR); // 中心-右上 + } + + // 标签文字 + // TODO: 用 font.text() 渲染标签 + } + + /** 切换状态。 */ + public void toggle() { + checked = !checked; + if (onToggle != null) onToggle.run(); + } + + /** 命中测试包围盒。 */ + public LayoutRect hitRect() { + return LayoutRect.of(x, y, SIZE, SIZE); + } +} diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/SliderComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/SliderComponent.java new file mode 100644 index 00000000..fd97e601 --- /dev/null +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/SliderComponent.java @@ -0,0 +1,95 @@ +package dev.anvilcraft.lib.v2.ui.component; + +import dev.anvilcraft.lib.v2.ui.Constraints; +import dev.anvilcraft.lib.v2.ui.LayoutRect; +import dev.anvilcraft.lib.v2.ui.MeasuredSize; +import dev.anvilcraft.lib.v2.ui.Modifier; +import dev.anvilcraft.lib.v2.ui.UIComponent; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.util.Mth; + +import java.util.Collections; +import java.util.List; + +/** + * 滑块。水平拖拽选择范围内的值。 + * 原版风格:深色轨道 + 浅色滑块按钮。 + */ +public class SliderComponent implements UIComponent { + + private static final int TRACK_COLOR = 0xFF404040; + private static final int THUMB_COLOR = 0xFFAAAAAA; + private static final float TRACK_H = 4; + private static final float THUMB_W = 8; + private static final float THUMB_H = 16; + + private Modifier modifier; + private float value; + private final float min, max; + private final float trackWidth; + private Runnable onChange; + + private float x, y, width, height; + + public SliderComponent(Modifier modifier, float value, float min, float max, float trackWidth, Runnable onChange) { + this.modifier = modifier; + this.value = Mth.clamp(value, min, max); + this.min = min; + this.max = max; + this.trackWidth = trackWidth; + this.onChange = onChange; + } + + public SliderComponent modifier(Modifier m) { this.modifier = m; return this; } + public float value() { return value; } + + @Override public Modifier modifier() { return modifier; } + @Override public List children() { return Collections.emptyList(); } + + @Override + public MeasuredSize measure(Constraints constraints) { + return MeasuredSize.of( + constraints.constrainWidth(trackWidth), + constraints.constrainHeight(THUMB_H) + ); + } + + @Override + public void layout(float x, float y, float width, float height) { + this.x = x; + this.y = y; + this.width = width; + this.height = height; + } + + @Override + public void extractRenderState(GuiGraphicsExtractor extractor) { + int ix = (int) x, iy = (int) y; + int iw = (int) width; + + // 轨道 — 居中画在 THUMB_H 中间 + int trackY = (int) (y + (THUMB_H - TRACK_H) / 2f); + extractor.fill(ix, trackY, ix + iw, (int) (trackY + TRACK_H), TRACK_COLOR); + + // 滑块 — 按比例定位 + float ratio = (value - min) / (max - min); + float thumbX = x + ratio * (width - THUMB_W); + int tix = (int) thumbX, tiy = (int) y; + extractor.fill(tix, tiy, tix + (int) THUMB_W, tiy + (int) THUMB_H, THUMB_COLOR); + } + + /** 根据鼠标 X 坐标更新值。 */ + public void setValueFromMouse(float mouseX) { + float ratio = Mth.clamp((mouseX - x) / width, 0f, 1f); + float newValue = min + ratio * (max - min); + if (newValue != value) { + value = newValue; + if (onChange != null) onChange.run(); + } + } + + /** 命中测试包围盒(整个轨道+滑块区域)。 */ + public LayoutRect hitRect() { + return LayoutRect.of(x, y, width, height); + } +} diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextFieldComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextFieldComponent.java new file mode 100644 index 00000000..ba7e7e72 --- /dev/null +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextFieldComponent.java @@ -0,0 +1,144 @@ +package dev.anvilcraft.lib.v2.ui.component; + +import dev.anvilcraft.lib.v2.ui.Constraints; +import dev.anvilcraft.lib.v2.ui.LayoutRect; +import dev.anvilcraft.lib.v2.ui.MeasuredSize; +import dev.anvilcraft.lib.v2.ui.Modifier; +import dev.anvilcraft.lib.v2.ui.UIComponent; +import dev.anvilcraft.lib.v2.ui.input.KeyInputHandler; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.input.KeyEvent; +import org.jspecify.annotations.Nullable; + +import java.util.Collections; +import java.util.List; + +/** + * 单行文本输入框。支持键盘输入、光标、Backspace/Delete、Home/End。 + * 需要焦点:点击获得焦点,点击外部失去焦点。 + */ +public class TextFieldComponent implements UIComponent, KeyInputHandler { + + private static final int BG_COLOR = 0xFF202020; + private static final int TEXT_COLOR = 0xFFFFFFFF; + private static final int CURSOR_COLOR = 0xFFFFFFFF; + private static final float PADDING_H = 4; + private static final float PADDING_V = 4; + private static final float WIDTH = 160; + + private Modifier modifier; + private final StringBuilder buffer = new StringBuilder(); + private int cursorPos; + private boolean focused; + @Nullable + private Runnable onChange; + + private float x, y, width, height; + + public TextFieldComponent(Modifier modifier, String initialText, @Nullable Runnable onChange) { + this.modifier = modifier; + this.buffer.append(initialText != null ? initialText : ""); + this.cursorPos = this.buffer.length(); + this.onChange = onChange; + } + + public TextFieldComponent modifier(Modifier m) { this.modifier = m; return this; } + public String text() { return buffer.toString(); } + public void setFocused(boolean focused) { this.focused = focused; } + public boolean isFocused() { return focused; } + + @Override public Modifier modifier() { return modifier; } + @Override public List children() { return Collections.emptyList(); } + + @Override + public MeasuredSize measure(Constraints constraints) { + var font = Minecraft.getInstance().font; + return MeasuredSize.of( + constraints.constrainWidth(WIDTH), + constraints.constrainHeight(font.lineHeight + PADDING_V * 2) + ); + } + + @Override + public void layout(float x, float y, float width, float height) { + this.x = x; + this.y = y; + this.width = width; + this.height = height; + } + + @Override + public void extractRenderState(GuiGraphicsExtractor extractor) { + int ix = (int) x, iy = (int) y, iw = (int) width, ih = (int) height; + + // 背景 + extractor.fill(ix, iy, ix + iw, iy + ih, BG_COLOR); + + // 显示文字 + var font = Minecraft.getInstance().font; + int textX = ix + (int) PADDING_H; + int textY = (int) (y + (height + font.lineHeight) / 2f - font.lineHeight); + extractor.text(font, buffer.toString(), textX, textY, TEXT_COLOR); + + // 光标(聚焦时绘制) + if (focused) { + String textBefore = buffer.substring(0, cursorPos); + int cursorX = (int) (x + PADDING_H + font.width(textBefore)); + extractor.fill(cursorX, iy + 2, cursorX + 1, iy + ih - 2, CURSOR_COLOR); + } + } + + // ── KeyInputHandler ── + + @Override + public boolean onKeyPressed(KeyEvent event) { + int key = event.key(); + int mods = event.modifiers(); + + // 控制键 + if (key == 259) { // Backspace + if (cursorPos > 0) { buffer.deleteCharAt(cursorPos - 1); cursorPos--; fireChange(); } + return true; + } + if (key == 261) { // Delete + if (cursorPos < buffer.length()) { buffer.deleteCharAt(cursorPos); fireChange(); } + return true; + } + if (key == 263) { if (cursorPos > 0) cursorPos--; return true; } // Left + if (key == 262) { if (cursorPos < buffer.length()) cursorPos++; return true; } // Right + if (key == 268) { cursorPos = 0; return true; } // Home + if (key == 269) { cursorPos = buffer.length(); return true; } // End + + // 可打印字符 (GLFW key codes: 32=Space, 39=',', 44='.', 45='-', 47='/', 48-57=0-9, 59=';', 61='=', 65-90=A-Z, 91-93=[\]) + char c = glfwKeyToChar(key, (mods & 0x1) != 0); + if (c != 0) { + buffer.insert(cursorPos, c); + cursorPos++; + fireChange(); + return true; + } + return false; + } + + /** GLFW 按键码 → 字符(英文键盘布局)。 */ + private static char glfwKeyToChar(int key, boolean shift) { + if (key >= 65 && key <= 90) return (char) (shift ? key : key + 32); // A-Z / a-z + if (key >= 48 && key <= 57) return (char) (shift ? ")!@#$%^&*(".charAt(key - 48) : key); // 0-9 + return (char) switch (key) { + case 32 -> ' '; case 39 -> '\''; case 44 -> ','; case 45 -> '-'; + case 46 -> '.'; case 47 -> '/'; case 59 -> ';'; case 61 -> '='; + case 91 -> '['; case 92 -> '\\'; case 93 -> ']'; case 96 -> '`'; + default -> 0; + }; + } + + private void fireChange() { + if (onChange != null) onChange.run(); + } + + /** 命中测试包围盒。 */ + public LayoutRect hitRect() { + return LayoutRect.of(x, y, width, height); + } +} diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/input/KeyInputHandler.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/input/KeyInputHandler.java new file mode 100644 index 00000000..ef25fe61 --- /dev/null +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/input/KeyInputHandler.java @@ -0,0 +1,13 @@ +package dev.anvilcraft.lib.v2.ui.input; + +import net.minecraft.client.input.KeyEvent; + +/** + * 可接收键盘输入的组件接口。 + * 由 {@link dev.anvilcraft.lib.v2.ui.DeclarativeScreen} 的焦点系统驱动。 + */ +public interface KeyInputHandler { + + /** 按键按下时调用。返回 true 表示已处理。 */ + boolean onKeyPressed(KeyEvent event); +} diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/input/package-info.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/input/package-info.java new file mode 100644 index 00000000..a59de3b1 --- /dev/null +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/input/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package dev.anvilcraft.lib.v2.ui.input; + +import org.jspecify.annotations.NullMarked; From 7d4f5dafa41a31928851095d31f068899fbfc7eb Mon Sep 17 00:00:00 2001 From: Gugle Date: Mon, 18 May 2026 23:20:47 +0800 Subject: [PATCH 16/67] feat(ui): implement Animatable for tick-driven animations and add GridComponent for grid layout --- module.ui/TODO.md | 10 +-- .../dev/anvilcraft/lib/v2/ui/Animatable.java | 63 ++++++++++++++ .../dev/anvilcraft/lib/v2/ui/Composition.java | 12 +++ .../dev/anvilcraft/lib/v2/ui/ForEach.java | 26 ++++++ .../dev/anvilcraft/lib/v2/ui/UIScope.java | 12 +++ .../lib/v2/ui/component/GridComponent.java | 85 +++++++++++++++++++ .../lib/v2/ui/component/GridScope.java | 7 ++ 7 files changed, 210 insertions(+), 5 deletions(-) create mode 100644 module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Animatable.java create mode 100644 module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/ForEach.java create mode 100644 module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/GridComponent.java create mode 100644 module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/GridScope.java diff --git a/module.ui/TODO.md b/module.ui/TODO.md index b777da90..70a2b57c 100644 --- a/module.ui/TODO.md +++ b/module.ui/TODO.md @@ -64,11 +64,11 @@ ## Phase 8: 高级特性 -- [ ] `Grid` — 网格布局 -- [ ] `ForEach` — 循环渲染(带 key 稳定 slot 复用) -- [ ] `if` / `when` 条件渲染 -- [ ] `Animatable` — 时间驱动动画值(基于 Minecraft tick,不依赖协程) -- [ ] `LazyColumn` — 虚拟化长列表 +- [x] `Grid` — 网格布局(columns × auto-rows,hSpacing/vSpacing) +- [x] `ForEach` — 循环渲染工具(`ForEach.of(scope, items, (s, item) -> ...)`) +- [x] `Animatable` — tick 驱动动画值(ease-in-out,`Composition.watch()` 自动驱动) +- [ ] 条件渲染 — `if` 天然工作于 content lambda 重执行时;key 稳定需要后续优化 +- [ ] `LazyColumn` — 虚拟化长列表,延后 ## Phase 9: 测试 diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Animatable.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Animatable.java new file mode 100644 index 00000000..ebca8e45 --- /dev/null +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Animatable.java @@ -0,0 +1,63 @@ +package dev.anvilcraft.lib.v2.ui; + +import net.minecraft.util.Mth; + +/** + * 基于游戏 tick 的动画值。从当前值平滑过渡到目标值。 + * 每 tick 调用 {@link #tick()} 推进动画。 + */ +public class Animatable { + + private float value; + private float startValue; + private float targetValue; + private int durationTicks; + private int elapsed; + + public Animatable(float initialValue) { + this.value = initialValue; + this.targetValue = initialValue; + } + + /** 获取当前动画值。 */ + public float getValue() { + return value; + } + + /** 动画到目标值,durationTicks 帧内完成。 */ + public void animateTo(float target, int durationTicks) { + if (durationTicks <= 0) { + this.value = target; + this.targetValue = target; + this.elapsed = 0; + return; + } + this.startValue = this.value; + this.targetValue = target; + this.durationTicks = durationTicks; + this.elapsed = 0; + } + + /** 直接设置值(无动画)。 */ + public void setValue(float value) { + this.value = value; + this.targetValue = value; + this.elapsed = 0; + } + + /** 每 tick 调用一次以推进动画。返回 true 表示动画进行中。 */ + public boolean tick() { + if (elapsed >= durationTicks) return false; + elapsed++; + float t = durationTicks > 0 ? (float) elapsed / durationTicks : 1f; + // ease-in-out + float eased = t < 0.5f ? 2f * t * t : -1f + (4f - 2f * t) * t; + this.value = Mth.lerp(eased, startValue, targetValue); + return elapsed < durationTicks; + } + + /** 动画是否进行中。 */ + public boolean isRunning() { + return elapsed < durationTicks; + } +} diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Composition.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Composition.java index 9560cef2..017e64e7 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Composition.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Composition.java @@ -55,6 +55,7 @@ public static Composition current() { private boolean dirty = true; private Consumer content; private UIScope rootScope; + private final List animatables = new ArrayList<>(); public Composition(UIScope rootScope) { this.rootScope = rootScope; @@ -69,6 +70,13 @@ public void invalidate() { dirty = true; } + /** 注册动画值,每帧自动 tick。动画进行中时自动触发 recompose。 */ + public void watch(Animatable anim) { + if (!animatables.contains(anim)) { + animatables.add(anim); + } + } + // ── remember ── /** @@ -117,6 +125,10 @@ public void emit(UIComponent component) { public void renderFrame(GuiGraphicsExtractor extractor, float screenWidth, float screenHeight) { CURRENT.set(this); try { + // Tick animations — if any running, mark dirty + for (Animatable anim : animatables) { + if (anim.tick()) dirty = true; + } if (dirty || hasDirtySlots()) { recompose(); dirty = false; diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/ForEach.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/ForEach.java new file mode 100644 index 00000000..532012d2 --- /dev/null +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/ForEach.java @@ -0,0 +1,26 @@ +package dev.anvilcraft.lib.v2.ui; + +import java.util.List; +import java.util.function.BiConsumer; + +/** + * 循环渲染工具。将列表中的每个元素映射为一组 UI 组件。 + * 列表顺序稳定时,slot 位置保持稳定。 + * + *
{@code
+ * ForEach.of(scope, items, (s, item) -> {
+ *     s.Text(item.name());
+ * });
+ * }
+ */ +public final class ForEach { + + private ForEach() {} + + /** 对列表中的每个元素执行内容函数。 */ + public static void of(UIScope scope, List items, BiConsumer content) { + for (T item : items) { + content.accept(scope, item); + } + } +} diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIScope.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIScope.java index eb44fc6a..fa3db6be 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIScope.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIScope.java @@ -8,6 +8,8 @@ import dev.anvilcraft.lib.v2.ui.component.BoxScope; import dev.anvilcraft.lib.v2.ui.component.ColumnComponent; import dev.anvilcraft.lib.v2.ui.component.ColumnScope; +import dev.anvilcraft.lib.v2.ui.component.GridComponent; +import dev.anvilcraft.lib.v2.ui.component.GridScope; import dev.anvilcraft.lib.v2.ui.component.ImageComponent; import dev.anvilcraft.lib.v2.ui.component.RowComponent; import dev.anvilcraft.lib.v2.ui.component.RowScope; @@ -129,4 +131,14 @@ public BoxComponent Box(Consumer content) { Composition.current().emit(c); return c; } + + public GridComponent Grid(int columns, Consumer content) { + GridComponent c = new GridComponent(Modifier.NONE, columns); + GridScope inner = new GridScope(); + content.accept(inner); + c.setChildren(inner.getChildren()); + addChild(c); + Composition.current().emit(c); + return c; + } } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/GridComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/GridComponent.java new file mode 100644 index 00000000..8d5dd314 --- /dev/null +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/GridComponent.java @@ -0,0 +1,85 @@ +package dev.anvilcraft.lib.v2.ui.component; + +import dev.anvilcraft.lib.v2.ui.Constraints; +import dev.anvilcraft.lib.v2.ui.MeasuredSize; +import dev.anvilcraft.lib.v2.ui.Modifier; +import dev.anvilcraft.lib.v2.ui.UIComponent; +import net.minecraft.client.gui.GuiGraphicsExtractor; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * 网格布局。子组件按列数排列,每格大小由最大子组件决定。 + */ +public class GridComponent implements UIComponent { + + private Modifier modifier; + private final int columns; + private List children = Collections.emptyList(); + private List childSizes = Collections.emptyList(); + private float hSpacing, vSpacing; + + private float x, y, width, height; + private float cellW, cellH; + + public GridComponent(Modifier modifier, int columns) { + this.modifier = modifier; + this.columns = Math.max(1, columns); + } + + public void setChildren(List children) { + this.children = List.copyOf(children); + } + + public GridComponent modifier(Modifier m) { this.modifier = m; return this; } + public GridComponent spacing(float h, float v) { this.hSpacing = h; this.vSpacing = v; return this; } + + @Override public Modifier modifier() { return modifier; } + @Override public List children() { return children; } + + @Override + public MeasuredSize measure(Constraints constraints) { + if (children.isEmpty()) return MeasuredSize.ZERO; + + float maxW = 0, maxH = 0; + List sizes = new ArrayList<>(children.size()); + Constraints childC = new Constraints(0, Float.MAX_VALUE, 0, Float.MAX_VALUE); + + for (UIComponent child : children) { + MeasuredSize s = child.measure(childC); + sizes.add(s); + maxW = Math.max(maxW, s.width()); + maxH = Math.max(maxH, s.height()); + } + + this.childSizes = sizes; + this.cellW = maxW; + this.cellH = maxH; + + int rows = (children.size() + columns - 1) / columns; + float totalW = maxW * columns + hSpacing * (columns - 1); + float totalH = maxH * rows + vSpacing * (rows - 1); + + return MeasuredSize.of(constraints.constrainWidth(totalW), constraints.constrainHeight(totalH)); + } + + @Override + public void layout(float x, float y, float width, float height) { + this.x = x; this.y = y; this.width = width; this.height = height; + + for (int i = 0; i < children.size(); i++) { + int col = i % columns; + int row = i / columns; + float cx = x + col * (cellW + hSpacing); + float cy = y + row * (cellH + vSpacing); + children.get(i).layout(cx, cy, cellW, cellH); + } + } + + @Override + public void extractRenderState(GuiGraphicsExtractor extractor) { + for (UIComponent child : children) child.extractRenderState(extractor); + } +} diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/GridScope.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/GridScope.java new file mode 100644 index 00000000..e85bb6bc --- /dev/null +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/GridScope.java @@ -0,0 +1,7 @@ +package dev.anvilcraft.lib.v2.ui.component; + +import dev.anvilcraft.lib.v2.ui.UIScope; + +/** Scope for children inside a {@link GridComponent}. */ +public class GridScope extends UIScope { +} From 29fb80785a6a402eb631af766b51a64b4e3a7832 Mon Sep 17 00:00:00 2001 From: Gugle Date: Mon, 18 May 2026 23:27:24 +0800 Subject: [PATCH 17/67] feat(ui): enhance DeclarativeTestScreen with comprehensive component tests and update Slider/TextField components to use Consumer for change handling --- .../client/screen/DeclarativeTestScreen.java | 102 +++++++++++++++--- .../dev/anvilcraft/lib/v2/ui/UIScope.java | 6 +- .../lib/v2/ui/component/SliderComponent.java | 7 +- .../v2/ui/component/TextFieldComponent.java | 8 +- 4 files changed, 97 insertions(+), 26 deletions(-) diff --git a/module.test/src/main/java/dev/anvilcraft/lib/v2/test/client/screen/DeclarativeTestScreen.java b/module.test/src/main/java/dev/anvilcraft/lib/v2/test/client/screen/DeclarativeTestScreen.java index 704046e9..ebcc0533 100644 --- a/module.test/src/main/java/dev/anvilcraft/lib/v2/test/client/screen/DeclarativeTestScreen.java +++ b/module.test/src/main/java/dev/anvilcraft/lib/v2/test/client/screen/DeclarativeTestScreen.java @@ -4,9 +4,10 @@ import dev.anvilcraft.lib.v2.ui.component.TextComponent; import net.minecraft.network.chat.Component; +import java.util.List; + /** - * 端到端测试 Screen,展示声明式 UI 的全部 Phase 3+4 功能: - * Column / Row / Box 布局,Text / Button / Spacer 组件,状态管理。 + * 声明式 UI 全面测试 Screen。覆盖 Phase 1-8 所有组件和功能。 */ public class DeclarativeTestScreen extends DeclarativeScreen { @@ -16,36 +17,103 @@ public DeclarativeTestScreen() { @Override protected void content(UIScope scope) { - MutableState counter = Composition.current() - .remember(() -> new MutableState<>(0)); + Composition comp = Composition.current(); + MutableState counter = comp.remember(() -> new MutableState<>(0)); + MutableState checked = comp.remember(() -> new MutableState<>(false)); + MutableState text = comp.remember(() -> new MutableState<>("")); scope.Column(col -> { - col.Text("Declarative UI Demo") - .color(0xFFFFFF00); + // ── 标题 ── + col.Text("=== AnvilLib Declarative UI ===").color(0xFFFFFF00); + + // ── 1. Text 样式 ── + col.Text("1) Text styles:"); + col.Row(row -> { + row.Text("Shadow").shadow(true).color(0xFFFFAA00); + row.Text(" | Left").align(TextComponent.Align.LEFT); + row.Text("Center").align(TextComponent.Align.CENTER).color(0xFF00AAFF); + row.Text("Right|").align(TextComponent.Align.RIGHT); + }).spacing(8); col.Spacer(0, 4); - // Row: 横向按钮栏 + // ── 2. Button + 状态 ── + col.Text("2) Button + state (Counter):"); col.Row(row -> { row.Button("-", () -> counter.setValue(counter.getValue() - 1)); - row.Text(" " + counter.getValue() + " ") - .align(TextComponent.Align.CENTER); + row.Text(" " + counter.getValue() + " ").align(TextComponent.Align.CENTER); row.Button("+", () -> counter.setValue(counter.getValue() + 1)); + row.Button("Reset", () -> counter.setValue(0)); }).spacing(4); - col.Spacer(0, 8); + col.Spacer(0, 4); - // Box: 叠加 + // ── 3. Box 层叠 ── + col.Text("3) Box overlay:"); col.Box(box -> { - box.Text(" "); - box.Text("Count: " + counter.getValue()) - .color(0xFF00FF00); + box.Text(" "); + box.Text("<< Overlay Text >>").color(0xFF00FF00); }).contentAlignment(Alignment.Horizontal.Center, Alignment.Vertical.Center); - col.Spacer(0, 8); + col.Spacer(0, 4); + + // ── 4. Grid 网格 ── + col.Text("4) Grid (3 columns):"); + col.Grid(3, grid -> { + for (int i = 0; i < 6; i++) { + grid.Button("G" + i, () -> {}); + } + }).spacing(2, 2); + + col.Spacer(0, 4); + + // ── 5. Checkbox ── + col.Text("5) Checkbox:"); + col.Row(row -> { + row.Checkbox("Enable feature", checked.getValue(), + () -> checked.setValue(!checked.getValue())); + row.Text(" Enabled: " + checked.getValue()); + }).spacing(4); + + col.Spacer(0, 4); + + // ── 6. Slider ── + MutableState sliderVal = comp.remember(() -> new MutableState<>(50f)); + col.Text("6) Slider:"); + col.Row(row -> { + row.Slider(sliderVal.getValue(), 0, 100, 100, + v -> sliderVal.setValue(v)); + row.Text(" " + sliderVal.getValue().intValue() + "%"); + }).spacing(4); + + col.Spacer(0, 4); + + // ── 7. TextField ── + col.Text("7) TextField:"); + col.Row(row -> { + row.TextField("Type here...", v -> text.setValue(v)); + row.Text(" Value: '" + text.getValue() + "'"); + }).spacing(4); + + col.Spacer(0, 4); + + // ── 8. ForEach ── + col.Text("8) ForEach (list of 4 items):"); + ForEach.of(col, List.of("Apple", "Banana", "Cherry", "Date"), + (s, item) -> s.Text(" - " + item)); + + col.Spacer(0, 4); + + // ── 9. Modifier 样式 ── + col.Text("9) Modifiers:"); + col.Row(row -> { + row.Button("Styled", () -> {}) + .modifier(Modifier.NONE.background(0xFF884444).border(1, 0xFFFF8888)); + row.Text(" "); + row.Text("Padded").modifier(Modifier.NONE.padding(8).background(0xFF444488)); + }).spacing(4); - col.Button("Reset", () -> counter.setValue(0)); - }).spacing(8); + }).spacing(6).modifier(Modifier.NONE.padding(10)); } } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIScope.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIScope.java index fa3db6be..672085ac 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIScope.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIScope.java @@ -81,14 +81,16 @@ public CheckboxComponent Checkbox(String label, boolean checked, Runnable onTogg return c; } - public SliderComponent Slider(float value, float min, float max, float width, Runnable onChange) { + public SliderComponent Slider(float value, float min, float max, float width, + java.util.function.Consumer onChange) { SliderComponent c = new SliderComponent(Modifier.NONE, value, min, max, width, onChange); addChild(c); Composition.current().emit(c); return c; } - public TextFieldComponent TextField(String initialText, @Nullable Runnable onChange) { + public TextFieldComponent TextField(String initialText, + java.util.function.Consumer onChange) { TextFieldComponent c = new TextFieldComponent(Modifier.NONE, initialText, onChange); addChild(c); Composition.current().emit(c); diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/SliderComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/SliderComponent.java index fd97e601..e204fb35 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/SliderComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/SliderComponent.java @@ -27,11 +27,12 @@ public class SliderComponent implements UIComponent { private float value; private final float min, max; private final float trackWidth; - private Runnable onChange; + private java.util.function.Consumer onChange; private float x, y, width, height; - public SliderComponent(Modifier modifier, float value, float min, float max, float trackWidth, Runnable onChange) { + public SliderComponent(Modifier modifier, float value, float min, float max, float trackWidth, + java.util.function.Consumer onChange) { this.modifier = modifier; this.value = Mth.clamp(value, min, max); this.min = min; @@ -84,7 +85,7 @@ public void setValueFromMouse(float mouseX) { float newValue = min + ratio * (max - min); if (newValue != value) { value = newValue; - if (onChange != null) onChange.run(); + if (onChange != null) onChange.accept(value); } } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextFieldComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextFieldComponent.java index ba7e7e72..123e7bc1 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextFieldComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextFieldComponent.java @@ -31,12 +31,12 @@ public class TextFieldComponent implements UIComponent, KeyInputHandler { private final StringBuilder buffer = new StringBuilder(); private int cursorPos; private boolean focused; - @Nullable - private Runnable onChange; + private java.util.function.Consumer onChange; private float x, y, width, height; - public TextFieldComponent(Modifier modifier, String initialText, @Nullable Runnable onChange) { + public TextFieldComponent(Modifier modifier, String initialText, + java.util.function.Consumer onChange) { this.modifier = modifier; this.buffer.append(initialText != null ? initialText : ""); this.cursorPos = this.buffer.length(); @@ -134,7 +134,7 @@ private static char glfwKeyToChar(int key, boolean shift) { } private void fireChange() { - if (onChange != null) onChange.run(); + if (onChange != null) onChange.accept(buffer.toString()); } /** 命中测试包围盒。 */ From 5db100b3a12eada91bbdcba756c95deafec98acd Mon Sep 17 00:00:00 2001 From: Gugle Date: Mon, 18 May 2026 23:31:00 +0800 Subject: [PATCH 18/67] feat(ui): refactor SliderComponent and TextFieldComponent to use simplified Consumer type for change handling --- .../src/main/java/dev/anvilcraft/lib/v2/ui/UIComponent.java | 4 +++- .../src/main/java/dev/anvilcraft/lib/v2/ui/UIScope.java | 4 ++-- .../dev/anvilcraft/lib/v2/ui/component/SliderComponent.java | 5 +++-- .../anvilcraft/lib/v2/ui/component/TextFieldComponent.java | 5 +++-- 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIComponent.java index 7e1eaae5..8d5128aa 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIComponent.java @@ -2,6 +2,8 @@ import net.minecraft.client.gui.GuiGraphicsExtractor; +import java.util.List; + /** * Core interface for all UI components. *

@@ -18,7 +20,7 @@ public interface UIComponent { Modifier modifier(); /** Children of this component, or empty list for leaf components. */ - java.util.List children(); + List children(); /** * Measure this component given parent constraints. diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIScope.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIScope.java index 672085ac..365999e0 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIScope.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIScope.java @@ -82,7 +82,7 @@ public CheckboxComponent Checkbox(String label, boolean checked, Runnable onTogg } public SliderComponent Slider(float value, float min, float max, float width, - java.util.function.Consumer onChange) { + Consumer onChange) { SliderComponent c = new SliderComponent(Modifier.NONE, value, min, max, width, onChange); addChild(c); Composition.current().emit(c); @@ -90,7 +90,7 @@ public SliderComponent Slider(float value, float min, float max, float width, } public TextFieldComponent TextField(String initialText, - java.util.function.Consumer onChange) { + Consumer onChange) { TextFieldComponent c = new TextFieldComponent(Modifier.NONE, initialText, onChange); addChild(c); Composition.current().emit(c); diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/SliderComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/SliderComponent.java index e204fb35..fa06fbea 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/SliderComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/SliderComponent.java @@ -10,6 +10,7 @@ import java.util.Collections; import java.util.List; +import java.util.function.Consumer; /** * 滑块。水平拖拽选择范围内的值。 @@ -27,12 +28,12 @@ public class SliderComponent implements UIComponent { private float value; private final float min, max; private final float trackWidth; - private java.util.function.Consumer onChange; + private Consumer onChange; private float x, y, width, height; public SliderComponent(Modifier modifier, float value, float min, float max, float trackWidth, - java.util.function.Consumer onChange) { + Consumer onChange) { this.modifier = modifier; this.value = Mth.clamp(value, min, max); this.min = min; diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextFieldComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextFieldComponent.java index 123e7bc1..bc037aae 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextFieldComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextFieldComponent.java @@ -13,6 +13,7 @@ import java.util.Collections; import java.util.List; +import java.util.function.Consumer; /** * 单行文本输入框。支持键盘输入、光标、Backspace/Delete、Home/End。 @@ -31,12 +32,12 @@ public class TextFieldComponent implements UIComponent, KeyInputHandler { private final StringBuilder buffer = new StringBuilder(); private int cursorPos; private boolean focused; - private java.util.function.Consumer onChange; + private Consumer onChange; private float x, y, width, height; public TextFieldComponent(Modifier modifier, String initialText, - java.util.function.Consumer onChange) { + Consumer onChange) { this.modifier = modifier; this.buffer.append(initialText != null ? initialText : ""); this.cursorPos = this.buffer.length(); From fcf3255851ee67993ed19fa5b50a55afeb7cb242 Mon Sep 17 00:00:00 2001 From: Gugle Date: Mon, 18 May 2026 23:36:37 +0800 Subject: [PATCH 19/67] feat(ui): implement scroll handling in Composition and DeclarativeScreen for improved rendering --- .../dev/anvilcraft/lib/v2/ui/Composition.java | 21 ++++++++++++------- .../lib/v2/ui/DeclarativeScreen.java | 7 ++++--- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Composition.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Composition.java index 017e64e7..726aa551 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Composition.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Composition.java @@ -122,10 +122,9 @@ public void emit(UIComponent component) { * Called every frame from {@link DeclarativeScreen#extractRenderState}. * Runs recomposition if dirty, then measure → layout → render. */ - public void renderFrame(GuiGraphicsExtractor extractor, float screenWidth, float screenHeight) { + public void renderFrame(GuiGraphicsExtractor extractor, float screenWidth, float screenHeight, float scrollY) { CURRENT.set(this); try { - // Tick animations — if any running, mark dirty for (Animatable anim : animatables) { if (anim.tick()) dirty = true; } @@ -133,9 +132,17 @@ public void renderFrame(GuiGraphicsExtractor extractor, float screenWidth, float recompose(); dirty = false; } - Constraints rootConstraints = new Constraints(0, screenWidth, 0, screenHeight); + Constraints rootConstraints = new Constraints(0, screenWidth, 0, Float.MAX_VALUE); + // 先 measure 获取内容高度,再 layout 应用滚动偏移 + float contentHeight = 0; for (UIComponent child : rootScope.getChildren()) { - renderTree(child, extractor, rootConstraints); + MeasuredSize size = child.measure(rootConstraints); + contentHeight = Math.max(contentHeight, size.height()); + } + float maxScroll = Math.max(0, contentHeight - screenHeight); + float clampedScroll = Math.clamp(scrollY, -maxScroll, 0); + for (UIComponent child : rootScope.getChildren()) { + renderTree(child, extractor, rootConstraints, clampedScroll); } } finally { CURRENT.set(null); @@ -167,7 +174,7 @@ private boolean hasDirtySlots() { // ── measure → layout → render walk ── - private void renderTree(UIComponent component, GuiGraphicsExtractor extractor, Constraints constraints) { + private void renderTree(UIComponent component, GuiGraphicsExtractor extractor, Constraints constraints, float scrollY) { // 1. Apply modifier to constraints Constraints modConstraints = component.modifier().foldIn( constraints, @@ -181,8 +188,8 @@ private void renderTree(UIComponent component, GuiGraphicsExtractor extractor, C (el, s) -> el.modifyMeasuredSize(component, modConstraints, s) ); - // 3. Layout - LayoutRect rect = LayoutRect.of(0, 0, size.width(), size.height()); + // 3. Layout — 应用滚动偏移 + LayoutRect rect = LayoutRect.of(0, scrollY, size.width(), size.height()); rect = component.modifier().foldOut( rect, (el, r) -> el.modifyLayout(r) diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java index 879ef977..0724c173 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java @@ -29,6 +29,7 @@ public abstract class DeclarativeScreen extends Screen { private final UIScope rootScope = new RootScope(); @Nullable private KeyInputHandler focusOwner; + private float scrollY; protected DeclarativeScreen(Component title) { super(title); @@ -49,7 +50,7 @@ protected void init() { public void extractRenderState(GuiGraphicsExtractor extractor, int mouseX, int mouseY, float partialTick) { super.extractRenderState(extractor, mouseX, mouseY, partialTick); if (composition != null) { - composition.renderFrame(extractor, this.width, this.height); + composition.renderFrame(extractor, this.width, this.height, scrollY); updateHover(mouseX, mouseY); } } @@ -90,8 +91,8 @@ public boolean mouseDragged(MouseButtonEvent event, double deltaX, double deltaY @Override public boolean mouseScrolled(double mouseX, double mouseY, double scrollX, double scrollY) { - // 后续 Phase: 路由到 ScrollComponent - return super.mouseScrolled(mouseX, mouseY, scrollX, scrollY); + this.scrollY += (float) (scrollY * 20); // 每格滚轮 20px + return true; } // ── 键盘输入 ── From abaa808ea0a673a8642c6737f27aad17e9a7a03b Mon Sep 17 00:00:00 2001 From: Gugle Date: Mon, 18 May 2026 23:41:00 +0800 Subject: [PATCH 20/67] feat(ui): add ScrollableComponent for vertical scrolling and enhance DeclarativeScreen with scroll handling --- module.ui/TODO.md | 1 + .../dev/anvilcraft/lib/v2/ui/Composition.java | 20 +-- .../lib/v2/ui/DeclarativeScreen.java | 24 +++- .../dev/anvilcraft/lib/v2/ui/UIScope.java | 12 ++ .../v2/ui/component/ScrollableComponent.java | 127 ++++++++++++++++++ .../lib/v2/ui/component/ScrollableScope.java | 7 + 6 files changed, 173 insertions(+), 18 deletions(-) create mode 100644 module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ScrollableComponent.java create mode 100644 module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ScrollableScope.java diff --git a/module.ui/TODO.md b/module.ui/TODO.md index 70a2b57c..cb1c2cbd 100644 --- a/module.ui/TODO.md +++ b/module.ui/TODO.md @@ -68,6 +68,7 @@ - [x] `ForEach` — 循环渲染工具(`ForEach.of(scope, items, (s, item) -> ...)`) - [x] `Animatable` — tick 驱动动画值(ease-in-out,`Composition.watch()` 自动驱动) - [ ] 条件渲染 — `if` 天然工作于 content lambda 重执行时;key 稳定需要后续优化 +- [x] `Scrollable` — 可滚动容器(maxHeight + scissor 裁剪 + 滚动条) - [ ] `LazyColumn` — 虚拟化长列表,延后 ## Phase 9: 测试 diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Composition.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Composition.java index 726aa551..4060431e 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Composition.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Composition.java @@ -122,7 +122,7 @@ public void emit(UIComponent component) { * Called every frame from {@link DeclarativeScreen#extractRenderState}. * Runs recomposition if dirty, then measure → layout → render. */ - public void renderFrame(GuiGraphicsExtractor extractor, float screenWidth, float screenHeight, float scrollY) { + public void renderFrame(GuiGraphicsExtractor extractor, float screenWidth, float screenHeight) { CURRENT.set(this); try { for (Animatable anim : animatables) { @@ -132,17 +132,9 @@ public void renderFrame(GuiGraphicsExtractor extractor, float screenWidth, float recompose(); dirty = false; } - Constraints rootConstraints = new Constraints(0, screenWidth, 0, Float.MAX_VALUE); - // 先 measure 获取内容高度,再 layout 应用滚动偏移 - float contentHeight = 0; + Constraints rootConstraints = new Constraints(0, screenWidth, 0, screenHeight); for (UIComponent child : rootScope.getChildren()) { - MeasuredSize size = child.measure(rootConstraints); - contentHeight = Math.max(contentHeight, size.height()); - } - float maxScroll = Math.max(0, contentHeight - screenHeight); - float clampedScroll = Math.clamp(scrollY, -maxScroll, 0); - for (UIComponent child : rootScope.getChildren()) { - renderTree(child, extractor, rootConstraints, clampedScroll); + renderTree(child, extractor, rootConstraints); } } finally { CURRENT.set(null); @@ -174,7 +166,7 @@ private boolean hasDirtySlots() { // ── measure → layout → render walk ── - private void renderTree(UIComponent component, GuiGraphicsExtractor extractor, Constraints constraints, float scrollY) { + private void renderTree(UIComponent component, GuiGraphicsExtractor extractor, Constraints constraints) { // 1. Apply modifier to constraints Constraints modConstraints = component.modifier().foldIn( constraints, @@ -188,8 +180,8 @@ private void renderTree(UIComponent component, GuiGraphicsExtractor extractor, C (el, s) -> el.modifyMeasuredSize(component, modConstraints, s) ); - // 3. Layout — 应用滚动偏移 - LayoutRect rect = LayoutRect.of(0, scrollY, size.width(), size.height()); + // 3. Layout + LayoutRect rect = LayoutRect.of(0, 0, size.width(), size.height()); rect = component.modifier().foldOut( rect, (el, r) -> el.modifyLayout(r) diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java index 0724c173..79a7d5c6 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java @@ -2,6 +2,7 @@ import dev.anvilcraft.lib.v2.ui.component.ButtonComponent; import dev.anvilcraft.lib.v2.ui.component.CheckboxComponent; +import dev.anvilcraft.lib.v2.ui.component.ScrollableComponent; import dev.anvilcraft.lib.v2.ui.component.SliderComponent; import dev.anvilcraft.lib.v2.ui.component.TextFieldComponent; import dev.anvilcraft.lib.v2.ui.input.KeyInputHandler; @@ -29,7 +30,6 @@ public abstract class DeclarativeScreen extends Screen { private final UIScope rootScope = new RootScope(); @Nullable private KeyInputHandler focusOwner; - private float scrollY; protected DeclarativeScreen(Component title) { super(title); @@ -50,7 +50,7 @@ protected void init() { public void extractRenderState(GuiGraphicsExtractor extractor, int mouseX, int mouseY, float partialTick) { super.extractRenderState(extractor, mouseX, mouseY, partialTick); if (composition != null) { - composition.renderFrame(extractor, this.width, this.height, scrollY); + composition.renderFrame(extractor, this.width, this.height); updateHover(mouseX, mouseY); } } @@ -91,8 +91,12 @@ public boolean mouseDragged(MouseButtonEvent event, double deltaX, double deltaY @Override public boolean mouseScrolled(double mouseX, double mouseY, double scrollX, double scrollY) { - this.scrollY += (float) (scrollY * 20); // 每格滚轮 20px - return true; + for (UIComponent child : rootScope.getChildren()) { + if (hitTestScroll(child, (float) mouseX, (float) mouseY, (float) scrollY)) { + return true; + } + } + return super.mouseScrolled(mouseX, mouseY, scrollX, scrollY); } // ── 键盘输入 ── @@ -155,6 +159,18 @@ private boolean hitTestDrag(UIComponent component, float px, float py) { return false; } + /** 滚轮命中测试(仅 ScrollableComponent 响应)。 */ + private boolean hitTestScroll(UIComponent component, float px, float py, float amount) { + var children = component.children(); + for (int i = children.size() - 1; i >= 0; i--) { + if (hitTestScroll(children.get(i), px, py, amount)) return true; + } + if (component instanceof ScrollableComponent sc && sc.hitRect().contains(px, py)) { + return sc.onScroll(amount); + } + return false; + } + /** 清除组件树中所有 TextField 的焦点。 */ private void clearFocusRecursive(UIComponent component) { if (component instanceof TextFieldComponent tf) tf.setFocused(false); diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIScope.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIScope.java index 365999e0..23fb9b8c 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIScope.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIScope.java @@ -13,6 +13,8 @@ import dev.anvilcraft.lib.v2.ui.component.ImageComponent; import dev.anvilcraft.lib.v2.ui.component.RowComponent; import dev.anvilcraft.lib.v2.ui.component.RowScope; +import dev.anvilcraft.lib.v2.ui.component.ScrollableComponent; +import dev.anvilcraft.lib.v2.ui.component.ScrollableScope; import dev.anvilcraft.lib.v2.ui.component.SpacerComponent; import dev.anvilcraft.lib.v2.ui.component.TextComponent; @@ -143,4 +145,14 @@ public GridComponent Grid(int columns, Consumer content) { Composition.current().emit(c); return c; } + + public ScrollableComponent Scrollable(float maxHeight, Consumer content) { + ScrollableComponent c = new ScrollableComponent(Modifier.NONE, maxHeight); + ScrollableScope inner = new ScrollableScope(); + content.accept(inner); + c.setChildren(inner.getChildren()); + addChild(c); + Composition.current().emit(c); + return c; + } } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ScrollableComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ScrollableComponent.java new file mode 100644 index 00000000..be6d51aa --- /dev/null +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ScrollableComponent.java @@ -0,0 +1,127 @@ +package dev.anvilcraft.lib.v2.ui.component; + +import dev.anvilcraft.lib.v2.ui.Constraints; +import dev.anvilcraft.lib.v2.ui.LayoutRect; +import dev.anvilcraft.lib.v2.ui.MeasuredSize; +import dev.anvilcraft.lib.v2.ui.Modifier; +import dev.anvilcraft.lib.v2.ui.UIComponent; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.util.Mth; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * 可滚动容器。限制最大高度,超出部分可垂直滚动。 + * 渲染时自动裁剪,并绘制滚动条。 + */ +public class ScrollableComponent implements UIComponent { + + private static final int SCROLLBAR_COLOR = 0xFF888888; + private static final int SCROLLBAR_BG = 0xFF333333; + private static final int SCROLLBAR_W = 4; + + private Modifier modifier; + private final float maxHeight; + private List children = Collections.emptyList(); + private List childSizes = Collections.emptyList(); + + private float scrollY; + private float contentHeight; + + private float x, y, width, height; + + public ScrollableComponent(Modifier modifier, float maxHeight) { + this.modifier = modifier; + this.maxHeight = maxHeight; + } + + public void setChildren(List children) { + this.children = List.copyOf(children); + } + + public ScrollableComponent modifier(Modifier m) { this.modifier = m; return this; } + + @Override public Modifier modifier() { return modifier; } + @Override public List children() { return children; } + + @Override + public MeasuredSize measure(Constraints constraints) { + if (children.isEmpty()) return MeasuredSize.ZERO; + + float maxW = 0; + float totalH = 0; + List sizes = new ArrayList<>(children.size()); + Constraints childC = new Constraints(0, constraints.maxWidth(), 0, Float.MAX_VALUE); + + for (UIComponent child : children) { + MeasuredSize s = child.measure(childC); + sizes.add(s); + totalH += s.height(); + maxW = Math.max(maxW, s.width()); + } + this.childSizes = sizes; + this.contentHeight = totalH; + + return MeasuredSize.of( + constraints.constrainWidth(maxW), + constraints.constrainHeight(Math.min(totalH, maxHeight)) + ); + } + + @Override + public void layout(float x, float y, float width, float height) { + this.x = x; + this.y = y; + this.width = width; + this.height = height; + + float currentY = y + scrollY; + for (int i = 0; i < children.size(); i++) { + UIComponent child = children.get(i); + MeasuredSize size = childSizes.get(i); + child.layout(x, currentY, width, size.height()); + currentY += size.height(); + } + } + + @Override + public void extractRenderState(GuiGraphicsExtractor extractor) { + int ix = (int) x, iy = (int) y, iw = (int) width, ih = (int) height; + + // 裁剪到容器范围 + extractor.enableScissor(ix, iy, ix + iw, iy + ih); + + for (UIComponent child : children) { + child.extractRenderState(extractor); + } + + extractor.disableScissor(); + + // 滚动条 + if (contentHeight > height) { + float barH = Math.max(16, height * height / contentHeight); + float maxScroll = contentHeight - height; + float barY = y + (-scrollY / maxScroll) * (height - barH); + int bx = (int) (x + width - SCROLLBAR_W - 1); + extractor.fill(bx, iy, bx + SCROLLBAR_W, iy + ih, SCROLLBAR_BG); + extractor.fill(bx, (int) barY, bx + SCROLLBAR_W, (int) (barY + barH), SCROLLBAR_COLOR); + } + } + + // ── 滚动 ── + + /** 处理滚轮事件。返回 true 表示已消费。 */ + public boolean onScroll(float amount) { + if (contentHeight <= height) return false; + float maxScroll = contentHeight - height; + scrollY = Mth.clamp(scrollY + amount * 20, -maxScroll, 0); + return true; + } + + /** 命中测试包围盒。 */ + public LayoutRect hitRect() { + return LayoutRect.of(x, y, width, height); + } +} diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ScrollableScope.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ScrollableScope.java new file mode 100644 index 00000000..7f066d18 --- /dev/null +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ScrollableScope.java @@ -0,0 +1,7 @@ +package dev.anvilcraft.lib.v2.ui.component; + +import dev.anvilcraft.lib.v2.ui.UIScope; + +/** Scope for children inside a {@link ScrollableComponent}. */ +public class ScrollableScope extends UIScope { +} From 1e1496066b9b1752c31879c2af21b11e5d5e0a96 Mon Sep 17 00:00:00 2001 From: Gugle Date: Mon, 18 May 2026 23:45:48 +0800 Subject: [PATCH 21/67] feat(ui): update ButtonComponent and TextComponent to support drop shadow in text rendering --- .../dev/anvilcraft/lib/v2/ui/component/ButtonComponent.java | 2 +- .../dev/anvilcraft/lib/v2/ui/component/TextComponent.java | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ButtonComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ButtonComponent.java index fcb90753..51b9fed9 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ButtonComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ButtonComponent.java @@ -81,7 +81,7 @@ public void extractRenderState(GuiGraphicsExtractor extractor) { int textX = (int) (x + (width - textW) / 2f); int textY = (int) (y + (height - font.lineHeight) / 2f); - extractor.text(font, txt, textX, textY, TEXT_COLOR); + extractor.text(font, txt, textX, textY, TEXT_COLOR, true); } /** 按钮包围盒,用于命中测试。 */ diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextComponent.java index b7a0035b..69c11ff7 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextComponent.java @@ -71,10 +71,7 @@ public void extractRenderState(GuiGraphicsExtractor extractor) { case RIGHT -> x + width - textW; }; - if (dropShadow) { - extractor.text(font, text, (int) renderX + 1, (int) y + 1, 0x33000000); - } - extractor.text(font, text, (int) renderX, (int) y, color); + extractor.text(font, text, (int) renderX, (int) y, color, dropShadow); } } From 8ee7f96a8160d7b5c11e231282bce35e47733cfa Mon Sep 17 00:00:00 2001 From: Gugle Date: Mon, 18 May 2026 23:49:08 +0800 Subject: [PATCH 22/67] feat(ui): enhance DeclarativeTestScreen with scrollable layout for improved component organization --- .../client/screen/DeclarativeTestScreen.java | 193 ++++++++++-------- 1 file changed, 103 insertions(+), 90 deletions(-) diff --git a/module.test/src/main/java/dev/anvilcraft/lib/v2/test/client/screen/DeclarativeTestScreen.java b/module.test/src/main/java/dev/anvilcraft/lib/v2/test/client/screen/DeclarativeTestScreen.java index ebcc0533..2d6e3b1f 100644 --- a/module.test/src/main/java/dev/anvilcraft/lib/v2/test/client/screen/DeclarativeTestScreen.java +++ b/module.test/src/main/java/dev/anvilcraft/lib/v2/test/client/screen/DeclarativeTestScreen.java @@ -1,6 +1,12 @@ package dev.anvilcraft.lib.v2.test.client.screen; -import dev.anvilcraft.lib.v2.ui.*; +import dev.anvilcraft.lib.v2.ui.Alignment; +import dev.anvilcraft.lib.v2.ui.Composition; +import dev.anvilcraft.lib.v2.ui.DeclarativeScreen; +import dev.anvilcraft.lib.v2.ui.ForEach; +import dev.anvilcraft.lib.v2.ui.Modifier; +import dev.anvilcraft.lib.v2.ui.MutableState; +import dev.anvilcraft.lib.v2.ui.UIScope; import dev.anvilcraft.lib.v2.ui.component.TextComponent; import net.minecraft.network.chat.Component; @@ -23,96 +29,103 @@ protected void content(UIScope scope) { MutableState text = comp.remember(() -> new MutableState<>("")); scope.Column(col -> { - // ── 标题 ── - col.Text("=== AnvilLib Declarative UI ===").color(0xFFFFFF00); - - // ── 1. Text 样式 ── - col.Text("1) Text styles:"); - col.Row(row -> { - row.Text("Shadow").shadow(true).color(0xFFFFAA00); - row.Text(" | Left").align(TextComponent.Align.LEFT); - row.Text("Center").align(TextComponent.Align.CENTER).color(0xFF00AAFF); - row.Text("Right|").align(TextComponent.Align.RIGHT); - }).spacing(8); - - col.Spacer(0, 4); - - // ── 2. Button + 状态 ── - col.Text("2) Button + state (Counter):"); - col.Row(row -> { - row.Button("-", () -> counter.setValue(counter.getValue() - 1)); - row.Text(" " + counter.getValue() + " ").align(TextComponent.Align.CENTER); - row.Button("+", () -> counter.setValue(counter.getValue() + 1)); - row.Button("Reset", () -> counter.setValue(0)); - }).spacing(4); - - col.Spacer(0, 4); - - // ── 3. Box 层叠 ── - col.Text("3) Box overlay:"); - col.Box(box -> { - box.Text(" "); - box.Text("<< Overlay Text >>").color(0xFF00FF00); - }).contentAlignment(Alignment.Horizontal.Center, Alignment.Vertical.Center); - - col.Spacer(0, 4); - - // ── 4. Grid 网格 ── - col.Text("4) Grid (3 columns):"); - col.Grid(3, grid -> { - for (int i = 0; i < 6; i++) { - grid.Button("G" + i, () -> {}); + col.Scrollable( + this.height, scrollableScope -> { + // ── 标题 ── + scrollableScope.Text("=== AnvilLib Declarative UI ===").color(0xFFFFFF00); + + // ── 1. Text 样式 ── + scrollableScope.Text("1) Text styles:"); + scrollableScope.Row(row -> { + row.Text("Shadow").shadow(true).color(0xFFFFAA00); + row.Text(" | Left").align(TextComponent.Align.LEFT); + row.Text("Center").align(TextComponent.Align.CENTER).color(0xFF00AAFF); + row.Text("Right|").align(TextComponent.Align.RIGHT); + }).spacing(8); + + scrollableScope.Spacer(0, 4); + + // ── 2. Button + 状态 ── + scrollableScope.Text("2) Button + state (Counter):"); + scrollableScope.Row(row -> { + row.Button("-", () -> counter.setValue(counter.getValue() - 1)); + row.Text(" " + counter.getValue() + " ").align(TextComponent.Align.CENTER); + row.Button("+", () -> counter.setValue(counter.getValue() + 1)); + row.Button("Reset", () -> counter.setValue(0)); + }).spacing(4); + + scrollableScope.Spacer(0, 4); + + // ── 3. Box 层叠 ── + scrollableScope.Text("3) Box overlay:"); + scrollableScope.Box(box -> { + box.Text(" "); + box.Text("<< Overlay Text >>").color(0xFF00FF00); + }).contentAlignment(Alignment.Horizontal.Center, Alignment.Vertical.Center); + + scrollableScope.Spacer(0, 4); + + // ── 4. Grid 网格 ── + scrollableScope.Text("4) Grid (3 columns):"); + scrollableScope.Grid( + 3, grid -> { + for (int i = 0; i < 6; i++) { + grid.Button( + "G" + i, () -> { + } + ); + } + } + ).spacing(2, 2); + + scrollableScope.Spacer(0, 4); + + // ── 5. Checkbox ── + scrollableScope.Text("5) Checkbox:"); + scrollableScope.Row(row -> { + row.Checkbox("Enable feature", checked.getValue(), () -> checked.setValue(!checked.getValue())); + row.Text(" Enabled: " + checked.getValue()); + }).spacing(4); + + scrollableScope.Spacer(0, 4); + + // ── 6. Slider ── + MutableState sliderVal = comp.remember(() -> new MutableState<>(50f)); + scrollableScope.Text("6) Slider:"); + scrollableScope.Row(row -> { + row.Slider(sliderVal.getValue(), 0, 100, 100, v -> sliderVal.setValue(v)); + row.Text(" " + sliderVal.getValue().intValue() + "%"); + }).spacing(4); + + scrollableScope.Spacer(0, 4); + + // ── 7. TextField ── + scrollableScope.Text("7) TextField:"); + scrollableScope.Row(row -> { + row.TextField("Type here...", v -> text.setValue(v)); + row.Text(" Value: '" + text.getValue() + "'"); + }).spacing(4); + + scrollableScope.Spacer(0, 4); + + // ── 8. ForEach ── + scrollableScope.Text("8) ForEach (list of 4 items):"); + ForEach.of(scrollableScope, List.of("Apple", "Banana", "Cherry", "Date"), (s, item) -> s.Text(" - " + item)); + + scrollableScope.Spacer(0, 4); + + // ── 9. Modifier 样式 ── + scrollableScope.Text("9) Modifiers:"); + scrollableScope.Row(row -> { + row.Button( + "Styled", () -> { + } + ).modifier(Modifier.NONE.background(0xFF884444).border(1, 0xFFFF8888)); + row.Text(" "); + row.Text("Padded").modifier(Modifier.NONE.padding(8).background(0xFF444488)); + }).spacing(4); } - }).spacing(2, 2); - - col.Spacer(0, 4); - - // ── 5. Checkbox ── - col.Text("5) Checkbox:"); - col.Row(row -> { - row.Checkbox("Enable feature", checked.getValue(), - () -> checked.setValue(!checked.getValue())); - row.Text(" Enabled: " + checked.getValue()); - }).spacing(4); - - col.Spacer(0, 4); - - // ── 6. Slider ── - MutableState sliderVal = comp.remember(() -> new MutableState<>(50f)); - col.Text("6) Slider:"); - col.Row(row -> { - row.Slider(sliderVal.getValue(), 0, 100, 100, - v -> sliderVal.setValue(v)); - row.Text(" " + sliderVal.getValue().intValue() + "%"); - }).spacing(4); - - col.Spacer(0, 4); - - // ── 7. TextField ── - col.Text("7) TextField:"); - col.Row(row -> { - row.TextField("Type here...", v -> text.setValue(v)); - row.Text(" Value: '" + text.getValue() + "'"); - }).spacing(4); - - col.Spacer(0, 4); - - // ── 8. ForEach ── - col.Text("8) ForEach (list of 4 items):"); - ForEach.of(col, List.of("Apple", "Banana", "Cherry", "Date"), - (s, item) -> s.Text(" - " + item)); - - col.Spacer(0, 4); - - // ── 9. Modifier 样式 ── - col.Text("9) Modifiers:"); - col.Row(row -> { - row.Button("Styled", () -> {}) - .modifier(Modifier.NONE.background(0xFF884444).border(1, 0xFFFF8888)); - row.Text(" "); - row.Text("Padded").modifier(Modifier.NONE.padding(8).background(0xFF444488)); - }).spacing(4); - + ); }).spacing(6).modifier(Modifier.NONE.padding(10)); } } From 4d454271570ad82326b33db774bd79672b16975b Mon Sep 17 00:00:00 2001 From: Gugle Date: Mon, 18 May 2026 23:57:21 +0800 Subject: [PATCH 23/67] feat(ui): refactor DeclarativeTestScreen to utilize Scrollable for improved layout structure --- .../client/screen/DeclarativeTestScreen.java | 67 ++++++++++--------- .../v2/ui/component/ScrollableComponent.java | 3 + 2 files changed, 37 insertions(+), 33 deletions(-) diff --git a/module.test/src/main/java/dev/anvilcraft/lib/v2/test/client/screen/DeclarativeTestScreen.java b/module.test/src/main/java/dev/anvilcraft/lib/v2/test/client/screen/DeclarativeTestScreen.java index 2d6e3b1f..0eeae50b 100644 --- a/module.test/src/main/java/dev/anvilcraft/lib/v2/test/client/screen/DeclarativeTestScreen.java +++ b/module.test/src/main/java/dev/anvilcraft/lib/v2/test/client/screen/DeclarativeTestScreen.java @@ -28,46 +28,46 @@ protected void content(UIScope scope) { MutableState checked = comp.remember(() -> new MutableState<>(false)); MutableState text = comp.remember(() -> new MutableState<>("")); - scope.Column(col -> { - col.Scrollable( - this.height, scrollableScope -> { + scope.Scrollable( + this.height, scroll -> { + scroll.Column(col -> { // ── 标题 ── - scrollableScope.Text("=== AnvilLib Declarative UI ===").color(0xFFFFFF00); + col.Text("=== AnvilLib Declarative UI ===").color(0xFFFFFF00); // ── 1. Text 样式 ── - scrollableScope.Text("1) Text styles:"); - scrollableScope.Row(row -> { + col.Text("1) Text styles:"); + col.Row(row -> { row.Text("Shadow").shadow(true).color(0xFFFFAA00); row.Text(" | Left").align(TextComponent.Align.LEFT); row.Text("Center").align(TextComponent.Align.CENTER).color(0xFF00AAFF); row.Text("Right|").align(TextComponent.Align.RIGHT); }).spacing(8); - scrollableScope.Spacer(0, 4); + col.Spacer(0, 4); // ── 2. Button + 状态 ── - scrollableScope.Text("2) Button + state (Counter):"); - scrollableScope.Row(row -> { + col.Text("2) Button + state (Counter):"); + col.Row(row -> { row.Button("-", () -> counter.setValue(counter.getValue() - 1)); row.Text(" " + counter.getValue() + " ").align(TextComponent.Align.CENTER); row.Button("+", () -> counter.setValue(counter.getValue() + 1)); row.Button("Reset", () -> counter.setValue(0)); }).spacing(4); - scrollableScope.Spacer(0, 4); + col.Spacer(0, 4); // ── 3. Box 层叠 ── - scrollableScope.Text("3) Box overlay:"); - scrollableScope.Box(box -> { + col.Text("3) Box overlay:"); + col.Box(box -> { box.Text(" "); box.Text("<< Overlay Text >>").color(0xFF00FF00); }).contentAlignment(Alignment.Horizontal.Center, Alignment.Vertical.Center); - scrollableScope.Spacer(0, 4); + col.Spacer(0, 4); // ── 4. Grid 网格 ── - scrollableScope.Text("4) Grid (3 columns):"); - scrollableScope.Grid( + col.Text("4) Grid (3 columns):"); + col.Grid( 3, grid -> { for (int i = 0; i < 6; i++) { grid.Button( @@ -78,45 +78,45 @@ protected void content(UIScope scope) { } ).spacing(2, 2); - scrollableScope.Spacer(0, 4); + col.Spacer(0, 4); // ── 5. Checkbox ── - scrollableScope.Text("5) Checkbox:"); - scrollableScope.Row(row -> { + col.Text("5) Checkbox:"); + col.Row(row -> { row.Checkbox("Enable feature", checked.getValue(), () -> checked.setValue(!checked.getValue())); row.Text(" Enabled: " + checked.getValue()); }).spacing(4); - scrollableScope.Spacer(0, 4); + col.Spacer(0, 4); // ── 6. Slider ── MutableState sliderVal = comp.remember(() -> new MutableState<>(50f)); - scrollableScope.Text("6) Slider:"); - scrollableScope.Row(row -> { + col.Text("6) Slider:"); + col.Row(row -> { row.Slider(sliderVal.getValue(), 0, 100, 100, v -> sliderVal.setValue(v)); row.Text(" " + sliderVal.getValue().intValue() + "%"); }).spacing(4); - scrollableScope.Spacer(0, 4); + col.Spacer(0, 4); // ── 7. TextField ── - scrollableScope.Text("7) TextField:"); - scrollableScope.Row(row -> { + col.Text("7) TextField:"); + col.Row(row -> { row.TextField("Type here...", v -> text.setValue(v)); row.Text(" Value: '" + text.getValue() + "'"); }).spacing(4); - scrollableScope.Spacer(0, 4); + col.Spacer(0, 4); // ── 8. ForEach ── - scrollableScope.Text("8) ForEach (list of 4 items):"); - ForEach.of(scrollableScope, List.of("Apple", "Banana", "Cherry", "Date"), (s, item) -> s.Text(" - " + item)); + col.Text("8) ForEach (list of 4 items):"); + ForEach.of(col, List.of("Apple", "Banana", "Cherry", "Date"), (s, item) -> s.Text(" - " + item)); - scrollableScope.Spacer(0, 4); + col.Spacer(0, 4); // ── 9. Modifier 样式 ── - scrollableScope.Text("9) Modifiers:"); - scrollableScope.Row(row -> { + col.Text("9) Modifiers:"); + col.Row(row -> { row.Button( "Styled", () -> { } @@ -124,9 +124,10 @@ protected void content(UIScope scope) { row.Text(" "); row.Text("Padded").modifier(Modifier.NONE.padding(8).background(0xFF444488)); }).spacing(4); - } - ); - }).spacing(6).modifier(Modifier.NONE.padding(10)); + + }).spacing(6).modifier(Modifier.NONE.padding(10)); + } + ); } } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ScrollableComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ScrollableComponent.java index be6d51aa..2a001c04 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ScrollableComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ScrollableComponent.java @@ -57,6 +57,9 @@ public MeasuredSize measure(Constraints constraints) { for (UIComponent child : children) { MeasuredSize s = child.measure(childC); + // 修饰符会扩展尺寸(如 padding),需要计入内容高度 + s = child.modifier().foldOut(s, + (el, sz) -> el.modifyMeasuredSize(child, childC, sz)); sizes.add(s); totalH += s.height(); maxW = Math.max(maxW, s.width()); From 0d77eea0d3ff586fa073d12352dbd19e555297c7 Mon Sep 17 00:00:00 2001 From: Gugle Date: Tue, 19 May 2026 00:01:19 +0800 Subject: [PATCH 24/67] feat(ui): implement runtime state preservation for ScrollableComponent in Composition --- .../java/dev/anvilcraft/lib/v2/ui/Composition.java | 14 ++++++++++++++ .../lib/v2/ui/component/ScrollableComponent.java | 3 +++ 2 files changed, 17 insertions(+) diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Composition.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Composition.java index 4060431e..2b833f13 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Composition.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Composition.java @@ -1,5 +1,6 @@ package dev.anvilcraft.lib.v2.ui; +import dev.anvilcraft.lib.v2.ui.component.ScrollableComponent; import net.minecraft.client.gui.GuiGraphicsExtractor; import java.util.*; @@ -106,6 +107,11 @@ public void emit(UIComponent component) { Slot slot; if (currentIndex < slots.size()) { slot = slots.get(currentIndex); + // 同类型组件保留运行时状态(如滚动位置) + UIComponent old = slot.component; + if (old != null && old.getClass() == component.getClass()) { + copyRuntimeState(old, component); + } slot.component = component; } else { slot = new Slot(); @@ -116,6 +122,14 @@ public void emit(UIComponent component) { currentIndex++; } + /** 将旧组件的运行时状态复制到新组件。 */ + private void copyRuntimeState(UIComponent old, UIComponent replacement) { + if (old instanceof ScrollableComponent oldSc + && replacement instanceof ScrollableComponent newSc) { + newSc.setScrollY(oldSc.getScrollY()); + } + } + // ── frame entry point ── /** diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ScrollableComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ScrollableComponent.java index 2a001c04..eb6008f2 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ScrollableComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ScrollableComponent.java @@ -123,6 +123,9 @@ public boolean onScroll(float amount) { return true; } + public float getScrollY() { return scrollY; } + public void setScrollY(float scrollY) { this.scrollY = scrollY; } + /** 命中测试包围盒。 */ public LayoutRect hitRect() { return LayoutRect.of(x, y, width, height); From a40ff8a36a786215bc20cf9e1b768ab4d33e9c3a Mon Sep 17 00:00:00 2001 From: Gugle Date: Tue, 19 May 2026 00:05:28 +0800 Subject: [PATCH 25/67] feat(ui): implement runtime state preservation for ScrollableComponent in Composition --- .../v2/ui/component/CheckboxComponent.java | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/CheckboxComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/CheckboxComponent.java index 7c92dc28..61bab4b1 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/CheckboxComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/CheckboxComponent.java @@ -12,13 +12,14 @@ /** * 复选框。点击切换 boolean 状态。 - * 原版风格:16x16 方框 + 选中时内部对勾。 + * 16x16 方框,未选中=深色空心,选中=浅色填充。 */ public class CheckboxComponent implements UIComponent { - private static final int BOX_COLOR = 0xFF404040; - private static final int CHECK_COLOR = 0xFFFFFFFF; - private static final float SIZE = 16; + private static final int BOX_COLOR = 0xFF404040; + private static final int CHECKED_COLOR = 0xFFFFFFFF; + private static final float SIZE = 16; + private static final float INSET = 3; private Modifier modifier; private String label; @@ -61,15 +62,16 @@ public void layout(float x, float y, float width, float height) { public void extractRenderState(GuiGraphicsExtractor extractor) { int ix = (int) x, iy = (int) y; - // 方框背景 + // 外层深灰方块(始终显示) extractor.fill(ix, iy, ix + (int) SIZE, iy + (int) SIZE, BOX_COLOR); - // 选中对勾(简化:十字线) + // 选中时中间白色小方块 if (checked) { - int cx = ix + (int) SIZE / 2, cy = iy + (int) SIZE / 2; - int s = 4; - extractor.fill(cx - s, cy, cx, cy + s, CHECK_COLOR); // 左下-中心 - extractor.fill(cx, cy, cx + s + 2, cy - s, CHECK_COLOR); // 中心-右上 + int iix = ix + (int) INSET; + int iiy = iy + (int) INSET; + int iiw = (int) SIZE - (int) INSET * 2; + int iih = (int) SIZE - (int) INSET * 2; + extractor.fill(iix, iiy, iix + iiw, iiy + iih, CHECKED_COLOR); } // 标签文字 From ecb9b9d6e6da49c43eeb3a64cd0e730e2c37021a Mon Sep 17 00:00:00 2001 From: Gugle Date: Tue, 19 May 2026 00:08:59 +0800 Subject: [PATCH 26/67] feat(ui): enhance TextFieldComponent with state preservation and focus management --- .../dev/anvilcraft/lib/v2/ui/Composition.java | 7 ++++++ .../lib/v2/ui/DeclarativeScreen.java | 23 +++++++++++++++++++ .../v2/ui/component/TextFieldComponent.java | 5 +++- 3 files changed, 34 insertions(+), 1 deletion(-) diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Composition.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Composition.java index 2b833f13..9765536e 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Composition.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Composition.java @@ -1,6 +1,7 @@ package dev.anvilcraft.lib.v2.ui; import dev.anvilcraft.lib.v2.ui.component.ScrollableComponent; +import dev.anvilcraft.lib.v2.ui.component.TextFieldComponent; import net.minecraft.client.gui.GuiGraphicsExtractor; import java.util.*; @@ -128,6 +129,12 @@ private void copyRuntimeState(UIComponent old, UIComponent replacement) { && replacement instanceof ScrollableComponent newSc) { newSc.setScrollY(oldSc.getScrollY()); } + if (old instanceof TextFieldComponent oldTf + && replacement instanceof TextFieldComponent newTf) { + newTf.setBufferText(oldTf.getBufferText()); + newTf.setCursorPos(oldTf.getCursorPos()); + newTf.setFocused(oldTf.isFocused()); + } } // ── frame entry point ── diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java index 79a7d5c6..d4ba5e90 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java @@ -52,9 +52,32 @@ public void extractRenderState(GuiGraphicsExtractor extractor, int mouseX, int m if (composition != null) { composition.renderFrame(extractor, this.width, this.height); updateHover(mouseX, mouseY); + refreshFocus(); } } + /** recompose 后重新绑定 focusOwner(旧实例可能已被替换)。 */ + private void refreshFocus() { + if (focusOwner == null) return; + for (UIComponent child : rootScope.getChildren()) { + KeyInputHandler found = findFocused(child); + if (found != null) { + focusOwner = found; + return; + } + } + focusOwner = null; + } + + private KeyInputHandler findFocused(UIComponent component) { + if (component instanceof TextFieldComponent tf && tf.isFocused()) return tf; + for (UIComponent child : component.children()) { + KeyInputHandler found = findFocused(child); + if (found != null) return found; + } + return null; + } + // ── 鼠标输入 ── @Override diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextFieldComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextFieldComponent.java index bc037aae..86d1ece6 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextFieldComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextFieldComponent.java @@ -45,7 +45,10 @@ public TextFieldComponent(Modifier modifier, String initialText, } public TextFieldComponent modifier(Modifier m) { this.modifier = m; return this; } - public String text() { return buffer.toString(); } + public String getBufferText() { return buffer.toString(); } + public void setBufferText(String text) { buffer.setLength(0); buffer.append(text); } + public int getCursorPos() { return cursorPos; } + public void setCursorPos(int pos) { this.cursorPos = Math.clamp(pos, 0, buffer.length()); } public void setFocused(boolean focused) { this.focused = focused; } public boolean isFocused() { return focused; } From a065ed76d34aaa3534cf0321620cbc3d94a9e798 Mon Sep 17 00:00:00 2001 From: Gugle Date: Tue, 19 May 2026 00:11:49 +0800 Subject: [PATCH 27/67] feat(ui): refactor state management to use Ref instead of MutableState for improved performance --- .../v2/test/client/screen/DeclarativeTestScreen.java | 10 +++++----- .../java/dev/anvilcraft/lib/v2/ui/Composition.java | 11 ++++++++--- .../lib/v2/ui/{MutableState.java => Ref.java} | 4 ++-- 3 files changed, 15 insertions(+), 10 deletions(-) rename module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/{MutableState.java => Ref.java} (94%) diff --git a/module.test/src/main/java/dev/anvilcraft/lib/v2/test/client/screen/DeclarativeTestScreen.java b/module.test/src/main/java/dev/anvilcraft/lib/v2/test/client/screen/DeclarativeTestScreen.java index 0eeae50b..d0bd393e 100644 --- a/module.test/src/main/java/dev/anvilcraft/lib/v2/test/client/screen/DeclarativeTestScreen.java +++ b/module.test/src/main/java/dev/anvilcraft/lib/v2/test/client/screen/DeclarativeTestScreen.java @@ -5,7 +5,7 @@ import dev.anvilcraft.lib.v2.ui.DeclarativeScreen; import dev.anvilcraft.lib.v2.ui.ForEach; import dev.anvilcraft.lib.v2.ui.Modifier; -import dev.anvilcraft.lib.v2.ui.MutableState; +import dev.anvilcraft.lib.v2.ui.Ref; import dev.anvilcraft.lib.v2.ui.UIScope; import dev.anvilcraft.lib.v2.ui.component.TextComponent; import net.minecraft.network.chat.Component; @@ -24,9 +24,9 @@ public DeclarativeTestScreen() { @Override protected void content(UIScope scope) { Composition comp = Composition.current(); - MutableState counter = comp.remember(() -> new MutableState<>(0)); - MutableState checked = comp.remember(() -> new MutableState<>(false)); - MutableState text = comp.remember(() -> new MutableState<>("")); + Ref counter = comp.ref(0); + Ref checked = comp.ref(false); + Ref text = comp.ref(""); scope.Scrollable( this.height, scroll -> { @@ -90,7 +90,7 @@ protected void content(UIScope scope) { col.Spacer(0, 4); // ── 6. Slider ── - MutableState sliderVal = comp.remember(() -> new MutableState<>(50f)); + Ref sliderVal = comp.remember(() -> new Ref<>(50f)); col.Text("6) Slider:"); col.Row(row -> { row.Slider(sliderVal.getValue(), 0, 100, 100, v -> sliderVal.setValue(v)); diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Composition.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Composition.java index 9765536e..f8574a60 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Composition.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Composition.java @@ -79,7 +79,7 @@ public void watch(Animatable anim) { } } - // ── remember ── + // ── remember / ref ── /** * Persist a value across recompositions. @@ -98,6 +98,11 @@ public T remember(Supplier init) { return value; } + /** {@code comp.ref(0)} 等价于 {@code comp.remember(() -> new Ref<>(0))}。 */ + public Ref ref(T initialValue) { + return remember(() -> new Ref<>(initialValue)); + } + // ── emit ── /** @@ -236,9 +241,9 @@ private void renderTree(UIComponent component, GuiGraphicsExtractor extractor, C public static class Slot { UIComponent component; boolean dirty = true; - final Set> readStates = new HashSet<>(); + final Set> readStates = new HashSet<>(); - void addReadState(MutableState state) { + void addReadState(Ref state) { readStates.add(state); } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/MutableState.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Ref.java similarity index 94% rename from module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/MutableState.java rename to module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Ref.java index 2dd172bc..b3af25f6 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/MutableState.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Ref.java @@ -14,12 +14,12 @@ * * @param the type of value held */ -public class MutableState { +public class Ref { private T value; final Set readers = new HashSet<>(); - public MutableState(@Nullable T initialValue) { + public Ref(@Nullable T initialValue) { this.value = initialValue; } From 7d0b02b68ce502f19f69171cd86f08ebcfd4889b Mon Sep 17 00:00:00 2001 From: Gugle Date: Tue, 19 May 2026 00:13:04 +0800 Subject: [PATCH 28/67] feat(ui): simplify slider and text field value updates using method references --- .../lib/v2/test/client/screen/DeclarativeTestScreen.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/module.test/src/main/java/dev/anvilcraft/lib/v2/test/client/screen/DeclarativeTestScreen.java b/module.test/src/main/java/dev/anvilcraft/lib/v2/test/client/screen/DeclarativeTestScreen.java index d0bd393e..2f3ac70c 100644 --- a/module.test/src/main/java/dev/anvilcraft/lib/v2/test/client/screen/DeclarativeTestScreen.java +++ b/module.test/src/main/java/dev/anvilcraft/lib/v2/test/client/screen/DeclarativeTestScreen.java @@ -93,7 +93,7 @@ protected void content(UIScope scope) { Ref sliderVal = comp.remember(() -> new Ref<>(50f)); col.Text("6) Slider:"); col.Row(row -> { - row.Slider(sliderVal.getValue(), 0, 100, 100, v -> sliderVal.setValue(v)); + row.Slider(sliderVal.getValue(), 0, 100, 100, sliderVal::setValue); row.Text(" " + sliderVal.getValue().intValue() + "%"); }).spacing(4); @@ -102,7 +102,7 @@ protected void content(UIScope scope) { // ── 7. TextField ── col.Text("7) TextField:"); col.Row(row -> { - row.TextField("Type here...", v -> text.setValue(v)); + row.TextField("Type here...", text::setValue); row.Text(" Value: '" + text.getValue() + "'"); }).spacing(4); From d189dd07052fd3646a42b5ab4c48dc02f87a8f2c Mon Sep 17 00:00:00 2001 From: Gugle Date: Tue, 19 May 2026 00:26:03 +0800 Subject: [PATCH 29/67] feat(ui): translate comments to Chinese for better localization --- .../dev/anvilcraft/lib/v2/ui/Animatable.java | 2 +- .../dev/anvilcraft/lib/v2/ui/Composition.java | 52 ++++++------ .../dev/anvilcraft/lib/v2/ui/Constraints.java | 2 +- .../lib/v2/ui/DeclarativeScreen.java | 2 +- .../dev/anvilcraft/lib/v2/ui/LayoutRect.java | 2 +- .../anvilcraft/lib/v2/ui/MeasuredSize.java | 2 +- .../dev/anvilcraft/lib/v2/ui/Modifier.java | 10 +-- .../java/dev/anvilcraft/lib/v2/ui/Ref.java | 15 ++-- .../dev/anvilcraft/lib/v2/ui/UIComponent.java | 24 +++--- .../dev/anvilcraft/lib/v2/ui/UIScope.java | 80 +++++++++++++++++-- .../lib/v2/ui/component/BoxScope.java | 2 +- .../lib/v2/ui/component/ColumnComponent.java | 4 +- .../lib/v2/ui/component/ColumnScope.java | 2 +- .../lib/v2/ui/component/GridScope.java | 2 +- .../lib/v2/ui/component/RowScope.java | 2 +- .../lib/v2/ui/component/ScrollableScope.java | 2 +- .../v2/ui/component/TextFieldComponent.java | 8 +- .../lib/v2/ui/modifier/ModifierElement.java | 6 +- 18 files changed, 141 insertions(+), 78 deletions(-) diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Animatable.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Animatable.java index ebca8e45..13c29b8d 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Animatable.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Animatable.java @@ -50,7 +50,7 @@ public boolean tick() { if (elapsed >= durationTicks) return false; elapsed++; float t = durationTicks > 0 ? (float) elapsed / durationTicks : 1f; - // ease-in-out + // 缓入缓出 float eased = t < 0.5f ? 2f * t * t : -1f + (4f - 2f * t) * t; this.value = Mth.lerp(eased, startValue, targetValue); return elapsed < durationTicks; diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Composition.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Composition.java index f8574a60..5f8d042d 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Composition.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Composition.java @@ -11,28 +11,26 @@ import org.jspecify.annotations.Nullable; /** - * The composition engine that drives recomposition, state tracking, and rendering. + * 组合引擎,驱动 recompose、状态追踪和渲染。 *

- * One Composition is created per {@link DeclarativeScreen}. - * It manages a flat slot table indexed by call-site position. - * During recomposition, the content lambda replays and each - * {@link #emit(UIComponent)} call diffs against the slot at the - * same index. + * 每个 {@link DeclarativeScreen} 创建一个 Composition。 + * 它维护一个按调用位置索引的扁平 slot table。 + * recompose 时内容 lambda 重执行,每次 {@link #emit(UIComponent)} 调用 + * 与同索引的 slot 做 diff。 *

- * State reads are tracked per slot so that writes only mark - * affected slots dirty — not the entire tree. + * 状态读取按 slot 追踪,写入只标记受影响 slot 为脏——不会波及整棵树。 */ public class Composition { private static final ThreadLocal CURRENT = new ThreadLocal<>(); - /** Returns the composition active on this thread, or null. */ + /** 返回当前线程上的组合实例,可能为 null。 */ @Nullable public static Composition currentOrNull() { return CURRENT.get(); } - /** Returns the composition active on this thread, throwing if absent. */ + /** 返回当前线程上的组合实例,不存在则抛出异常。 */ public static Composition current() { Composition c = CURRENT.get(); if (c == null) { @@ -48,11 +46,11 @@ public static Composition current() { private int currentRememberKey; private final Map rememberedValues = new HashMap<>(); - /** The slot currently being emitted (set during {@link #emit}). */ + /** 当前正在 emit 的 slot(在 {@link #emit} 期间设置)。 */ @Nullable Slot currentSlot; - // ── state ── + // ── 状态 ── private boolean dirty = true; private Consumer content; @@ -67,7 +65,7 @@ public void setContent(Consumer content) { this.content = content; } - /** Mark the composition as needing recomposition next frame. */ + /** 标记组合需要在下一帧 recompose。 */ public void invalidate() { dirty = true; } @@ -82,9 +80,7 @@ public void watch(Animatable anim) { // ── remember / ref ── /** - * Persist a value across recompositions. - * The init supplier is only called on first composition; - * subsequent recompositions return the existing value. + * 在多次 recompose 间持久化一个值。init supplier 只在首次组合时调用。 */ @SuppressWarnings("unchecked") public T remember(Supplier init) { @@ -106,8 +102,7 @@ public Ref ref(T initialValue) { // ── emit ── /** - * Emit a component to the current call-site position in the slot table. - * Called by component factory functions. + * 向当前调用位置的 slot 中 emit 一个组件。由组件工厂函数调用。 */ public void emit(UIComponent component) { Slot slot; @@ -142,11 +137,11 @@ private void copyRuntimeState(UIComponent old, UIComponent replacement) { } } - // ── frame entry point ── + // ── 每帧入口 ── /** - * Called every frame from {@link DeclarativeScreen#extractRenderState}. - * Runs recomposition if dirty, then measure → layout → render. + * 每帧从 {@link DeclarativeScreen#extractRenderState} 调用。 + * 若脏则 recompose,然后 measure → layout → render。 */ public void renderFrame(GuiGraphicsExtractor extractor, float screenWidth, float screenHeight) { CURRENT.set(this); @@ -173,7 +168,7 @@ private void recompose() { rootScope.clearChildren(); currentIndex = 0; currentRememberKey = 0; - // Clear slot dirty flags before recompose + // recompose 前清除 slot 脏标记 for (Slot slot : slots) { slot.dirty = false; } @@ -190,7 +185,14 @@ private boolean hasDirtySlots() { return false; } - // ── measure → layout → render walk ── + // ── measure → layout → render 遍历 ── + + // 1. 修饰符作用于约束 + // 2. 测量 + // 3. 布局 + // 4. 发射修饰符渲染状态(背景、边框等) + // 5. 发射组件自身渲染状态 + // 6. 递归子组件(容器组件的 measure/layout/extractRenderState 自行处理) private void renderTree(UIComponent component, GuiGraphicsExtractor extractor, Constraints constraints) { // 1. Apply modifier to constraints @@ -235,8 +237,8 @@ private void renderTree(UIComponent component, GuiGraphicsExtractor extractor, C // ── slot ── /** - * A position in the slot table. Each slot holds a component and - * tracks which states it reads for precise dirty marking. + * slot table 中的一个位置。每个 slot 持有一个组件, + * 并追踪它读取了哪些状态,以便精确标记脏。 */ public static class Slot { UIComponent component; diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Constraints.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Constraints.java index c99e90a6..1a2abbc2 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Constraints.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Constraints.java @@ -1,7 +1,7 @@ package dev.anvilcraft.lib.v2.ui; /** - * Min/max bounds passed from parent to child during measure. + * 父容器传给子组件的 min/max 尺寸约束。 */ public record Constraints(float minWidth, float maxWidth, float minHeight, float maxHeight) { diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java index d4ba5e90..0646401d 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java @@ -216,7 +216,7 @@ private void updateHoverRecursive(UIComponent component, float mx, float my) { } } - // ── internal ── + // ── 内部类 ── private static class RootScope extends UIScope { } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/LayoutRect.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/LayoutRect.java index 8c11e56a..4f2ec29c 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/LayoutRect.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/LayoutRect.java @@ -1,7 +1,7 @@ package dev.anvilcraft.lib.v2.ui; /** - * A positioned rectangle after the layout pass. + * 布局阶段之后的定位矩形。 */ public record LayoutRect(float x, float y, float width, float height) { diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/MeasuredSize.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/MeasuredSize.java index 516ee6e4..60940f2e 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/MeasuredSize.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/MeasuredSize.java @@ -1,7 +1,7 @@ package dev.anvilcraft.lib.v2.ui; /** - * Result of a component's measure pass. + * 组件 measure 阶段的结果。 */ public record MeasuredSize(float width, float height) { diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Modifier.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Modifier.java index 42520c75..d53043b6 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Modifier.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Modifier.java @@ -6,10 +6,10 @@ import java.util.function.BiFunction; /** - * Chainable modifier API. Modifiers form a linked list via {@link #then}. + * 链式修饰符 API。修饰符通过 {@link #then} 形成链表。 *

- * Each modifier element can participate in measure, layout, and render phases. - * Callers fold over the chain with {@link #foldIn} / {@link #foldOut}. + * 每个修饰符元素可参与 measure、layout、render 阶段。 + * 调用方通过 {@link #foldIn} / {@link #foldOut} 遍历链。 */ public interface Modifier { @@ -19,12 +19,12 @@ public interface Modifier { R foldOut(R initial, BiFunction operation); - /** Return a new chain with the given element prepended. */ + /** 返回一个前置了给定元素的新链。 */ default Modifier prepend(ModifierElement element) { return then(new SingleElementModifier(element)); } - // ── factory shortcuts ── + // ── 工厂快捷方法 ── default Modifier size(float width, float height) { return prepend(ModifierElement.size(width, height)); diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Ref.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Ref.java index b3af25f6..4d2a4440 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Ref.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Ref.java @@ -7,12 +7,12 @@ import org.jspecify.annotations.Nullable; /** - * An observable state holder. + * 可观察状态持有者。 *

- * Reads are tracked against the current composition slot. - * Writes mark all reader slots dirty so recomposition is scoped. + * 读取时追踪当前 composition slot,写入时标记所有 reader slot 为脏, + * 从而实现精确范围的 recompose。 * - * @param the type of value held + * @param 持有值的类型 */ public class Ref { @@ -24,8 +24,7 @@ public Ref(@Nullable T initialValue) { } /** - * Read the current value, recording this slot as a reader - * if called within a composition emission. + * 读取当前值。若在 composition emission 期间调用,记录此 slot 为 reader。 */ @Nullable public T getValue() { @@ -37,9 +36,7 @@ public T getValue() { return value; } - /** - * Set a new value. If changed, marks all reader slots dirty. - */ + /** 设置新值。若值发生变化,标记所有 reader slot 为脏。 */ public void setValue(@Nullable T newValue) { if (!Objects.equals(value, newValue)) { value = newValue; diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIComponent.java index 8d5128aa..b303ad7a 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIComponent.java @@ -5,38 +5,36 @@ import java.util.List; /** - * Core interface for all UI components. + * 所有 UI 组件的核心接口。 *

- * Components participate in three phases each frame: + * 组件每帧参与三个阶段: *

    - *
  1. {@link #measure(Constraints)} — determine desired size given parent constraints
  2. - *
  3. {@link #layout(float, float, float, float)} — receive final position from parent
  4. - *
  5. {@link #extractRenderState(GuiGraphicsExtractor)} — submit render states for GPU rendering
  6. + *
  7. {@link #measure(Constraints)} — 根据父容器约束确定期望尺寸
  8. + *
  9. {@link #layout(float, float, float, float)} — 接收父容器分配的最终位置
  10. + *
  11. {@link #extractRenderState(GuiGraphicsExtractor)} — 提交渲染状态给 GPU
  12. *
*/ public interface UIComponent { - /** The modifier chain applied to this component. */ + /** 应用于此组件的修饰符链。 */ Modifier modifier(); - /** Children of this component, or empty list for leaf components. */ + /** 子组件列表,叶子组件返回空列表。 */ List children(); /** - * Measure this component given parent constraints. - * Container components recursively measure children. + * 根据父容器约束测量此组件。容器组件递归测量子组件。 */ MeasuredSize measure(Constraints constraints); /** - * Set final position after layout pass. - * Container components position their children. + * 布局阶段后设置最终位置。容器组件在此方法内定位子组件。 */ void layout(float x, float y, float width, float height); /** - * Submit render states to the Minecraft GUI render pipeline. - * Called after measure+layout, once per frame. + * 提交渲染状态到 Minecraft GUI 渲染管线。 + * 在 measure+layout 之后调用,每帧一次。 */ void extractRenderState(GuiGraphicsExtractor extractor); } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIScope.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIScope.java index 23fb9b8c..bc0b4bcf 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIScope.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIScope.java @@ -28,33 +28,39 @@ import org.jspecify.annotations.Nullable; /** - * Base scope for building component trees. + * 组件树的构建作用域。 *

- * Container components (Column, Row, Box) create a scope, - * run their content lambda against it, then collect the children. + * 容器组件(Column、Row、Box 等)创建一个 scope, + * 对其执行内容 lambda,然后收集子组件。 *

- * Component builders are defined here as concrete methods - * so all scope subclasses inherit them. + * 组件构建器在此定义为具体方法,所有 scope 子类自动继承。 */ public abstract class UIScope { final List children = new ArrayList<>(); + /** 向当前 scope 添加一个子组件。 */ public void addChild(UIComponent child) { children.add(child); } + /** 返回当前 scope 中已收集的子组件列表(只读)。 */ public List getChildren() { return Collections.unmodifiableList(children); } - /** Internal: clear children before recomposition. */ + /** 内部方法:recompose 前清空子组件。 */ void clearChildren() { children.clear(); } - // ── component builders ── + // ── 组件构建器 ── + /** + * 创建一行文字。 + * @param text 显示文本 + * @return TextComponent 实例,可链式设置颜色、对齐、阴影等 + */ public TextComponent Text(String text) { TextComponent c = new TextComponent(Modifier.NONE, text); addChild(c); @@ -62,6 +68,11 @@ public TextComponent Text(String text) { return c; } + /** + * 创建固定尺寸的空白占位。 + * @param width 宽度(像素) + * @param height 高度(像素) + */ public SpacerComponent Spacer(float width, float height) { SpacerComponent c = new SpacerComponent(Modifier.NONE, width, height); addChild(c); @@ -69,6 +80,12 @@ public SpacerComponent Spacer(float width, float height) { return c; } + /** + * 创建一个纹理精灵图片。 + * @param sprite 纹理标识符 + * @param width 显示宽度 + * @param height 显示高度 + */ public ImageComponent Image(Identifier sprite, float width, float height) { ImageComponent c = new ImageComponent(Modifier.NONE, sprite, width, height); addChild(c); @@ -76,6 +93,12 @@ public ImageComponent Image(Identifier sprite, float width, float height) { return c; } + /** + * 创建复选框。 + * @param label 标签文字 + * @param checked 初始选中状态 + * @param onToggle 切换时的回调 + */ public CheckboxComponent Checkbox(String label, boolean checked, Runnable onToggle) { CheckboxComponent c = new CheckboxComponent(Modifier.NONE, label, checked, onToggle); addChild(c); @@ -83,6 +106,14 @@ public CheckboxComponent Checkbox(String label, boolean checked, Runnable onTogg return c; } + /** + * 创建滑块。 + * @param value 初始值 + * @param min 最小值 + * @param max 最大值 + * @param width 轨道宽度(像素) + * @param onChange 值变化回调,接收新值 + */ public SliderComponent Slider(float value, float min, float max, float width, Consumer onChange) { SliderComponent c = new SliderComponent(Modifier.NONE, value, min, max, width, onChange); @@ -91,6 +122,11 @@ public SliderComponent Slider(float value, float min, float max, float width, return c; } + /** + * 创建单行文本输入框。 + * @param initialText 初始文字 + * @param onChange 文字变化回调,接收完整文本 + */ public TextFieldComponent TextField(String initialText, Consumer onChange) { TextFieldComponent c = new TextFieldComponent(Modifier.NONE, initialText, onChange); @@ -99,6 +135,11 @@ public TextFieldComponent TextField(String initialText, return c; } + /** + * 创建可点击按钮。 + * @param label 按钮文字 + * @param onClick 点击回调(可为 null) + */ public ButtonComponent Button(String label, @Nullable Runnable onClick) { ButtonComponent c = new ButtonComponent(Modifier.NONE, label, onClick); addChild(c); @@ -106,6 +147,11 @@ public ButtonComponent Button(String label, @Nullable Runnable onClick) { return c; } + /** + * 创建纵向线性布局容器。 + * @param content 子组件声明 lambda + * @return ColumnComponent 实例,可链式设置 spacing、alignment 等 + */ public ColumnComponent Column(Consumer content) { ColumnComponent c = new ColumnComponent(Modifier.NONE); ColumnScope inner = new ColumnScope(); @@ -116,6 +162,11 @@ public ColumnComponent Column(Consumer content) { return c; } + /** + * 创建横向线性布局容器。 + * @param content 子组件声明 lambda + * @return RowComponent 实例,可链式设置 spacing、alignment 等 + */ public RowComponent Row(Consumer content) { RowComponent c = new RowComponent(Modifier.NONE); RowScope inner = new RowScope(); @@ -126,6 +177,10 @@ public RowComponent Row(Consumer content) { return c; } + /** + * 创建层叠布局容器。子组件按声明顺序从底到顶重叠。 + * @param content 子组件声明 lambda + */ public BoxComponent Box(Consumer content) { BoxComponent c = new BoxComponent(Modifier.NONE); BoxScope inner = new BoxScope(); @@ -136,6 +191,12 @@ public BoxComponent Box(Consumer content) { return c; } + /** + * 创建网格布局容器。 + * @param columns 列数 + * @param content 子组件声明 lambda + * @return GridComponent 实例,可链式设置 spacing + */ public GridComponent Grid(int columns, Consumer content) { GridComponent c = new GridComponent(Modifier.NONE, columns); GridScope inner = new GridScope(); @@ -146,6 +207,11 @@ public GridComponent Grid(int columns, Consumer content) { return c; } + /** + * 创建可滚动容器。内容超过 maxHeight 时可垂直滚动,自动裁剪并显示滚动条。 + * @param maxHeight 容器最大可见高度(像素) + * @param content 子组件声明 lambda + */ public ScrollableComponent Scrollable(float maxHeight, Consumer content) { ScrollableComponent c = new ScrollableComponent(Modifier.NONE, maxHeight); ScrollableScope inner = new ScrollableScope(); diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/BoxScope.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/BoxScope.java index aba58454..3f0d5557 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/BoxScope.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/BoxScope.java @@ -3,7 +3,7 @@ import dev.anvilcraft.lib.v2.ui.UIScope; /** - * Scope for children inside a {@link BoxComponent}. + * {@link BoxComponent} 的子级作用域。 */ public class BoxScope extends UIScope { } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ColumnComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ColumnComponent.java index f7f9c5ee..6101647f 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ColumnComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ColumnComponent.java @@ -20,7 +20,7 @@ public class ColumnComponent implements UIComponent { private Alignment.Horizontal horizontalAlignment = Alignment.Horizontal.Start; private float spacing; - // layout state + // 布局状态 private float x, y, width, height; public ColumnComponent(Modifier modifier) { @@ -31,7 +31,7 @@ public void setChildren(List children) { this.children = List.copyOf(children); } - // ── chained setters ── + // ── 链式 setter ── public ColumnComponent verticalArrangement(Arrangement.Vertical va) { this.verticalArrangement = va; diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ColumnScope.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ColumnScope.java index 493b298b..5c4bdbcb 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ColumnScope.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ColumnScope.java @@ -3,7 +3,7 @@ import dev.anvilcraft.lib.v2.ui.UIScope; /** - * Scope for children inside a {@link ColumnComponent}. + * {@link ColumnComponent} 的子级作用域。 */ public class ColumnScope extends UIScope { } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/GridScope.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/GridScope.java index e85bb6bc..e63e7de1 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/GridScope.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/GridScope.java @@ -2,6 +2,6 @@ import dev.anvilcraft.lib.v2.ui.UIScope; -/** Scope for children inside a {@link GridComponent}. */ +/** {@link GridComponent} 子级作用域。 */ public class GridScope extends UIScope { } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/RowScope.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/RowScope.java index b52d0896..f810e42a 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/RowScope.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/RowScope.java @@ -3,7 +3,7 @@ import dev.anvilcraft.lib.v2.ui.UIScope; /** - * Scope for children inside a {@link RowComponent}. + * {@link RowComponent} 的子级作用域。 */ public class RowScope extends UIScope { } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ScrollableScope.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ScrollableScope.java index 7f066d18..b3b5cf79 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ScrollableScope.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ScrollableScope.java @@ -2,6 +2,6 @@ import dev.anvilcraft.lib.v2.ui.UIScope; -/** Scope for children inside a {@link ScrollableComponent}. */ +/** {@link ScrollableComponent} 子级作用域。 */ public class ScrollableScope extends UIScope { } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextFieldComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextFieldComponent.java index 86d1ece6..03e7dcc6 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextFieldComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextFieldComponent.java @@ -93,7 +93,7 @@ public void extractRenderState(GuiGraphicsExtractor extractor) { } } - // ── KeyInputHandler ── + // ── 键盘输入处理 ── @Override public boolean onKeyPressed(KeyEvent event) { @@ -101,7 +101,7 @@ public boolean onKeyPressed(KeyEvent event) { int mods = event.modifiers(); // 控制键 - if (key == 259) { // Backspace + if (key == 259) { // 退格 if (cursorPos > 0) { buffer.deleteCharAt(cursorPos - 1); cursorPos--; fireChange(); } return true; } @@ -109,8 +109,8 @@ public boolean onKeyPressed(KeyEvent event) { if (cursorPos < buffer.length()) { buffer.deleteCharAt(cursorPos); fireChange(); } return true; } - if (key == 263) { if (cursorPos > 0) cursorPos--; return true; } // Left - if (key == 262) { if (cursorPos < buffer.length()) cursorPos++; return true; } // Right + if (key == 263) { if (cursorPos > 0) cursorPos--; return true; } // ← + if (key == 262) { if (cursorPos < buffer.length()) cursorPos++; return true; } // → if (key == 268) { cursorPos = 0; return true; } // Home if (key == 269) { cursorPos = buffer.length(); return true; } // End diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/ModifierElement.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/ModifierElement.java index 270c3332..7ad70e96 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/ModifierElement.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/ModifierElement.java @@ -7,8 +7,8 @@ import net.minecraft.client.gui.GuiGraphicsExtractor; /** - * A single node in a {@link dev.anvilcraft.lib.v2.ui.Modifier} chain. - * Each element can intercept measure, layout, and render phases. + * {@link dev.anvilcraft.lib.v2.ui.Modifier} 链中的单个节点。 + * 每个元素可拦截 measure、layout、render 阶段。 */ public interface ModifierElement { @@ -27,7 +27,7 @@ default LayoutRect modifyLayout(LayoutRect rect) { default void emitRenderState(GuiGraphicsExtractor extractor, LayoutRect bounds) { } - // ── factory methods ── + // ── 工厂方法 ── static ModifierElement size(float width, float height) { return new SizeElement(width, width, height, height); From 21b748255536187e5978e4204bdbfa91c3475d91 Mon Sep 17 00:00:00 2001 From: Gugle Date: Tue, 19 May 2026 00:35:20 +0800 Subject: [PATCH 30/67] feat(ui): replace TextFieldComponent with TextInputComponent for improved input handling --- .../client/screen/DeclarativeTestScreen.java | 8 +- .../dev/anvilcraft/lib/v2/ui/Composition.java | 12 +- .../lib/v2/ui/DeclarativeScreen.java | 17 +- .../dev/anvilcraft/lib/v2/ui/UIScope.java | 11 +- .../v2/ui/component/TextFieldComponent.java | 148 ------------------ .../v2/ui/component/TextInputComponent.java | 145 +++++++++++++++++ .../lib/v2/ui/input/KeyInputHandler.java | 9 +- 7 files changed, 181 insertions(+), 169 deletions(-) delete mode 100644 module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextFieldComponent.java create mode 100644 module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextInputComponent.java diff --git a/module.test/src/main/java/dev/anvilcraft/lib/v2/test/client/screen/DeclarativeTestScreen.java b/module.test/src/main/java/dev/anvilcraft/lib/v2/test/client/screen/DeclarativeTestScreen.java index 2f3ac70c..fac66f49 100644 --- a/module.test/src/main/java/dev/anvilcraft/lib/v2/test/client/screen/DeclarativeTestScreen.java +++ b/module.test/src/main/java/dev/anvilcraft/lib/v2/test/client/screen/DeclarativeTestScreen.java @@ -40,7 +40,7 @@ protected void content(UIScope scope) { row.Text("Shadow").shadow(true).color(0xFFFFAA00); row.Text(" | Left").align(TextComponent.Align.LEFT); row.Text("Center").align(TextComponent.Align.CENTER).color(0xFF00AAFF); - row.Text("Right|").align(TextComponent.Align.RIGHT); + row.Text("Right |").align(TextComponent.Align.RIGHT); }).spacing(8); col.Spacer(0, 4); @@ -99,10 +99,10 @@ protected void content(UIScope scope) { col.Spacer(0, 4); - // ── 7. TextField ── - col.Text("7) TextField:"); + // ── 7. TextInput ── + col.Text("7) TextInput:"); col.Row(row -> { - row.TextField("Type here...", text::setValue); + row.TextInput("Enter text...", text::setValue); row.Text(" Value: '" + text.getValue() + "'"); }).spacing(4); diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Composition.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Composition.java index 5f8d042d..333858ce 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Composition.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Composition.java @@ -1,7 +1,7 @@ package dev.anvilcraft.lib.v2.ui; import dev.anvilcraft.lib.v2.ui.component.ScrollableComponent; -import dev.anvilcraft.lib.v2.ui.component.TextFieldComponent; +import dev.anvilcraft.lib.v2.ui.component.TextInputComponent; import net.minecraft.client.gui.GuiGraphicsExtractor; import java.util.*; @@ -129,11 +129,11 @@ private void copyRuntimeState(UIComponent old, UIComponent replacement) { && replacement instanceof ScrollableComponent newSc) { newSc.setScrollY(oldSc.getScrollY()); } - if (old instanceof TextFieldComponent oldTf - && replacement instanceof TextFieldComponent newTf) { - newTf.setBufferText(oldTf.getBufferText()); - newTf.setCursorPos(oldTf.getCursorPos()); - newTf.setFocused(oldTf.isFocused()); + if (old instanceof TextInputComponent oldTi + && replacement instanceof TextInputComponent newTi) { + newTi.setValue(oldTi.getValue()); + newTi.setCursorPos(oldTi.getCursorPos()); + newTi.setFocused(oldTi.isFocused()); } } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java index 0646401d..6a8dc541 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java @@ -4,11 +4,12 @@ import dev.anvilcraft.lib.v2.ui.component.CheckboxComponent; import dev.anvilcraft.lib.v2.ui.component.ScrollableComponent; import dev.anvilcraft.lib.v2.ui.component.SliderComponent; -import dev.anvilcraft.lib.v2.ui.component.TextFieldComponent; +import dev.anvilcraft.lib.v2.ui.component.TextInputComponent; import dev.anvilcraft.lib.v2.ui.input.KeyInputHandler; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiGraphicsExtractor; 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.client.resources.sounds.SimpleSoundInstance; @@ -70,7 +71,7 @@ private void refreshFocus() { } private KeyInputHandler findFocused(UIComponent component) { - if (component instanceof TextFieldComponent tf && tf.isFocused()) return tf; + if (component instanceof TextInputComponent tf && tf.isFocused()) return tf; for (UIComponent child : component.children()) { KeyInputHandler found = findFocused(child); if (found != null) return found; @@ -136,6 +137,14 @@ public boolean keyPressed(KeyEvent event) { return super.keyPressed(event); } + @Override + public boolean charTyped(CharacterEvent event) { + if (focusOwner != null && focusOwner.onCharTyped(event)) { + return true; + } + return super.charTyped(event); + } + @Override public void onClose() { super.onClose(); @@ -161,7 +170,7 @@ private boolean hitTestClick(UIComponent component, float px, float py) { sl.setValueFromMouse(px); return true; } - if (component instanceof TextFieldComponent tf && tf.hitRect().contains(px, py)) { + if (component instanceof TextInputComponent tf && tf.hitRect().contains(px, py)) { tf.setFocused(true); focusOwner = tf; return true; @@ -196,7 +205,7 @@ private boolean hitTestScroll(UIComponent component, float px, float py, float a /** 清除组件树中所有 TextField 的焦点。 */ private void clearFocusRecursive(UIComponent component) { - if (component instanceof TextFieldComponent tf) tf.setFocused(false); + if (component instanceof TextInputComponent tf) tf.setFocused(false); for (UIComponent child : component.children()) clearFocusRecursive(child); } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIScope.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIScope.java index bc0b4bcf..d7b54981 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIScope.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIScope.java @@ -3,7 +3,7 @@ import dev.anvilcraft.lib.v2.ui.component.ButtonComponent; import dev.anvilcraft.lib.v2.ui.component.CheckboxComponent; import dev.anvilcraft.lib.v2.ui.component.SliderComponent; -import dev.anvilcraft.lib.v2.ui.component.TextFieldComponent; +import dev.anvilcraft.lib.v2.ui.component.TextInputComponent; import dev.anvilcraft.lib.v2.ui.component.BoxComponent; import dev.anvilcraft.lib.v2.ui.component.BoxScope; import dev.anvilcraft.lib.v2.ui.component.ColumnComponent; @@ -124,12 +124,11 @@ public SliderComponent Slider(float value, float min, float max, float width, /** * 创建单行文本输入框。 - * @param initialText 初始文字 - * @param onChange 文字变化回调,接收完整文本 + * @param placeholder 占位提示文字(灰色,仅在无输入时显示) + * @param onChange 文字变化回调,接收当前完整文本 */ - public TextFieldComponent TextField(String initialText, - Consumer onChange) { - TextFieldComponent c = new TextFieldComponent(Modifier.NONE, initialText, onChange); + public TextInputComponent TextInput(String placeholder, Consumer onChange) { + TextInputComponent c = new TextInputComponent(Modifier.NONE, placeholder, onChange); addChild(c); Composition.current().emit(c); return c; diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextFieldComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextFieldComponent.java deleted file mode 100644 index 03e7dcc6..00000000 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextFieldComponent.java +++ /dev/null @@ -1,148 +0,0 @@ -package dev.anvilcraft.lib.v2.ui.component; - -import dev.anvilcraft.lib.v2.ui.Constraints; -import dev.anvilcraft.lib.v2.ui.LayoutRect; -import dev.anvilcraft.lib.v2.ui.MeasuredSize; -import dev.anvilcraft.lib.v2.ui.Modifier; -import dev.anvilcraft.lib.v2.ui.UIComponent; -import dev.anvilcraft.lib.v2.ui.input.KeyInputHandler; -import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.GuiGraphicsExtractor; -import net.minecraft.client.input.KeyEvent; -import org.jspecify.annotations.Nullable; - -import java.util.Collections; -import java.util.List; -import java.util.function.Consumer; - -/** - * 单行文本输入框。支持键盘输入、光标、Backspace/Delete、Home/End。 - * 需要焦点:点击获得焦点,点击外部失去焦点。 - */ -public class TextFieldComponent implements UIComponent, KeyInputHandler { - - private static final int BG_COLOR = 0xFF202020; - private static final int TEXT_COLOR = 0xFFFFFFFF; - private static final int CURSOR_COLOR = 0xFFFFFFFF; - private static final float PADDING_H = 4; - private static final float PADDING_V = 4; - private static final float WIDTH = 160; - - private Modifier modifier; - private final StringBuilder buffer = new StringBuilder(); - private int cursorPos; - private boolean focused; - private Consumer onChange; - - private float x, y, width, height; - - public TextFieldComponent(Modifier modifier, String initialText, - Consumer onChange) { - this.modifier = modifier; - this.buffer.append(initialText != null ? initialText : ""); - this.cursorPos = this.buffer.length(); - this.onChange = onChange; - } - - public TextFieldComponent modifier(Modifier m) { this.modifier = m; return this; } - public String getBufferText() { return buffer.toString(); } - public void setBufferText(String text) { buffer.setLength(0); buffer.append(text); } - public int getCursorPos() { return cursorPos; } - public void setCursorPos(int pos) { this.cursorPos = Math.clamp(pos, 0, buffer.length()); } - public void setFocused(boolean focused) { this.focused = focused; } - public boolean isFocused() { return focused; } - - @Override public Modifier modifier() { return modifier; } - @Override public List children() { return Collections.emptyList(); } - - @Override - public MeasuredSize measure(Constraints constraints) { - var font = Minecraft.getInstance().font; - return MeasuredSize.of( - constraints.constrainWidth(WIDTH), - constraints.constrainHeight(font.lineHeight + PADDING_V * 2) - ); - } - - @Override - public void layout(float x, float y, float width, float height) { - this.x = x; - this.y = y; - this.width = width; - this.height = height; - } - - @Override - public void extractRenderState(GuiGraphicsExtractor extractor) { - int ix = (int) x, iy = (int) y, iw = (int) width, ih = (int) height; - - // 背景 - extractor.fill(ix, iy, ix + iw, iy + ih, BG_COLOR); - - // 显示文字 - var font = Minecraft.getInstance().font; - int textX = ix + (int) PADDING_H; - int textY = (int) (y + (height + font.lineHeight) / 2f - font.lineHeight); - extractor.text(font, buffer.toString(), textX, textY, TEXT_COLOR); - - // 光标(聚焦时绘制) - if (focused) { - String textBefore = buffer.substring(0, cursorPos); - int cursorX = (int) (x + PADDING_H + font.width(textBefore)); - extractor.fill(cursorX, iy + 2, cursorX + 1, iy + ih - 2, CURSOR_COLOR); - } - } - - // ── 键盘输入处理 ── - - @Override - public boolean onKeyPressed(KeyEvent event) { - int key = event.key(); - int mods = event.modifiers(); - - // 控制键 - if (key == 259) { // 退格 - if (cursorPos > 0) { buffer.deleteCharAt(cursorPos - 1); cursorPos--; fireChange(); } - return true; - } - if (key == 261) { // Delete - if (cursorPos < buffer.length()) { buffer.deleteCharAt(cursorPos); fireChange(); } - return true; - } - if (key == 263) { if (cursorPos > 0) cursorPos--; return true; } // ← - if (key == 262) { if (cursorPos < buffer.length()) cursorPos++; return true; } // → - if (key == 268) { cursorPos = 0; return true; } // Home - if (key == 269) { cursorPos = buffer.length(); return true; } // End - - // 可打印字符 (GLFW key codes: 32=Space, 39=',', 44='.', 45='-', 47='/', 48-57=0-9, 59=';', 61='=', 65-90=A-Z, 91-93=[\]) - char c = glfwKeyToChar(key, (mods & 0x1) != 0); - if (c != 0) { - buffer.insert(cursorPos, c); - cursorPos++; - fireChange(); - return true; - } - return false; - } - - /** GLFW 按键码 → 字符(英文键盘布局)。 */ - private static char glfwKeyToChar(int key, boolean shift) { - if (key >= 65 && key <= 90) return (char) (shift ? key : key + 32); // A-Z / a-z - if (key >= 48 && key <= 57) return (char) (shift ? ")!@#$%^&*(".charAt(key - 48) : key); // 0-9 - return (char) switch (key) { - case 32 -> ' '; case 39 -> '\''; case 44 -> ','; case 45 -> '-'; - case 46 -> '.'; case 47 -> '/'; case 59 -> ';'; case 61 -> '='; - case 91 -> '['; case 92 -> '\\'; case 93 -> ']'; case 96 -> '`'; - default -> 0; - }; - } - - private void fireChange() { - if (onChange != null) onChange.accept(buffer.toString()); - } - - /** 命中测试包围盒。 */ - public LayoutRect hitRect() { - return LayoutRect.of(x, y, width, height); - } -} diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextInputComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextInputComponent.java new file mode 100644 index 00000000..855bc54b --- /dev/null +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextInputComponent.java @@ -0,0 +1,145 @@ +package dev.anvilcraft.lib.v2.ui.component; + +import dev.anvilcraft.lib.v2.ui.Constraints; +import dev.anvilcraft.lib.v2.ui.LayoutRect; +import dev.anvilcraft.lib.v2.ui.MeasuredSize; +import dev.anvilcraft.lib.v2.ui.Modifier; +import dev.anvilcraft.lib.v2.ui.UIComponent; +import dev.anvilcraft.lib.v2.ui.input.KeyInputHandler; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.input.CharacterEvent; +import net.minecraft.client.input.KeyEvent; +import net.minecraft.network.chat.Style; +import net.minecraft.util.StringUtil; +import org.jspecify.annotations.Nullable; + +import java.util.Collections; +import java.util.List; +import java.util.function.Consumer; + +/** + * 单行文本输入框。参照原版 {@code EditBox} 实现。 + *

+ * placeholder 作为占位提示(灰色),开始输入后直接替换为输入内容。 + * 字符输入通过 {@link CharacterEvent} 处理,支持所有语言和输入法。 + */ +public class TextInputComponent implements UIComponent, KeyInputHandler { + + private static final int BG_COLOR = 0xFF202020; + private static final int TEXT_COLOR = 0xFFFFFFFF; + private static final int PLACEHOLDER_COLOR = 0xFF555555; + private static final int CURSOR_COLOR = 0xFFFFFFFF; + private static final float PADDING_H = 4; + private static final float PADDING_V = 4; + private static final float WIDTH = 160; + + private Modifier modifier; + private String value = ""; + private final String placeholder; + private boolean focused; + private Consumer onChange; + private int displayPos; + private int cursorPos; + + private float x, y, width, height; + + public TextInputComponent(Modifier modifier, String placeholder, Consumer onChange) { + this.modifier = modifier; + this.placeholder = placeholder != null ? placeholder : ""; + this.onChange = onChange; + } + + public TextInputComponent modifier(Modifier m) { this.modifier = m; return this; } + public String getValue() { return value; } + public void setValue(String value) { this.value = value != null ? value : ""; this.cursorPos = this.value.length(); } + public int getCursorPos() { return cursorPos; } + public void setCursorPos(int pos) { this.cursorPos = Math.clamp(pos, 0, value.length()); } + public void setFocused(boolean focused) { this.focused = focused; } + public boolean isFocused() { return focused; } + + @Override public Modifier modifier() { return modifier; } + @Override public List children() { return Collections.emptyList(); } + + @Override + public MeasuredSize measure(Constraints constraints) { + var font = Minecraft.getInstance().font; + return MeasuredSize.of( + constraints.constrainWidth(WIDTH), + constraints.constrainHeight(font.lineHeight + PADDING_V * 2) + ); + } + + @Override + public void layout(float x, float y, float width, float height) { + this.x = x; + this.y = y; + this.width = width; + this.height = height; + } + + @Override + public void extractRenderState(GuiGraphicsExtractor extractor) { + int ix = (int) x, iy = (int) y, iw = (int) width, ih = (int) height; + extractor.fill(ix, iy, ix + iw, iy + ih, BG_COLOR); + + var font = Minecraft.getInstance().font; + int textX = ix + (int) PADDING_H; + int textY = (int) (y + (height + font.lineHeight) / 2f - font.lineHeight); + boolean hasText = !value.isEmpty(); + + String display = hasText ? value : placeholder; + int color = hasText ? TEXT_COLOR : PLACEHOLDER_COLOR; + extractor.text(font, display, textX, textY, color); + + if (focused && hasText) { + String before = value.substring(0, Math.min(cursorPos, value.length())); + int cursorX = (int) (x + PADDING_H + font.width(before)); + extractor.fill(cursorX, iy + 2, cursorX + 1, iy + ih - 2, CURSOR_COLOR); + } + } + + // ── 键盘输入 ── + + @Override + public boolean onKeyPressed(KeyEvent event) { + int key = event.key(); + if (key == 259) { // 退格 + if (cursorPos > 0) { value = new StringBuilder(value).deleteCharAt(cursorPos - 1).toString(); cursorPos--; fireChange(); } + return true; + } + if (key == 261) { // Delete + if (cursorPos < value.length()) { value = new StringBuilder(value).deleteCharAt(cursorPos).toString(); fireChange(); } + return true; + } + if (key == 263) { if (cursorPos > 0) cursorPos--; return true; } // ← + if (key == 262) { if (cursorPos < value.length()) cursorPos++; return true; } // → + if (key == 268) { cursorPos = 0; return true; } // Home + if (key == 269) { cursorPos = value.length(); return true; } // End + return false; + } + + /** 字符输入——支持所有语言、输入法、小键盘。参照原版 {@code EditBox.charTyped}。 */ + @Override + public boolean onCharTyped(CharacterEvent event) { + if (!event.isAllowedChatCharacter()) return false; + String text = StringUtil.filterText(event.codepointAsString()); + if (text.isEmpty()) return false; + insertText(text); + return true; + } + + private void insertText(String text) { + value = new StringBuilder(value).insert(cursorPos, text).toString(); + cursorPos += text.length(); + fireChange(); + } + + private void fireChange() { + if (onChange != null) onChange.accept(value); + } + + public LayoutRect hitRect() { + return LayoutRect.of(x, y, width, height); + } +} diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/input/KeyInputHandler.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/input/KeyInputHandler.java index ef25fe61..41b0d3a4 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/input/KeyInputHandler.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/input/KeyInputHandler.java @@ -1,5 +1,6 @@ package dev.anvilcraft.lib.v2.ui.input; +import net.minecraft.client.input.CharacterEvent; import net.minecraft.client.input.KeyEvent; /** @@ -8,6 +9,12 @@ */ public interface KeyInputHandler { - /** 按键按下时调用。返回 true 表示已处理。 */ + /** 控制键按下时调用。返回 true 表示已处理。 */ boolean onKeyPressed(KeyEvent event); + + /** + * 字符输入时调用(支持所有语言、输入法、小键盘)。 + * 返回 true 表示已处理。 + */ + boolean onCharTyped(CharacterEvent event); } From a1371f5407414b73ce983381c0f8e20d4d07c40f Mon Sep 17 00:00:00 2001 From: Gugle Date: Tue, 19 May 2026 00:41:29 +0800 Subject: [PATCH 31/67] feat(ui): adjust ScrollableComponent layout to account for scrollbar width --- .../anvilcraft/lib/v2/ui/component/ScrollableComponent.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ScrollableComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ScrollableComponent.java index eb6008f2..8affdb5f 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ScrollableComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ScrollableComponent.java @@ -53,7 +53,7 @@ public MeasuredSize measure(Constraints constraints) { float maxW = 0; float totalH = 0; List sizes = new ArrayList<>(children.size()); - Constraints childC = new Constraints(0, constraints.maxWidth(), 0, Float.MAX_VALUE); + Constraints childC = new Constraints(0, constraints.maxWidth() - SCROLLBAR_W - 1, 0, Float.MAX_VALUE); for (UIComponent child : children) { MeasuredSize s = child.measure(childC); @@ -81,10 +81,11 @@ public void layout(float x, float y, float width, float height) { this.height = height; float currentY = y + scrollY; + float childW = width - SCROLLBAR_W - 1; for (int i = 0; i < children.size(); i++) { UIComponent child = children.get(i); MeasuredSize size = childSizes.get(i); - child.layout(x, currentY, width, size.height()); + child.layout(x, currentY, childW, size.height()); currentY += size.height(); } } From 4376111627536ca9e7aba362b4d0aa533bbbf551 Mon Sep 17 00:00:00 2001 From: Gugle Date: Tue, 19 May 2026 00:47:45 +0800 Subject: [PATCH 32/67] feat(ui): enhance ScrollableComponent with scrollbar drag functionality --- .../lib/v2/ui/DeclarativeScreen.java | 24 ++++++++- .../v2/ui/component/ScrollableComponent.java | 51 +++++++++++++++++-- 2 files changed, 69 insertions(+), 6 deletions(-) diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java index 6a8dc541..27d11dfd 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java @@ -113,6 +113,14 @@ public boolean mouseDragged(MouseButtonEvent event, double deltaX, double deltaY return super.mouseDragged(event, deltaX, deltaY); } + @Override + public boolean mouseReleased(MouseButtonEvent event) { + for (UIComponent child : rootScope.getChildren()) { + stopDragRecursive(child); + } + return super.mouseReleased(event); + } + @Override public boolean mouseScrolled(double mouseX, double mouseY, double scrollX, double scrollY) { for (UIComponent child : rootScope.getChildren()) { @@ -175,10 +183,14 @@ private boolean hitTestClick(UIComponent component, float px, float py) { focusOwner = tf; return true; } + if (component instanceof ScrollableComponent sc && sc.isOnScrollbar(px, py)) { + sc.startScrollbarDrag(py); + return true; + } return false; } - /** 拖拽命中测试(仅 Slider 响应)。 */ + /** 拖拽命中测试(Slider + Scrollable 滚动条)。 */ private boolean hitTestDrag(UIComponent component, float px, float py) { var children = component.children(); for (int i = children.size() - 1; i >= 0; i--) { @@ -188,9 +200,19 @@ private boolean hitTestDrag(UIComponent component, float px, float py) { sl.setValueFromMouse(px); return true; } + if (component instanceof ScrollableComponent sc && sc.isScrollbarDragging()) { + sc.onScrollbarDrag(py); + return true; + } return false; } + /** 递归停止拖拽状态。 */ + private void stopDragRecursive(UIComponent component) { + if (component instanceof ScrollableComponent sc) sc.stopScrollbarDrag(); + for (UIComponent child : component.children()) stopDragRecursive(child); + } + /** 滚轮命中测试(仅 ScrollableComponent 响应)。 */ private boolean hitTestScroll(UIComponent component, float px, float py, float amount) { var children = component.children(); diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ScrollableComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ScrollableComponent.java index 8affdb5f..456ae01c 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ScrollableComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ScrollableComponent.java @@ -29,6 +29,8 @@ public class ScrollableComponent implements UIComponent { private float scrollY; private float contentHeight; + private boolean scrollbarDragging; + private float dragAnchorY; private float x, y, width, height; @@ -105,18 +107,16 @@ public void extractRenderState(GuiGraphicsExtractor extractor) { // 滚动条 if (contentHeight > height) { - float barH = Math.max(16, height * height / contentHeight); - float maxScroll = contentHeight - height; - float barY = y + (-scrollY / maxScroll) * (height - barH); + float bh = barH(); + float by = barY(); int bx = (int) (x + width - SCROLLBAR_W - 1); extractor.fill(bx, iy, bx + SCROLLBAR_W, iy + ih, SCROLLBAR_BG); - extractor.fill(bx, (int) barY, bx + SCROLLBAR_W, (int) (barY + barH), SCROLLBAR_COLOR); + extractor.fill(bx, (int) by, bx + SCROLLBAR_W, (int) (by + bh), SCROLLBAR_COLOR); } } // ── 滚动 ── - /** 处理滚轮事件。返回 true 表示已消费。 */ public boolean onScroll(float amount) { if (contentHeight <= height) return false; float maxScroll = contentHeight - height; @@ -124,6 +124,47 @@ public boolean onScroll(float amount) { return true; } + /** 鼠标是否在滚动条滑块上。 */ + public boolean isOnScrollbar(float mx, float my) { + if (contentHeight <= height) return false; + float bh = barH(); + float by = barY(); + int bx = (int) (x + width - SCROLLBAR_W - 1); + return mx >= bx && mx < bx + SCROLLBAR_W && my >= by && my < by + bh; + } + + /** 开始拖拽滚动条。 */ + public void startScrollbarDrag(float my) { + scrollbarDragging = true; + dragAnchorY = my - barY(); + } + + /** 拖拽滚动条时更新位置。 */ + public void onScrollbarDrag(float my) { + if (!scrollbarDragging) return; + float bh = barH(); + float maxScroll = contentHeight - height; + float newBarY = my - dragAnchorY; + float ratio = Mth.clamp(newBarY / (height - bh), 0f, 1f); + scrollY = -(ratio * maxScroll); + } + + /** 停止拖拽。 */ + public void stopScrollbarDrag() { + scrollbarDragging = false; + } + + public boolean isScrollbarDragging() { return scrollbarDragging; } + + private float barH() { + return Math.max(16, height * height / contentHeight); + } + + private float barY() { + float maxScroll = contentHeight - height; + return y + (-scrollY / maxScroll) * (height - barH()); + } + public float getScrollY() { return scrollY; } public void setScrollY(float scrollY) { this.scrollY = scrollY; } From 5b61affde5d75c090be42aefec109fb73f6d4bea Mon Sep 17 00:00:00 2001 From: Gugle Date: Tue, 19 May 2026 00:53:19 +0800 Subject: [PATCH 33/67] feat(ui): update TODO.md for improved clarity and localization --- module.ui/TODO.md | 194 +++++++++++++++++++++------------------------- 1 file changed, 89 insertions(+), 105 deletions(-) diff --git a/module.ui/TODO.md b/module.ui/TODO.md index cb1c2cbd..5c431a27 100644 --- a/module.ui/TODO.md +++ b/module.ui/TODO.md @@ -1,130 +1,114 @@ -# module.ui TODO +# module.ui — 声明式 UI 系统 -受 ArkUI 启发的声明式 UI 系统。纯 Java API,Consumer/Runnable 作为尾参,Kotlin SAM 转换自动获得尾随 Lambda DSL。 -状态驱动,与 Minecraft GuiGraphicsExtractor 渲染管线集成。 +参考 ArkUI 声明式组件体系。纯 Java API,`Consumer`/`Runnable` 尾参,Kotlin SAM 转 DSL。 --- -## Phase 1: 构建系统 + 核心框架 +## 容器 / 布局组件 + +| 组件 | 状态 | 用途 | +|---------------------------|-------|----------------------------------| +| **Column** | ✅ 已实现 | 纵向线性布局,主轴=垂直,交叉轴=水平 | +| **Row** | ✅ 已实现 | 横向线性布局,主轴=水平,交叉轴=垂直 | +| **Box** (≡ Stack) | ✅ 已实现 | 层叠布局,子组件按声明顺序从底到顶重叠 | +| **Grid** | ✅ 已实现 | 网格布局,指定列数,自动换行 | +| **Scrollable** (≡ Scroll) | ✅ 已实现 | 可滚动容器,maxHeight 超限时裁剪 + 滚动条 + 拖拽 | +| **Flex** | ❎ 未实现 | 弹性布局,子组件按权重分配空间 | +| **List** / **LazyColumn** | ❎ 未实现 | 虚拟化长列表,仅渲染可见区域 | +| **Tabs** | ❎ 未实现 | 标签页切换容器 | +| **Swiper** | ❎ 未实现 | 轮播/滑动切换容器 | +| **SideBarContainer** | ❎ 未实现 | 侧边栏抽屉容器 | +| **Panel** | ❎ 未实现 | 可滑动面板 | +| **Refresh** | ❎ 未实现 | 下拉刷新容器 | +| **RelativeContainer** | ❎ 未实现 | 相对定位布局 | -- [x] `module.ui/build.gradle` — 添加 `implementation project(':anvillib-rendering-neoforge-26.1')` -- [x] `Constraints` — min/max width/height 约束 -- [x] `Modifier` — 链式 API 接口(`then` / `foldIn` / `foldOut`) -- [x] `ModifierElement` — 单个修饰符节点接口 -- [x] `UIComponent` — 核心接口:`measure(Constraints): MeasuredSize` / `layout(...)` / `extractRenderState(GuiGraphicsExtractor)` -- [x] `Composition` — slot table + `emit()` / `recompose()` / `invalidate()` - -## Phase 2: 状态管理 - -- [x] `MutableState` — 可观察状态:getter 记录 reader slot,setter 精确 markDirty -- [x] `remember { }` — 按 slot 位置持久化,recompose 时回读同一对象 -- [x] 脏标记传播:只重执行 dirty group,干净子树跳过 -- [x] `DeclarativeTestScreen` — 端到端验证 Screen(`/anvillib_test_client declarative`) - -## Phase 3: 布局容器 - -- [x] `Column` + `ColumnScope` — 纵向排列(重构:spacing / verticalArrangement / horizontalAlignment) -- [x] `Row` + `RowScope` — 横向排列(horizontalArrangement / verticalAlignment / spacing) -- [x] `Box` + `BoxScope` — 层叠(contentAlignment) -- [x] MeasurePolicy 内联实现(随组件复杂度提升再提取为策略对象) -- [x] `Arrangement.Vertical` / `Arrangement.Horizontal` — Top/Center/Bottom, Start/Center/End, SpaceBetween/SpaceAround/SpaceEvenly -- [x] `Alignment.Horizontal` / `Alignment.Vertical` — Start/Center/End, Top/Center/Bottom - -## Phase 4: 基础组件 - -- [x] `Text` — 重构:加 shadow(默认开启)、Align LEFT/CENTER/RIGHT、原版默认色 -- [x] `Button` — 重构:原版配色(0xFF404040 / hover 0xFF606060)、shadow 文字、hover 状态预留 -- [x] `Spacer` — 新建:固定尺寸空白占位 -- [x] `Image` — 新建:`blitSprite` 渲染 `Identifier` 纹理 - -## Phase 5: Modifier Elements - -- [x] `SizeElement` — `.size()` `.fillMaxWidth()` `.fillMaxHeight()` `.fillMaxSize()` -- [x] `PaddingElement` — `.padding(all)` `.padding(horizontal, vertical)` -- [x] `BackgroundElement` — `.background(color)` → SdfGraphics.box(支持 round) -- [x] `BorderElement` — `.border(width, color)` → SdfGraphics.stroke(支持 round) -- [x] 所有组件增加 `.modifier(Modifier)` setter(`modifier` 字段改为可变) -- [x] `ClickElement` — 推迟到 Phase 6 与输入路由一起实现 - -## Phase 6: 屏幕集成 - -- [x] `DeclarativeScreen` — Screen 子类,每帧 dirty check → recompose → measure → layout → render -- [x] `extractRenderState()` — 整合 Composition.renderFrame + hover 更新 -- [x] 鼠标事件 — `mouseClicked` 命中测试 + 点击分发;hover 遍历更新 ButtonComponent -- [x] 键盘事件 — `keyPressed` (ESC 关闭) -- [x] 滚轮事件 — `mouseScrolled` 预留接口 - -## Phase 7: 输入组件 - -- [x] `TextField` — 单行文本输入(Backspace/Delete/光标移动/Home/End,GLFW 按键→字符映射) -- [x] `Checkbox` — 16x16 方框 + 选中对勾,点击切换 -- [x] `Slider` — 轨道 + 滑块,点击/拖拽设值 -- [x] `KeyInputHandler` 接口 + DeclarativeScreen 焦点系统(focusOwner) -- [x] 键盘路由:keyPressed → 焦点组件;鼠标:drag → Slider +--- -## Phase 8: 高级特性 +## 基础组件 + +| 组件 | 状态 | 用途 | +|-------------------------|-------|----------------------------------------| +| **Text** | ✅ 已实现 | 单行文字,支持颜色/阴影/对齐(LEFT/CENTER/RIGHT) | +| **Button** | ✅ 已实现 | 可点击按钮,原版配色,hover 状态,点击音效 | +| **Image** | ✅ 已实现 | 纹理精灵渲染(`blitSprite`) | +| **Spacer** (≡ Blank) | ✅ 已实现 | 固定尺寸空白占位 | +| **Checkbox** | ✅ 已实现 | 复选框,16×16 深灰外框 + 选中时白色内填充 | +| **Slider** | ✅ 已实现 | 水平滑块,点击/拖拽设值,`Consumer` 回调 | +| **TextInput** | ✅ 已实现 | 单行输入框,placeholder 占位,`charTyped` 多语言输入 | +| **Divider** | ❎ 未实现 | 分割线(水平/垂直) | +| **Toggle** | ❎ 未实现 | 开关切换(不同于 Checkbox 的方块填充风格) | +| **Radio** | ❎ 未实现 | 单选按钮 | +| **Progress** | ❎ 未实现 | 进度条(线性/圆形) | +| **LoadingProgress** | ❎ 未实现 | 加载动画 | +| **TextArea** | ❎ 未实现 | 多行文本输入框 | +| **Search** | ❎ 未实现 | 搜索输入框 | +| **Select** | ❎ 未实现 | 下拉选择器 | +| **Menu** / **MenuItem** | ❎ 未实现 | 右键菜单 / 弹出菜单 | +| **Hyperlink** | ❎ 未实现 | 超链接文字 | +| **Marquee** | ❎ 未实现 | 跑马灯滚动文字 | +| **Rating** | ❎ 未实现 | 星级评分 | +| **Badge** | ❎ 未实现 | 角标/红点提示 | +| **QRCode** | ❎ 未实现 | 二维码显示 | -- [x] `Grid` — 网格布局(columns × auto-rows,hSpacing/vSpacing) -- [x] `ForEach` — 循环渲染工具(`ForEach.of(scope, items, (s, item) -> ...)`) -- [x] `Animatable` — tick 驱动动画值(ease-in-out,`Composition.watch()` 自动驱动) -- [ ] 条件渲染 — `if` 天然工作于 content lambda 重执行时;key 稳定需要后续优化 -- [x] `Scrollable` — 可滚动容器(maxHeight + scissor 裁剪 + 滚动条) -- [ ] `LazyColumn` — 虚拟化长列表,延后 +--- -## Phase 9: 测试 +## 选择器组件 -- [ ] 布局算法单元测试(Column/Row/Box measure + layout) -- [ ] Slot diffing 单元测试(recompose 后 slot table 正确性) -- [ ] State 传播单元测试(精确 markDirty 范围) -- [ ] `module.test` 中创建示例 `DeclarativeScreen` 验证端到端 +| 组件 | 状态 | 用途 | +|----------------|-------|---------| +| **TextPicker** | ❎ 未实现 | 文字滚轮选择器 | +| **DatePicker** | ❎ 未实现 | 日期选择器 | +| **TimePicker** | ❎ 未实现 | 时间选择器 | --- -## 设计笔记:组件扩展方式 +## 交互 / 手势 -组件 = 工厂函数 + 实现类,两者并存: +| 组件 | 状态 | 用途 | +|-------------------------|-------|----------------------------------------------| +| **onClick** (≡ Gesture) | ✅ 已实现 | 点击事件,命中测试 + Button/Checkbox/Slider/TextInput | +| **onHover** | ✅ 已实现 | hover 状态更新(`updateHoverRecursive`) | +| **onScroll** | ✅ 已实现 | 滚轮事件 → Scrollable 路由 | +| **onDrag** | ✅ 已实现 | 拖拽事件 → Slider + Scrollable 滚动条 | +| **onKey** | ✅ 已实现 | 键盘事件 → `KeyInputHandler` + `charTyped` | -- **工厂函数** — DSL 入口。创建实例 → 注册到父容器 + Slot Table → 返回实例(链式调用) -- **实现类** — 可继承、可覆盖 `measure()` / `layout()` / `extractRenderState()` +--- -三种扩展方式: +## 状态管理 -### 1. 组合(90% 场景) +| 组件 | 状态 | 用途 | +|-------------------------|-------|---------------------------------------------------| +| **Ref\** (≡ @State) | ✅ 已实现 | 可观察状态持有者,读取追踪 slot,写入精确 markDirty | +| **remember** | ✅ 已实现 | 跨 recompose 持久化值,按调用位置缓存 | +| **ref()** | ✅ 已实现 | `comp.ref(0)` 等价于 `remember(() -> new Ref<>(0))` | +| **Animatable** | ✅ 已实现 | tick 驱动动画值,ease-in-out,`Composition.watch()` 自动推进 | -已有组件拼出新组件,不改内部实现。 +--- -```java -public static TextComponent FancyLabel(ColumnScope scope, String text) { - return scope.Text(text).fontSize(24).color(0xFFAAAAFF); -} -``` +## 修饰符系统 -### 2. 继承实现类(需要新行为时) +| 组件 | 状态 | 用途 | +|--------------------------------------|-------|--------------------------------------| +| **Modifier** | ✅ 已实现 | 链式修饰符 API(`then`/`foldIn`/`foldOut`) | +| `.size()` / `.width()` / `.height()` | ✅ 已实现 | 尺寸约束 | +| `.fillMaxWidth()` / `.fillMaxSize()` | ✅ 已实现 | 填充父容器 | +| `.padding()` | ✅ 已实现 | 内边距 | +| `.background()` | ✅ 已实现 | SDF 圆角填充背景 | +| `.border()` | ✅ 已实现 | SDF 描边边框 | -extends 现有组件或 implements `UIComponent`,覆盖核心方法,再提供配套工厂函数。 +--- -```java -public class RainbowText extends TextComponent { - @Override - public void extractRenderState(GuiGraphicsExtractor e) { - this.color = Color.HSBtoRGB(...); - super.extractRenderState(e); - } -} -``` +## 引擎核心 -### 3. Modifier 扩展(可复用样式/行为) +| 组件 | 状态 | 用途 | +|-----------------------|-------|----------------------------------------------------------------| +| **Composition** | ✅ 已实现 | Slot table 引擎:emit / recompose / invalidate / copyRuntimeState | +| **DeclarativeScreen** | ✅ 已实现 | Screen 宿主:dirty check → recompose → measure → layout → render | -封装常用样式为 Modifier 工厂方法,与具体组件解耦。 +--- -```java -public static Modifier cardStyle(Modifier m) { - return m.background(0xFF333333).roundedCorner(8).padding(12, 8); -} -``` +## 统计 -| 方式 | 适用 | 耦合 | -|----------|--------------------------|----------| -| 组合 | 拼装现有组件 | 无 | -| 继承 | 全新 measure/layout/render | 与父类耦合 | -| Modifier | 可复用样式/行为 | 无,任何组件通用 | +- 总计参考组件:**52** +- 已实现:**25**(含引擎核心 + 状态管理 + 修饰符) +- 未实现:**27** From da87c5d7cbc34cafb13ee20e9ae95277a63fcbac Mon Sep 17 00:00:00 2001 From: Gugle Date: Tue, 19 May 2026 00:55:10 +0800 Subject: [PATCH 34/67] feat(ui): improve SliderComponent value calculation to prevent division by zero --- .../dev/anvilcraft/lib/v2/ui/component/SliderComponent.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/SliderComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/SliderComponent.java index fa06fbea..17d9bc29 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/SliderComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/SliderComponent.java @@ -82,7 +82,7 @@ public void extractRenderState(GuiGraphicsExtractor extractor) { /** 根据鼠标 X 坐标更新值。 */ public void setValueFromMouse(float mouseX) { - float ratio = Mth.clamp((mouseX - x) / width, 0f, 1f); + float ratio = Mth.clamp((mouseX - x) / Math.max(width - 1, 1), 0f, 1f); float newValue = min + ratio * (max - min); if (newValue != value) { value = newValue; From 4be4daf08a7b5ec85b56145a8ac96199c7c6a705 Mon Sep 17 00:00:00 2001 From: Gugle Date: Tue, 19 May 2026 01:03:51 +0800 Subject: [PATCH 35/67] feat(ui): simplify component constructors by removing onClick and onChange parameters --- .../client/screen/DeclarativeTestScreen.java | 26 +++++++++---------- .../dev/anvilcraft/lib/v2/ui/UIScope.java | 21 ++++++--------- .../lib/v2/ui/component/ButtonComponent.java | 3 +-- .../v2/ui/component/CheckboxComponent.java | 4 +-- .../lib/v2/ui/component/SliderComponent.java | 4 +-- .../v2/ui/component/TextInputComponent.java | 4 +-- 6 files changed, 27 insertions(+), 35 deletions(-) diff --git a/module.test/src/main/java/dev/anvilcraft/lib/v2/test/client/screen/DeclarativeTestScreen.java b/module.test/src/main/java/dev/anvilcraft/lib/v2/test/client/screen/DeclarativeTestScreen.java index fac66f49..290a0688 100644 --- a/module.test/src/main/java/dev/anvilcraft/lib/v2/test/client/screen/DeclarativeTestScreen.java +++ b/module.test/src/main/java/dev/anvilcraft/lib/v2/test/client/screen/DeclarativeTestScreen.java @@ -48,10 +48,10 @@ protected void content(UIScope scope) { // ── 2. Button + 状态 ── col.Text("2) Button + state (Counter):"); col.Row(row -> { - row.Button("-", () -> counter.setValue(counter.getValue() - 1)); + row.Button("-").onClick(() -> counter.setValue(counter.getValue() - 1)); row.Text(" " + counter.getValue() + " ").align(TextComponent.Align.CENTER); - row.Button("+", () -> counter.setValue(counter.getValue() + 1)); - row.Button("Reset", () -> counter.setValue(0)); + row.Button("+").onClick(() -> counter.setValue(counter.getValue() + 1)); + row.Button("Reset").onClick(() -> counter.setValue(0)); }).spacing(4); col.Spacer(0, 4); @@ -70,10 +70,7 @@ protected void content(UIScope scope) { col.Grid( 3, grid -> { for (int i = 0; i < 6; i++) { - grid.Button( - "G" + i, () -> { - } - ); + grid.Button("G" + i); } } ).spacing(2, 2); @@ -83,7 +80,8 @@ protected void content(UIScope scope) { // ── 5. Checkbox ── col.Text("5) Checkbox:"); col.Row(row -> { - row.Checkbox("Enable feature", checked.getValue(), () -> checked.setValue(!checked.getValue())); + row.Checkbox("Enable feature", checked.getValue()) + .onToggle(() -> checked.setValue(!checked.getValue())); row.Text(" Enabled: " + checked.getValue()); }).spacing(4); @@ -93,7 +91,8 @@ protected void content(UIScope scope) { Ref sliderVal = comp.remember(() -> new Ref<>(50f)); col.Text("6) Slider:"); col.Row(row -> { - row.Slider(sliderVal.getValue(), 0, 100, 100, sliderVal::setValue); + row.Slider(sliderVal.getValue(), 0, 100, 100) + .onChange(sliderVal::setValue); row.Text(" " + sliderVal.getValue().intValue() + "%"); }).spacing(4); @@ -102,7 +101,8 @@ protected void content(UIScope scope) { // ── 7. TextInput ── col.Text("7) TextInput:"); col.Row(row -> { - row.TextInput("Enter text...", text::setValue); + row.TextInput("Enter text...") + .onChange(text::setValue); row.Text(" Value: '" + text.getValue() + "'"); }).spacing(4); @@ -117,10 +117,8 @@ protected void content(UIScope scope) { // ── 9. Modifier 样式 ── col.Text("9) Modifiers:"); col.Row(row -> { - row.Button( - "Styled", () -> { - } - ).modifier(Modifier.NONE.background(0xFF884444).border(1, 0xFFFF8888)); + row.Button("Styled") + .modifier(Modifier.NONE.background(0xFF884444).border(1, 0xFFFF8888)); row.Text(" "); row.Text("Padded").modifier(Modifier.NONE.padding(8).background(0xFF444488)); }).spacing(4); diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIScope.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIScope.java index d7b54981..123032ca 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIScope.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIScope.java @@ -97,10 +97,9 @@ public ImageComponent Image(Identifier sprite, float width, float height) { * 创建复选框。 * @param label 标签文字 * @param checked 初始选中状态 - * @param onToggle 切换时的回调 */ - public CheckboxComponent Checkbox(String label, boolean checked, Runnable onToggle) { - CheckboxComponent c = new CheckboxComponent(Modifier.NONE, label, checked, onToggle); + public CheckboxComponent Checkbox(String label, boolean checked) { + CheckboxComponent c = new CheckboxComponent(Modifier.NONE, label, checked); addChild(c); Composition.current().emit(c); return c; @@ -112,11 +111,9 @@ public CheckboxComponent Checkbox(String label, boolean checked, Runnable onTogg * @param min 最小值 * @param max 最大值 * @param width 轨道宽度(像素) - * @param onChange 值变化回调,接收新值 */ - public SliderComponent Slider(float value, float min, float max, float width, - Consumer onChange) { - SliderComponent c = new SliderComponent(Modifier.NONE, value, min, max, width, onChange); + public SliderComponent Slider(float value, float min, float max, float width) { + SliderComponent c = new SliderComponent(Modifier.NONE, value, min, max, width); addChild(c); Composition.current().emit(c); return c; @@ -125,10 +122,9 @@ public SliderComponent Slider(float value, float min, float max, float width, /** * 创建单行文本输入框。 * @param placeholder 占位提示文字(灰色,仅在无输入时显示) - * @param onChange 文字变化回调,接收当前完整文本 */ - public TextInputComponent TextInput(String placeholder, Consumer onChange) { - TextInputComponent c = new TextInputComponent(Modifier.NONE, placeholder, onChange); + public TextInputComponent TextInput(String placeholder) { + TextInputComponent c = new TextInputComponent(Modifier.NONE, placeholder); addChild(c); Composition.current().emit(c); return c; @@ -137,10 +133,9 @@ public TextInputComponent TextInput(String placeholder, Consumer onChang /** * 创建可点击按钮。 * @param label 按钮文字 - * @param onClick 点击回调(可为 null) */ - public ButtonComponent Button(String label, @Nullable Runnable onClick) { - ButtonComponent c = new ButtonComponent(Modifier.NONE, label, onClick); + public ButtonComponent Button(String label) { + ButtonComponent c = new ButtonComponent(Modifier.NONE, label); addChild(c); Composition.current().emit(c); return c; diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ButtonComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ButtonComponent.java index 51b9fed9..587f065a 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ButtonComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ButtonComponent.java @@ -33,10 +33,9 @@ public class ButtonComponent implements UIComponent { private float x, y, width, height; - public ButtonComponent(Modifier modifier, String label, @Nullable Runnable onClick) { + public ButtonComponent(Modifier modifier, String label) { this.modifier = modifier; this.label = label; - this.onClick = onClick; } public ButtonComponent label(String label) { this.label = label; return this; } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/CheckboxComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/CheckboxComponent.java index 61bab4b1..b661bc4a 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/CheckboxComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/CheckboxComponent.java @@ -28,14 +28,14 @@ public class CheckboxComponent implements UIComponent { private float x, y, width, height; - public CheckboxComponent(Modifier modifier, String label, boolean checked, Runnable onToggle) { + public CheckboxComponent(Modifier modifier, String label, boolean checked) { this.modifier = modifier; this.label = label; this.checked = checked; - this.onToggle = onToggle; } public CheckboxComponent modifier(Modifier m) { this.modifier = m; return this; } + public CheckboxComponent onToggle(Runnable onToggle) { this.onToggle = onToggle; return this; } @Override public Modifier modifier() { return modifier; } @Override public List children() { return Collections.emptyList(); } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/SliderComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/SliderComponent.java index 17d9bc29..74d50d9e 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/SliderComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/SliderComponent.java @@ -32,8 +32,7 @@ public class SliderComponent implements UIComponent { private float x, y, width, height; - public SliderComponent(Modifier modifier, float value, float min, float max, float trackWidth, - Consumer onChange) { + public SliderComponent(Modifier modifier, float value, float min, float max, float trackWidth) { this.modifier = modifier; this.value = Mth.clamp(value, min, max); this.min = min; @@ -43,6 +42,7 @@ public SliderComponent(Modifier modifier, float value, float min, float max, flo } public SliderComponent modifier(Modifier m) { this.modifier = m; return this; } + public SliderComponent onChange(Consumer onChange) { this.onChange = onChange; return this; } public float value() { return value; } @Override public Modifier modifier() { return modifier; } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextInputComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextInputComponent.java index 855bc54b..5362c4ba 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextInputComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextInputComponent.java @@ -44,13 +44,13 @@ public class TextInputComponent implements UIComponent, KeyInputHandler { private float x, y, width, height; - public TextInputComponent(Modifier modifier, String placeholder, Consumer onChange) { + public TextInputComponent(Modifier modifier, String placeholder) { this.modifier = modifier; this.placeholder = placeholder != null ? placeholder : ""; - this.onChange = onChange; } public TextInputComponent modifier(Modifier m) { this.modifier = m; return this; } + public TextInputComponent onChange(Consumer onChange) { this.onChange = onChange; return this; } public String getValue() { return value; } public void setValue(String value) { this.value = value != null ? value : ""; this.cursorPos = this.value.length(); } public int getCursorPos() { return cursorPos; } From 73d41de8c51b73cd17c0d4bbc558a81e2b6a5293 Mon Sep 17 00:00:00 2001 From: Gugle Date: Tue, 19 May 2026 01:05:00 +0800 Subject: [PATCH 36/67] feat(ui): simplify slider value management by using ref() instead of remember() --- .../lib/v2/test/client/screen/DeclarativeTestScreen.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/module.test/src/main/java/dev/anvilcraft/lib/v2/test/client/screen/DeclarativeTestScreen.java b/module.test/src/main/java/dev/anvilcraft/lib/v2/test/client/screen/DeclarativeTestScreen.java index 290a0688..54fa9abf 100644 --- a/module.test/src/main/java/dev/anvilcraft/lib/v2/test/client/screen/DeclarativeTestScreen.java +++ b/module.test/src/main/java/dev/anvilcraft/lib/v2/test/client/screen/DeclarativeTestScreen.java @@ -88,7 +88,7 @@ protected void content(UIScope scope) { col.Spacer(0, 4); // ── 6. Slider ── - Ref sliderVal = comp.remember(() -> new Ref<>(50f)); + Ref sliderVal = comp.ref(50f); col.Text("6) Slider:"); col.Row(row -> { row.Slider(sliderVal.getValue(), 0, 100, 100) From 79aea64cb92de489808455840dec21d5457179fc Mon Sep 17 00:00:00 2001 From: Gugle Date: Tue, 19 May 2026 01:13:06 +0800 Subject: [PATCH 37/67] feat(ui): implement vModel two-way binding for Checkbox, Slider, and TextInput components --- .../client/screen/DeclarativeTestScreen.java | 9 ++-- .../dev/anvilcraft/lib/v2/ui/UIScope.java | 43 ++++++++++++++++++- 2 files changed, 45 insertions(+), 7 deletions(-) diff --git a/module.test/src/main/java/dev/anvilcraft/lib/v2/test/client/screen/DeclarativeTestScreen.java b/module.test/src/main/java/dev/anvilcraft/lib/v2/test/client/screen/DeclarativeTestScreen.java index 54fa9abf..84fb5265 100644 --- a/module.test/src/main/java/dev/anvilcraft/lib/v2/test/client/screen/DeclarativeTestScreen.java +++ b/module.test/src/main/java/dev/anvilcraft/lib/v2/test/client/screen/DeclarativeTestScreen.java @@ -80,8 +80,7 @@ protected void content(UIScope scope) { // ── 5. Checkbox ── col.Text("5) Checkbox:"); col.Row(row -> { - row.Checkbox("Enable feature", checked.getValue()) - .onToggle(() -> checked.setValue(!checked.getValue())); + row.Checkbox("Enable feature", checked); row.Text(" Enabled: " + checked.getValue()); }).spacing(4); @@ -91,8 +90,7 @@ protected void content(UIScope scope) { Ref sliderVal = comp.ref(50f); col.Text("6) Slider:"); col.Row(row -> { - row.Slider(sliderVal.getValue(), 0, 100, 100) - .onChange(sliderVal::setValue); + row.Slider(0, 100, 100, sliderVal); row.Text(" " + sliderVal.getValue().intValue() + "%"); }).spacing(4); @@ -101,8 +99,7 @@ protected void content(UIScope scope) { // ── 7. TextInput ── col.Text("7) TextInput:"); col.Row(row -> { - row.TextInput("Enter text...") - .onChange(text::setValue); + row.TextInput("Enter text...", text); row.Text(" Value: '" + text.getValue() + "'"); }).spacing(4); diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIScope.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIScope.java index 123032ca..ef08467d 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIScope.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIScope.java @@ -106,7 +106,20 @@ public CheckboxComponent Checkbox(String label, boolean checked) { } /** - * 创建滑块。 + * 创建复选框(vModel 双向绑定)。 + * @param label 标签文字 + * @param vModel {@link Ref}<{@link Boolean}>,点击时自动同步值,无需手动 onToggle + */ + public CheckboxComponent Checkbox(String label, Ref vModel) { + CheckboxComponent c = new CheckboxComponent(Modifier.NONE, label, vModel.getValue()); + c.onToggle(() -> vModel.setValue(!vModel.getValue())); + addChild(c); + Composition.current().emit(c); + return c; + } + + /** + * 创建滑块(手动值)。 * @param value 初始值 * @param min 最小值 * @param max 最大值 @@ -119,6 +132,21 @@ public SliderComponent Slider(float value, float min, float max, float width) { return c; } + /** + * 创建滑块(vModel 双向绑定)。 + * @param min 最小值 + * @param max 最大值 + * @param width 轨道宽度(像素) + * @param vModel {@link Ref}<{@link Float}>,拖拽时自动同步值 + */ + public SliderComponent Slider(float min, float max, float width, Ref vModel) { + SliderComponent c = new SliderComponent(Modifier.NONE, vModel.getValue(), min, max, width); + c.onChange(vModel::setValue); + addChild(c); + Composition.current().emit(c); + return c; + } + /** * 创建单行文本输入框。 * @param placeholder 占位提示文字(灰色,仅在无输入时显示) @@ -130,6 +158,19 @@ public TextInputComponent TextInput(String placeholder) { return c; } + /** + * 创建单行文本输入框(vModel 双向绑定)。 + * @param placeholder 占位提示文字 + * @param vModel {@link Ref}<{@link String}>,输入时自动同步值 + */ + public TextInputComponent TextInput(String placeholder, Ref vModel) { + TextInputComponent c = new TextInputComponent(Modifier.NONE, placeholder); + c.onChange(vModel::setValue); + addChild(c); + Composition.current().emit(c); + return c; + } + /** * 创建可点击按钮。 * @param label 按钮文字 From e5f5a2b463f4a1193902730764dcd86dfa8b446f Mon Sep 17 00:00:00 2001 From: Gugle Date: Tue, 19 May 2026 01:29:18 +0800 Subject: [PATCH 38/67] feat(ui): add DropdownComponent for improved UI selection options --- .../client/screen/DeclarativeTestScreen.java | 16 +- .../dev/anvilcraft/lib/v2/ui/Composition.java | 5 + .../lib/v2/ui/DeclarativeScreen.java | 18 +- .../dev/anvilcraft/lib/v2/ui/UIScope.java | 13 ++ .../v2/ui/component/DropdownComponent.java | 161 ++++++++++++++++++ 5 files changed, 208 insertions(+), 5 deletions(-) create mode 100644 module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/DropdownComponent.java diff --git a/module.test/src/main/java/dev/anvilcraft/lib/v2/test/client/screen/DeclarativeTestScreen.java b/module.test/src/main/java/dev/anvilcraft/lib/v2/test/client/screen/DeclarativeTestScreen.java index 84fb5265..e2f2af6e 100644 --- a/module.test/src/main/java/dev/anvilcraft/lib/v2/test/client/screen/DeclarativeTestScreen.java +++ b/module.test/src/main/java/dev/anvilcraft/lib/v2/test/client/screen/DeclarativeTestScreen.java @@ -105,14 +105,22 @@ protected void content(UIScope scope) { col.Spacer(0, 4); - // ── 8. ForEach ── - col.Text("8) ForEach (list of 4 items):"); + // ── 8. Dropdown ── + col.Text("8) Dropdown:"); + col.Dropdown(new String[]{"Option A", "Option B", "Option C"}, 0) + .onChange(v -> {}); + col.Text(" (click to open)"); + + col.Spacer(0, 4); + + // ── 9. ForEach ── + col.Text("9) ForEach (list of 4 items):"); ForEach.of(col, List.of("Apple", "Banana", "Cherry", "Date"), (s, item) -> s.Text(" - " + item)); col.Spacer(0, 4); - // ── 9. Modifier 样式 ── - col.Text("9) Modifiers:"); + // ── 10. Modifier 样式 ── + col.Text("10) Modifiers:"); col.Row(row -> { row.Button("Styled") .modifier(Modifier.NONE.background(0xFF884444).border(1, 0xFFFF8888)); diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Composition.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Composition.java index 333858ce..3db62756 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Composition.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Composition.java @@ -1,5 +1,6 @@ package dev.anvilcraft.lib.v2.ui; +import dev.anvilcraft.lib.v2.ui.component.DropdownComponent; import dev.anvilcraft.lib.v2.ui.component.ScrollableComponent; import dev.anvilcraft.lib.v2.ui.component.TextInputComponent; import net.minecraft.client.gui.GuiGraphicsExtractor; @@ -135,6 +136,10 @@ private void copyRuntimeState(UIComponent old, UIComponent replacement) { newTi.setCursorPos(oldTi.getCursorPos()); newTi.setFocused(oldTi.isFocused()); } + if (old instanceof DropdownComponent oldDd + && replacement instanceof DropdownComponent newDd) { + newDd.setOpen(oldDd.isOpen()); + } } // ── 每帧入口 ── diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java index 27d11dfd..a70486ac 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java @@ -2,6 +2,7 @@ import dev.anvilcraft.lib.v2.ui.component.ButtonComponent; import dev.anvilcraft.lib.v2.ui.component.CheckboxComponent; +import dev.anvilcraft.lib.v2.ui.component.DropdownComponent; import dev.anvilcraft.lib.v2.ui.component.ScrollableComponent; import dev.anvilcraft.lib.v2.ui.component.SliderComponent; import dev.anvilcraft.lib.v2.ui.component.TextInputComponent; @@ -87,7 +88,11 @@ public boolean mouseClicked(MouseButtonEvent event, boolean isDoubleClick) { var mc = Minecraft.getInstance(); int mx = (int) mc.mouseHandler.getScaledXPos(mc.getWindow()); int my = (int) mc.mouseHandler.getScaledYPos(mc.getWindow()); - // 点击空白处清除焦点 + // 先关闭所有下拉菜单,命中后再由 hitTestClick 重新打开 + for (UIComponent child : rootScope.getChildren()) { + closeDropdownsRecursive(child); + } + // 清除焦点 focusOwner = null; for (UIComponent child : rootScope.getChildren()) { clearFocusRecursive(child); @@ -183,6 +188,11 @@ private boolean hitTestClick(UIComponent component, float px, float py) { focusOwner = tf; return true; } + if (component instanceof DropdownComponent dd) { + if (dd.clickPopup(px, py)) return true; + if (dd.clickTrigger(px, py)) return true; + return false; + } if (component instanceof ScrollableComponent sc && sc.isOnScrollbar(px, py)) { sc.startScrollbarDrag(py); return true; @@ -207,6 +217,12 @@ private boolean hitTestDrag(UIComponent component, float px, float py) { return false; } + /** 递归关闭所有 Dropdown。 */ + private void closeDropdownsRecursive(UIComponent component) { + if (component instanceof DropdownComponent dd) dd.setOpen(false); + for (UIComponent child : component.children()) closeDropdownsRecursive(child); + } + /** 递归停止拖拽状态。 */ private void stopDragRecursive(UIComponent component) { if (component instanceof ScrollableComponent sc) sc.stopScrollbarDrag(); diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIScope.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIScope.java index ef08467d..7bf79251 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIScope.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIScope.java @@ -8,6 +8,7 @@ import dev.anvilcraft.lib.v2.ui.component.BoxScope; import dev.anvilcraft.lib.v2.ui.component.ColumnComponent; import dev.anvilcraft.lib.v2.ui.component.ColumnScope; +import dev.anvilcraft.lib.v2.ui.component.DropdownComponent; import dev.anvilcraft.lib.v2.ui.component.GridComponent; import dev.anvilcraft.lib.v2.ui.component.GridScope; import dev.anvilcraft.lib.v2.ui.component.ImageComponent; @@ -118,6 +119,18 @@ public CheckboxComponent Checkbox(String label, Ref vModel) { return c; } + /** + * 创建下拉菜单。 + * @param options 选项列表 + * @param selectedIndex 初始选中索引 + */ + public DropdownComponent Dropdown(String[] options, int selectedIndex) { + DropdownComponent c = new DropdownComponent(Modifier.NONE, options, selectedIndex); + addChild(c); + Composition.current().emit(c); + return c; + } + /** * 创建滑块(手动值)。 * @param value 初始值 diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/DropdownComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/DropdownComponent.java new file mode 100644 index 00000000..c240585e --- /dev/null +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/DropdownComponent.java @@ -0,0 +1,161 @@ +package dev.anvilcraft.lib.v2.ui.component; + +import dev.anvilcraft.lib.v2.ui.Constraints; +import dev.anvilcraft.lib.v2.ui.LayoutRect; +import dev.anvilcraft.lib.v2.ui.MeasuredSize; +import dev.anvilcraft.lib.v2.ui.Modifier; +import dev.anvilcraft.lib.v2.ui.UIComponent; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.util.Mth; +import org.jspecify.annotations.Nullable; + +import java.util.Collections; +import java.util.List; +import java.util.function.Consumer; + +/** + * 下拉菜单。点击展开选项列表,选择后收起。 + * 弹出层超出组件边界绘制,支持滚动(最多显示 6 项)。 + */ +public class DropdownComponent implements UIComponent { + + private static final int BG_COLOR = 0xFF404040; + private static final int HOVER_COLOR = 0xFF606060; + private static final int TEXT_COLOR = 0xFFFFFFFF; + private static final int POPUP_BG = 0xFF303030; + private static final int POPUP_HOVER = 0xFF505050; + private static final float PADDING_H = 8; + private static final float PADDING_V = 4; + private static final float ARROW_W = 10; + private static final int MAX_VISIBLE = 6; + + private Modifier modifier; + private final String[] options; + private int selectedIndex; + private boolean open; + private Consumer onChange; + + private float x, y, width, height; + + public DropdownComponent(Modifier modifier, String[] options, int selectedIndex) { + this.modifier = modifier; + this.options = options; + this.selectedIndex = Mth.clamp(selectedIndex, 0, options.length - 1); + } + + public DropdownComponent modifier(Modifier m) { this.modifier = m; return this; } + public DropdownComponent onChange(Consumer onChange) { this.onChange = onChange; return this; } + + public String selectedOption() { return options[selectedIndex]; } + public int selectedIndex() { return selectedIndex; } + + public void setOpen(boolean open) { this.open = open; } + public boolean isOpen() { return open; } + + @Override public Modifier modifier() { return modifier; } + @Override public List children() { return Collections.emptyList(); } + + @Override + public MeasuredSize measure(Constraints constraints) { + var font = Minecraft.getInstance().font; + float maxTextW = 0; + for (String opt : options) maxTextW = Math.max(maxTextW, font.width(opt)); + float w = maxTextW + PADDING_H * 2 + ARROW_W; + float h = font.lineHeight + PADDING_V * 2; + return MeasuredSize.of(constraints.constrainWidth(w), constraints.constrainHeight(h)); + } + + @Override + public void layout(float x, float y, float width, float height) { + this.x = x; this.y = y; this.width = width; this.height = height; + } + + @Override + public void extractRenderState(GuiGraphicsExtractor extractor) { + var font = Minecraft.getInstance().font; + int ix = (int) x, iy = (int) y, iw = (int) width, ih = (int) height; + + // 触发器 + int bg = open ? HOVER_COLOR : BG_COLOR; + extractor.fill(ix, iy, ix + iw, iy + ih, bg); + + String label = options[selectedIndex]; + int textY = (int) (y + (height + font.lineHeight) / 2f - font.lineHeight); + extractor.text(font, label, ix + (int) PADDING_H, textY, TEXT_COLOR); + + // ▼ 箭头 + String arrow = open ? "▲" : "▼"; + float arrowW = font.width(arrow); + extractor.text(font, arrow, (int) (x + width - PADDING_H - arrowW), textY, TEXT_COLOR); + + // 弹出层 + if (open && options.length > 0) { + int visible = Math.min(options.length, MAX_VISIBLE); + float itemH = font.lineHeight + 4; + float popupH = itemH * visible; + int piy = iy + ih; + + extractor.enableScissor(ix, piy, ix + iw, piy + (int) popupH); + extractor.fill(ix, piy, ix + iw, piy + (int) popupH, POPUP_BG); + + for (int i = 0; i < options.length; i++) { + float itemY = piy + i * itemH; + if (i < visible) { + int itemBg = (i == selectedIndex) ? HOVER_COLOR : POPUP_BG; + extractor.fill(ix, (int) itemY, ix + iw, (int) (itemY + itemH), itemBg); + extractor.text(font, options[i], ix + (int) PADDING_H, (int) (itemY + 2), TEXT_COLOR); + } + } + extractor.disableScissor(); + } + } + + /** 点击触发器区域 → 切换展开。返回 true 表示已消费事件。 */ + public boolean clickTrigger(float px, float py) { + if (triggerRect().contains(px, py)) { + open = !open; + return true; + } + return false; + } + + /** 点击弹出层中的某一项 → 选中并收起。返回 true 表示命中。 */ + public boolean clickPopup(float px, float py) { + if (!open) return false; + var font = Minecraft.getInstance().font; + float itemH = font.lineHeight + 4; + int visible = Math.min(options.length, MAX_VISIBLE); + float popupH = itemH * visible; + if (px < x || px > x + width || py < y + height || py > y + height + popupH) return false; + + int idx = (int) ((py - y - height) / itemH); + if (idx >= 0 && idx < options.length && idx < visible) { + select(idx); + return true; + } + return false; + } + + private void select(int idx) { + if (idx != selectedIndex) { + selectedIndex = idx; + if (onChange != null) onChange.accept(options[idx]); + } + open = false; + } + + private LayoutRect triggerRect() { + return LayoutRect.of(x, y, width, height); + } + + public LayoutRect hitRect() { + if (open) { + var font = Minecraft.getInstance().font; + float itemH = font.lineHeight + 4; + int visible = Math.min(options.length, MAX_VISIBLE); + return LayoutRect.of(x, y, width, height + itemH * visible); + } + return triggerRect(); + } +} From 2ec9a888917bce072ac7d8f7929f697ba1cb4d55 Mon Sep 17 00:00:00 2001 From: Gugle Date: Tue, 19 May 2026 01:39:56 +0800 Subject: [PATCH 39/67] feat(ui): enhance DropdownComponent with popup scrolling and z-order rendering --- .../client/screen/DeclarativeTestScreen.java | 2 +- .../dev/anvilcraft/lib/v2/ui/Composition.java | 1 + .../lib/v2/ui/DeclarativeScreen.java | 44 +++- .../dev/anvilcraft/lib/v2/ui/UIScope.java | 5 +- .../v2/ui/component/DropdownComponent.java | 210 +++++++++++++----- 5 files changed, 198 insertions(+), 64 deletions(-) diff --git a/module.test/src/main/java/dev/anvilcraft/lib/v2/test/client/screen/DeclarativeTestScreen.java b/module.test/src/main/java/dev/anvilcraft/lib/v2/test/client/screen/DeclarativeTestScreen.java index e2f2af6e..b9a1deb7 100644 --- a/module.test/src/main/java/dev/anvilcraft/lib/v2/test/client/screen/DeclarativeTestScreen.java +++ b/module.test/src/main/java/dev/anvilcraft/lib/v2/test/client/screen/DeclarativeTestScreen.java @@ -107,7 +107,7 @@ protected void content(UIScope scope) { // ── 8. Dropdown ── col.Text("8) Dropdown:"); - col.Dropdown(new String[]{"Option A", "Option B", "Option C"}, 0) + col.Dropdown(new String[]{"Option A", "Option B", "Option C"}, 0, 80) .onChange(v -> {}); col.Text(" (click to open)"); diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Composition.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Composition.java index 3db62756..6368cfa5 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Composition.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Composition.java @@ -139,6 +139,7 @@ private void copyRuntimeState(UIComponent old, UIComponent replacement) { if (old instanceof DropdownComponent oldDd && replacement instanceof DropdownComponent newDd) { newDd.setOpen(oldDd.isOpen()); + newDd.setPopupScrollY(oldDd.getPopupScrollY()); } } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java index a70486ac..62e87041 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java @@ -55,9 +55,18 @@ public void extractRenderState(GuiGraphicsExtractor extractor, int mouseX, int m composition.renderFrame(extractor, this.width, this.height); updateHover(mouseX, mouseY); refreshFocus(); + // 延迟渲染下拉弹出层(确保 z-order 正确) + for (UIComponent child : rootScope.getChildren()) { + renderPopups(child, extractor); + } } } + private void renderPopups(UIComponent component, GuiGraphicsExtractor extractor) { + if (component instanceof DropdownComponent dd) dd.renderPopup(extractor); + for (UIComponent child : component.children()) renderPopups(child, extractor); + } + /** recompose 后重新绑定 focusOwner(旧实例可能已被替换)。 */ private void refreshFocus() { if (focusOwner == null) return; @@ -88,21 +97,30 @@ public boolean mouseClicked(MouseButtonEvent event, boolean isDoubleClick) { var mc = Minecraft.getInstance(); int mx = (int) mc.mouseHandler.getScaledXPos(mc.getWindow()); int my = (int) mc.mouseHandler.getScaledYPos(mc.getWindow()); - // 先关闭所有下拉菜单,命中后再由 hitTestClick 重新打开 - for (UIComponent child : rootScope.getChildren()) { - closeDropdownsRecursive(child); - } + // 清除焦点 focusOwner = null; for (UIComponent child : rootScope.getChildren()) { clearFocusRecursive(child); } + + // 命中测试 + boolean hit = false; for (UIComponent child : rootScope.getChildren()) { - if (hitTestClick(child, mx, my)) { - mc.getSoundManager().play(SimpleSoundInstance.forUI(SoundEvents.UI_BUTTON_CLICK, 1.0f)); - return true; + if (hitTestClick(child, mx, my)) { hit = true; break; } + } + + // 未命中任何 dropdown 时关闭所有 + if (!hit) { + for (UIComponent child : rootScope.getChildren()) { + closeDropdownsRecursive(child); } } + + if (hit) { + mc.getSoundManager().play(SimpleSoundInstance.forUI(SoundEvents.UI_BUTTON_CLICK, 1.0f)); + return true; + } } return super.mouseClicked(event, isDoubleClick); } @@ -189,6 +207,7 @@ private boolean hitTestClick(UIComponent component, float px, float py) { return true; } if (component instanceof DropdownComponent dd) { + if (dd.isOnPopupScrollbar(px, py)) { dd.startPopupScrollbarDrag(py); return true; } if (dd.clickPopup(px, py)) return true; if (dd.clickTrigger(px, py)) return true; return false; @@ -214,6 +233,10 @@ private boolean hitTestDrag(UIComponent component, float px, float py) { sc.onScrollbarDrag(py); return true; } + if (component instanceof DropdownComponent dd && dd.isScrollbarDragging()) { + dd.onPopupScrollbarDrag(py); + return true; + } return false; } @@ -226,10 +249,11 @@ private void closeDropdownsRecursive(UIComponent component) { /** 递归停止拖拽状态。 */ private void stopDragRecursive(UIComponent component) { if (component instanceof ScrollableComponent sc) sc.stopScrollbarDrag(); + if (component instanceof DropdownComponent dd) dd.stopPopupScrollbarDrag(); for (UIComponent child : component.children()) stopDragRecursive(child); } - /** 滚轮命中测试(仅 ScrollableComponent 响应)。 */ + /** 滚轮命中测试(ScrollableComponent + Dropdown 弹出层)。 */ private boolean hitTestScroll(UIComponent component, float px, float py, float amount) { var children = component.children(); for (int i = children.size() - 1; i >= 0; i--) { @@ -238,6 +262,10 @@ private boolean hitTestScroll(UIComponent component, float px, float py, float a if (component instanceof ScrollableComponent sc && sc.hitRect().contains(px, py)) { return sc.onScroll(amount); } + if (component instanceof DropdownComponent dd && dd.isOpen() + && dd.popupRect().contains(px, py)) { + return dd.onPopupScroll(amount); + } return false; } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIScope.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIScope.java index 7bf79251..a51514d7 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIScope.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIScope.java @@ -123,9 +123,10 @@ public CheckboxComponent Checkbox(String label, Ref vModel) { * 创建下拉菜单。 * @param options 选项列表 * @param selectedIndex 初始选中索引 + * @param maxPopupHeight 弹出层最大高度 */ - public DropdownComponent Dropdown(String[] options, int selectedIndex) { - DropdownComponent c = new DropdownComponent(Modifier.NONE, options, selectedIndex); + public DropdownComponent Dropdown(String[] options, int selectedIndex, float maxPopupHeight) { + DropdownComponent c = new DropdownComponent(Modifier.NONE, options, selectedIndex, maxPopupHeight); addChild(c); Composition.current().emit(c); return c; diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/DropdownComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/DropdownComponent.java index c240585e..45cf3cf3 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/DropdownComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/DropdownComponent.java @@ -1,5 +1,6 @@ package dev.anvilcraft.lib.v2.ui.component; +import dev.anvilcraft.lib.v2.rendering.sdf.SdfGraphics; import dev.anvilcraft.lib.v2.ui.Constraints; import dev.anvilcraft.lib.v2.ui.LayoutRect; import dev.anvilcraft.lib.v2.ui.MeasuredSize; @@ -16,7 +17,7 @@ /** * 下拉菜单。点击展开选项列表,选择后收起。 - * 弹出层超出组件边界绘制,支持滚动(最多显示 6 项)。 + * 弹出层延迟渲染以确保 z-order 正确,支持最大高度 + 滚动。 */ public class DropdownComponent implements UIComponent { @@ -25,33 +26,42 @@ public class DropdownComponent implements UIComponent { private static final int TEXT_COLOR = 0xFFFFFFFF; private static final int POPUP_BG = 0xFF303030; private static final int POPUP_HOVER = 0xFF505050; + private static final int SCROLLBAR_COLOR = 0xFF888888; + private static final int SCROLLBAR_BG = 0xFF222222; + private static final int SCROLLBAR_W = 4; private static final float PADDING_H = 8; private static final float PADDING_V = 4; - private static final float ARROW_W = 10; - private static final int MAX_VISIBLE = 6; + private static final float ARROW_SIZE = 6; private Modifier modifier; private final String[] options; private int selectedIndex; private boolean open; + private final float maxPopupHeight; + private float popupScrollY; private Consumer onChange; + // popup 拖拽 + private boolean scrollbarDragging; + private float dragAnchorY; + private float x, y, width, height; - public DropdownComponent(Modifier modifier, String[] options, int selectedIndex) { + public DropdownComponent(Modifier modifier, String[] options, int selectedIndex, float maxPopupHeight) { this.modifier = modifier; this.options = options; - this.selectedIndex = Mth.clamp(selectedIndex, 0, options.length - 1); + this.selectedIndex = Mth.clamp(selectedIndex, 0, Math.max(0, options.length - 1)); + this.maxPopupHeight = maxPopupHeight; } public DropdownComponent modifier(Modifier m) { this.modifier = m; return this; } public DropdownComponent onChange(Consumer onChange) { this.onChange = onChange; return this; } - public String selectedOption() { return options[selectedIndex]; } - public int selectedIndex() { return selectedIndex; } - - public void setOpen(boolean open) { this.open = open; } + public String selectedOption() { return options.length > 0 ? options[selectedIndex] : ""; } + public void setOpen(boolean open) { this.open = open; if (!open) popupScrollY = 0; } public boolean isOpen() { return open; } + public float getPopupScrollY() { return popupScrollY; } + public void setPopupScrollY(float y) { this.popupScrollY = y; } @Override public Modifier modifier() { return modifier; } @Override public List children() { return Collections.emptyList(); } @@ -61,7 +71,7 @@ public MeasuredSize measure(Constraints constraints) { var font = Minecraft.getInstance().font; float maxTextW = 0; for (String opt : options) maxTextW = Math.max(maxTextW, font.width(opt)); - float w = maxTextW + PADDING_H * 2 + ARROW_W; + float w = maxTextW + PADDING_H * 2 + ARROW_SIZE + 8; float h = font.lineHeight + PADDING_V * 2; return MeasuredSize.of(constraints.constrainWidth(w), constraints.constrainHeight(h)); } @@ -71,78 +81,178 @@ public void layout(float x, float y, float width, float height) { this.x = x; this.y = y; this.width = width; this.height = height; } + // ── 触发器渲染 ── + @Override public void extractRenderState(GuiGraphicsExtractor extractor) { + renderTrigger(extractor); + // 弹出层由 DeclarativeScreen 在最后统一渲染 + } + + private void renderTrigger(GuiGraphicsExtractor extractor) { var font = Minecraft.getInstance().font; int ix = (int) x, iy = (int) y, iw = (int) width, ih = (int) height; - - // 触发器 int bg = open ? HOVER_COLOR : BG_COLOR; extractor.fill(ix, iy, ix + iw, iy + ih, bg); - String label = options[selectedIndex]; + String label = options.length > 0 ? options[selectedIndex] : ""; int textY = (int) (y + (height + font.lineHeight) / 2f - font.lineHeight); extractor.text(font, label, ix + (int) PADDING_H, textY, TEXT_COLOR); - // ▼ 箭头 - String arrow = open ? "▲" : "▼"; - float arrowW = font.width(arrow); - extractor.text(font, arrow, (int) (x + width - PADDING_H - arrowW), textY, TEXT_COLOR); - - // 弹出层 - if (open && options.length > 0) { - int visible = Math.min(options.length, MAX_VISIBLE); - float itemH = font.lineHeight + 4; - float popupH = itemH * visible; - int piy = iy + ih; - - extractor.enableScissor(ix, piy, ix + iw, piy + (int) popupH); - extractor.fill(ix, piy, ix + iw, piy + (int) popupH, POPUP_BG); - - for (int i = 0; i < options.length; i++) { - float itemY = piy + i * itemH; - if (i < visible) { - int itemBg = (i == selectedIndex) ? HOVER_COLOR : POPUP_BG; - extractor.fill(ix, (int) itemY, ix + iw, (int) (itemY + itemH), itemBg); - extractor.text(font, options[i], ix + (int) PADDING_H, (int) (itemY + 2), TEXT_COLOR); - } - } - extractor.disableScissor(); + // SDF 三角箭头 + float arrowCx = x + width - PADDING_H - ARROW_SIZE / 2f; + float arrowCy = y + height / 2f; + SdfGraphics.instance + .box(arrowCx, arrowCy, ARROW_SIZE, ARROW_SIZE) + .color(TEXT_COLOR) + .fill() + .center(true) + .draw(extractor); + } + + // ── 弹出层渲染(延迟调用,确保 z-order) ── + + /** 弹出层 item 高度。 */ + public float itemHeight() { + return Minecraft.getInstance().font.lineHeight + 4; + } + + /** 实际弹出层高度(min 内容高度, maxPopupHeight)。 */ + public float popupHeight() { + if (options.length == 0) return 0; + return Math.min(options.length * itemHeight(), maxPopupHeight); + } + + /** 弹出层包围盒。 */ + public LayoutRect popupRect() { + float ph = popupHeight(); + return LayoutRect.of(x, y + height, width, ph); + } + + /** 延迟渲染弹出层(在所有组件之后调用)。 */ + public void renderPopup(GuiGraphicsExtractor extractor) { + if (!open || options.length == 0) return; + + var font = Minecraft.getInstance().font; + float itemH = itemHeight(); + float ph = popupHeight(); + float totalH = options.length * itemH; + float maxScroll = Math.max(0, totalH - ph); + popupScrollY = Mth.clamp(popupScrollY, -maxScroll, 0); + + int px = (int) x, py = (int) (y + height), pw = (int) width; + + extractor.enableScissor(px, py, px + pw, py + (int) ph); + extractor.fill(px, py, px + pw, py + (int) ph, POPUP_BG); + + float startY = y + height + popupScrollY; + for (int i = 0; i < options.length; i++) { + float iy = startY + i * itemH; + float bottom = iy + itemH; + // 跳过完全不可见的项 + if (bottom <= y + height || iy >= y + height + ph) continue; + + int bg = (i == selectedIndex) ? POPUP_HOVER : POPUP_BG; + int fillTop = Math.max((int) iy, py); + int fillBot = Math.min((int) bottom, py + (int) ph); + extractor.fill(px, fillTop, px + pw, fillBot, bg); + extractor.text(font, options[i], px + (int) PADDING_H, (int) iy + 2, TEXT_COLOR); + } + extractor.disableScissor(); + + // 滚动条 + if (totalH > ph) { + float bh = Math.max(12, ph * ph / totalH); + float by = py + (-popupScrollY / maxScroll) * (ph - bh); + int bx = px + pw - SCROLLBAR_W - 1; + extractor.fill(bx, py, bx + SCROLLBAR_W, py + (int) ph, SCROLLBAR_BG); + extractor.fill(bx, (int) by, bx + SCROLLBAR_W, (int) (by + bh), SCROLLBAR_COLOR); } } - /** 点击触发器区域 → 切换展开。返回 true 表示已消费事件。 */ + // ── 交互 ── + + /** 点击触发器区域 → 切换展开。 */ public boolean clickTrigger(float px, float py) { if (triggerRect().contains(px, py)) { open = !open; + if (!open) popupScrollY = 0; return true; } return false; } - /** 点击弹出层中的某一项 → 选中并收起。返回 true 表示命中。 */ + /** 点击弹出层选项 → 选中并收起。返回 true 表示命中。 */ public boolean clickPopup(float px, float py) { - if (!open) return false; - var font = Minecraft.getInstance().font; - float itemH = font.lineHeight + 4; - int visible = Math.min(options.length, MAX_VISIBLE); - float popupH = itemH * visible; - if (px < x || px > x + width || py < y + height || py > y + height + popupH) return false; + if (!open || options.length == 0) return false; + float ph = popupHeight(); + if (px < x || px > x + width || py < y + height || py > y + height + ph) return false; - int idx = (int) ((py - y - height) / itemH); - if (idx >= 0 && idx < options.length && idx < visible) { + float itemH = itemHeight(); + float idxF = (py - y - height - popupScrollY) / itemH; + int idx = (int) idxF; + if (idx >= 0 && idx < options.length) { select(idx); return true; } return false; } + /** 滚轮滚动弹出层。 */ + public boolean onPopupScroll(float amount) { + if (!open) return false; + float ph = popupHeight(); + float totalH = options.length * itemHeight(); + if (totalH <= ph) return false; + float maxScroll = totalH - ph; + popupScrollY = Mth.clamp(popupScrollY + amount * 20, -maxScroll, 0); + return true; + } + + /** 弹出层滚动条命中测试。 */ + public boolean isOnPopupScrollbar(float mx, float my) { + if (!open) return false; + float ph = popupHeight(); + float totalH = options.length * itemHeight(); + if (totalH <= ph) return false; + float bh = Math.max(12, ph * ph / totalH); + float maxScroll = totalH - ph; + float by = y + height + (-popupScrollY / maxScroll) * (ph - bh); + int bx = (int) (x + width - SCROLLBAR_W - 1); + return mx >= bx && mx < bx + SCROLLBAR_W && my >= by && my < by + bh; + } + + public void startPopupScrollbarDrag(float my) { + scrollbarDragging = true; + float ph = popupHeight(); + float totalH = options.length * itemHeight(); + float bh = Math.max(12, ph * ph / totalH); + float maxScroll = totalH - ph; + float by = y + height + (-popupScrollY / maxScroll) * (ph - bh); + dragAnchorY = my - by; + } + + public void onPopupScrollbarDrag(float my) { + if (!scrollbarDragging) return; + float ph = popupHeight(); + float totalH = options.length * itemHeight(); + float bh = Math.max(12, ph * ph / totalH); + float maxScroll = totalH - ph; + float newBarY = my - dragAnchorY; + float ratio = Mth.clamp(newBarY / (ph - bh), 0f, 1f); + popupScrollY = -(ratio * maxScroll); + } + + public void stopPopupScrollbarDrag() { scrollbarDragging = false; } + public boolean isScrollbarDragging() { return scrollbarDragging; } + private void select(int idx) { if (idx != selectedIndex) { selectedIndex = idx; if (onChange != null) onChange.accept(options[idx]); } open = false; + popupScrollY = 0; } private LayoutRect triggerRect() { @@ -150,12 +260,6 @@ private LayoutRect triggerRect() { } public LayoutRect hitRect() { - if (open) { - var font = Minecraft.getInstance().font; - float itemH = font.lineHeight + 4; - int visible = Math.min(options.length, MAX_VISIBLE); - return LayoutRect.of(x, y, width, height + itemH * visible); - } return triggerRect(); } } From 6de8ac0532473b6c6c411d6be8222d8c226e5ea5 Mon Sep 17 00:00:00 2001 From: Gugle Date: Tue, 19 May 2026 01:51:46 +0800 Subject: [PATCH 40/67] feat(ui): add triangle rendering support to SDF graphics and components --- .../lib/v2/rendering/sdf/Sdf2d.java | 43 +++++++++++++++++++ .../lib/v2/rendering/sdf/SdfGraphics.java | 10 +++++ .../lib/v2/rendering/sdf/SdfParameters.java | 16 +++++++ .../lib/v2/rendering/sdf/SdfRenderType.java | 3 +- .../shaders/core/sdf_graphics.fsh | 30 +++++++++++++ .../v2/ui/component/DropdownComponent.java | 24 ++++++++--- 6 files changed, 119 insertions(+), 7 deletions(-) diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/sdf/Sdf2d.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/sdf/Sdf2d.java index 23aa4918..ff4df1b4 100644 --- a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/sdf/Sdf2d.java +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/sdf/Sdf2d.java @@ -98,6 +98,14 @@ public static float sd( shape.x, shape.y, shape.z ); + + case TRIANGLE -> sdTriangle( + px, py, + shape.x, shape.y, + shape.z, shape.w, + Float.intBitsToFloat(params.getTypeParams().w), + params.getSharedParams().w + ); }; if (params.isOnion()) { @@ -259,4 +267,39 @@ public static float sdEgg( ) - (ce + ra); } + /** + * 三角形 SDF。来自 IQ。 + */ + public static float sdTriangle( + float px, float py, + float x0, float y0, + float x1, float y1, + float x2, float y2 + ) { + float ex0 = x1 - x0, ey0 = y1 - y0; + float ex1 = x2 - x1, ey1 = y2 - y1; + float ex2 = x0 - x2, ey2 = y0 - y2; + float e0x = px - x0, e0y = py - y0; + float e1x = px - x1, e1y = py - y1; + float e2x = px - x2, e2y = py - y2; + + float v0 = e0x * ey0 - e0y * ex0; + float v1 = e1x * ey1 - e1y * ex1; + float v2 = e2x * ey2 - e2y * ex2; + + float d; + if (v0 * v1 > 0.0f && v1 * v2 > 0.0f) { + d = -Math.min(Math.min( + (e0x * ex0 + e0y * ey0) / (float) Math.sqrt(ex0 * ex0 + ey0 * ey0), + (e1x * ex1 + e1y * ey1) / (float) Math.sqrt(ex1 * ex1 + ey1 * ey1)), + (e2x * ex2 + e2y * ey2) / (float) Math.sqrt(ex2 * ex2 + ey2 * ey2)); + } else { + d = (float) Math.sqrt(Math.min(Math.min( + e0x * e0x + e0y * e0y, + e1x * e1x + e1y * e1y), + e2x * e2x + e2y * e2y)); + } + return d; + } + } diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/sdf/SdfGraphics.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/sdf/SdfGraphics.java index 7b1aef1b..f0d5c6fa 100644 --- a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/sdf/SdfGraphics.java +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/sdf/SdfGraphics.java @@ -109,6 +109,16 @@ public SdfGraphics egg( return this; } + /** 三角形。按逆时针顺序填入三个顶点坐标。包围盒自动计算。 */ + public SdfGraphics triangle( + float x0, float y0, + float x1, float y1, + float x2, float y2 + ) { + this.parameters.triangle(x0, y0, x1, y1, x2, y2); + return this; + } + public SdfGraphics color(int color) { this.parameters .color(color); return this; diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/sdf/SdfParameters.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/sdf/SdfParameters.java index 8e457c2d..43969770 100644 --- a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/sdf/SdfParameters.java +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/sdf/SdfParameters.java @@ -74,6 +74,22 @@ public void egg(float topRadius, float bottomRadius, float height) { this.shapeParams .set(height, bottomRadius, topRadius, 0.0f); } + /** 三角形。按逆时针顺序填入三个顶点坐标。顶点自动转为局部坐标。 */ + public void triangle(float x0, float y0, float x1, float y1, float x2, float y2) { + this._renderType(SdfRenderType.TRIANGLE); + float minX = Math.min(Math.min(x0, x1), x2); + float minY = Math.min(Math.min(y0, y1), y2); + float maxX = Math.max(Math.max(x0, x1), x2); + float maxY = Math.max(Math.max(y0, y1), y2); + float cx = minX + (maxX - minX) / 2f; + float cy = minY + (maxY - minY) / 2f; + this.rect.set(minX, minY, maxX - minX, maxY - minY); + this.shapeParams.set(x0 - cx, y0 - cy, x1 - cx, y1 - cy); + this.typeParams.z = 0; // onion off + this.typeParams.w = Float.floatToIntBits(x2 - cx); + this.sharedParams.w = y2 - cy; // fill 模式下安全 + } + public void smooth(float smooth) { this ._smooth(smooth); } diff --git a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/sdf/SdfRenderType.java b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/sdf/SdfRenderType.java index ed5cd227..c53c87b1 100644 --- a/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/sdf/SdfRenderType.java +++ b/module.rendering/src/main/java/dev/anvilcraft/lib/v2/rendering/sdf/SdfRenderType.java @@ -7,7 +7,8 @@ public enum SdfRenderType { SECTOR, PIE, CAPSULE, - EGG; + EGG, + TRIANGLE; private static final SdfRenderType[] VALUES = values(); diff --git a/module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/sdf_graphics.fsh b/module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/sdf_graphics.fsh index e375d52a..0a3e90d4 100644 --- a/module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/sdf_graphics.fsh +++ b/module.rendering/src/main/resources/assets/anvillib_rendering/shaders/core/sdf_graphics.fsh @@ -20,6 +20,7 @@ layout(std140) uniform SDFParameters { #define RT_PIE 4 #define RT_UCAPSULE 5 #define RT_EGG 6 +#define RT_TRIANGLE 7 #define PASS_FILL 0 #define PASS_LIGHT 1 @@ -106,6 +107,31 @@ float sdEgg( in vec2 p, in float he, in float ra, in float rb ) return length(vec2(p.x+ce,p.y))-(ce+ra); } +// from https://iquilezles.org/articles/distfunctions2d/ +float sdTriangle( in vec2 p, in vec2 v0, in vec2 v1, in vec2 v2 ) +{ + vec2 e0 = v1 - v0, e1 = v2 - v1, e2 = v0 - v2; + vec2 vp0 = p - v0, vp1 = p - v1, vp2 = p - v2; + + float s0 = vp0.x * e0.y - vp0.y * e0.x; + float s1 = vp1.x * e1.y - vp1.y * e1.x; + float s2 = vp2.x * e2.y - vp2.y * e2.x; + + float d; + if (s0 * s1 > 0.0 && s1 * s2 > 0.0) { + d = -min(min( + dot(vp0, e0) / length(e0), + dot(vp1, e1) / length(e1)), + dot(vp2, e2) / length(e2)); + } else { + d = sqrt(min(min( + dot(vp0, vp0), + dot(vp1, vp1)), + dot(vp2, vp2))); + } + return d; +} + void main() { Sdf params = SDFs[vIndex]; vec4 shape = params.Shape; @@ -135,6 +161,10 @@ void main() { case RT_EGG: d = sdEgg(p, shape.x, shape.y, shape.z); break; + case RT_TRIANGLE: + d = sdTriangle(p, shape.xy, shape.zw, + vec2(intBitsToFloat(params.Types.w), params.Shared.w)); + break; } float aa = max(fwidth(d) * 0.5, uSmoothRadius); diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/DropdownComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/DropdownComponent.java index 45cf3cf3..eb4ca4fa 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/DropdownComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/DropdownComponent.java @@ -102,12 +102,24 @@ private void renderTrigger(GuiGraphicsExtractor extractor) { // SDF 三角箭头 float arrowCx = x + width - PADDING_H - ARROW_SIZE / 2f; float arrowCy = y + height / 2f; - SdfGraphics.instance - .box(arrowCx, arrowCy, ARROW_SIZE, ARROW_SIZE) - .color(TEXT_COLOR) - .fill() - .center(true) - .draw(extractor); + float hs = ARROW_SIZE / 2f; + if (open) { + SdfGraphics.instance + .triangle(arrowCx - hs, arrowCy + hs * 0.6f, + arrowCx + hs, arrowCy + hs * 0.6f, + arrowCx, arrowCy - hs * 0.8f) + .color(TEXT_COLOR) + .fill() + .draw(extractor); + } else { + SdfGraphics.instance + .triangle(arrowCx - hs, arrowCy - hs * 0.6f, + arrowCx + hs, arrowCy - hs * 0.6f, + arrowCx, arrowCy + hs * 0.8f) + .color(TEXT_COLOR) + .fill() + .draw(extractor); + } } // ── 弹出层渲染(延迟调用,确保 z-order) ── From 187b58fc458b90faaaabea592a80c94f9f5cc008 Mon Sep 17 00:00:00 2001 From: Gugle Date: Tue, 19 May 2026 02:51:26 +0800 Subject: [PATCH 41/67] feat(ui): refactor components to use fluent accessors and simplify method signatures --- .../dev/anvilcraft/lib/v2/ui/Composition.java | 12 +++--- .../lib/v2/ui/DeclarativeScreen.java | 8 ++-- .../lib/v2/ui/component/BoxComponent.java | 13 +++--- .../lib/v2/ui/component/ButtonComponent.java | 12 ++++-- .../v2/ui/component/CheckboxComponent.java | 12 ++++-- .../lib/v2/ui/component/ColumnComponent.java | 40 ++++++------------- .../v2/ui/component/DropdownComponent.java | 22 +++++----- .../lib/v2/ui/component/GridComponent.java | 14 +++---- .../lib/v2/ui/component/ImageComponent.java | 19 ++++----- .../lib/v2/ui/component/RowComponent.java | 40 ++++++------------- .../v2/ui/component/ScrollableComponent.java | 22 +++++----- .../lib/v2/ui/component/SliderComponent.java | 14 ++++--- .../lib/v2/ui/component/SpacerComponent.java | 8 +++- .../lib/v2/ui/component/TextComponent.java | 22 +++++----- .../v2/ui/component/TextInputComponent.java | 22 +++++----- 15 files changed, 141 insertions(+), 139 deletions(-) diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Composition.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Composition.java index 6368cfa5..ec1ac426 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Composition.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Composition.java @@ -128,18 +128,18 @@ public void emit(UIComponent component) { private void copyRuntimeState(UIComponent old, UIComponent replacement) { if (old instanceof ScrollableComponent oldSc && replacement instanceof ScrollableComponent newSc) { - newSc.setScrollY(oldSc.getScrollY()); + newSc.setScrollY(oldSc.scrollY()); } if (old instanceof TextInputComponent oldTi && replacement instanceof TextInputComponent newTi) { - newTi.setValue(oldTi.getValue()); - newTi.setCursorPos(oldTi.getCursorPos()); - newTi.setFocused(oldTi.isFocused()); + newTi.setValue(oldTi.value()); + newTi.setCursorPos(oldTi.cursorPos()); + newTi.setFocused(oldTi.focused()); } if (old instanceof DropdownComponent oldDd && replacement instanceof DropdownComponent newDd) { - newDd.setOpen(oldDd.isOpen()); - newDd.setPopupScrollY(oldDd.getPopupScrollY()); + newDd.setOpen(oldDd.open()); + newDd.setPopupScrollY(oldDd.popupScrollY()); } } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java index 62e87041..180953d8 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java @@ -81,7 +81,7 @@ private void refreshFocus() { } private KeyInputHandler findFocused(UIComponent component) { - if (component instanceof TextInputComponent tf && tf.isFocused()) return tf; + if (component instanceof TextInputComponent tf && tf.focused()) return tf; for (UIComponent child : component.children()) { KeyInputHandler found = findFocused(child); if (found != null) return found; @@ -229,11 +229,11 @@ private boolean hitTestDrag(UIComponent component, float px, float py) { sl.setValueFromMouse(px); return true; } - if (component instanceof ScrollableComponent sc && sc.isScrollbarDragging()) { + if (component instanceof ScrollableComponent sc && sc.scrollbarDragging()) { sc.onScrollbarDrag(py); return true; } - if (component instanceof DropdownComponent dd && dd.isScrollbarDragging()) { + if (component instanceof DropdownComponent dd && dd.scrollbarDragging()) { dd.onPopupScrollbarDrag(py); return true; } @@ -262,7 +262,7 @@ private boolean hitTestScroll(UIComponent component, float px, float py, float a if (component instanceof ScrollableComponent sc && sc.hitRect().contains(px, py)) { return sc.onScroll(amount); } - if (component instanceof DropdownComponent dd && dd.isOpen() + if (component instanceof DropdownComponent dd && dd.open() && dd.popupRect().contains(px, py)) { return dd.onPopupScroll(amount); } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/BoxComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/BoxComponent.java index 1964585a..bef97363 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/BoxComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/BoxComponent.java @@ -1,5 +1,9 @@ package dev.anvilcraft.lib.v2.ui.component; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; + import dev.anvilcraft.lib.v2.ui.*; import net.minecraft.client.gui.GuiGraphicsExtractor; @@ -11,9 +15,12 @@ * 层叠布局。所有子组件重叠于同一区域,按声明顺序从底到顶绘制。 * Box 本身的大小由最大的子组件决定。 */ +@Accessors(fluent = true) public class BoxComponent implements UIComponent { + @Getter @Setter private Modifier modifier; + @Getter private List children = Collections.emptyList(); private List childSizes = Collections.emptyList(); @@ -35,13 +42,7 @@ public BoxComponent contentAlignment(Alignment.Horizontal h, Alignment.Vertical this.contentAlignmentV = v; return this; } - public BoxComponent modifier(Modifier m) { this.modifier = m; return this; } - - @Override - public Modifier modifier() { return modifier; } - @Override - public List children() { return children; } @Override public MeasuredSize measure(Constraints constraints) { diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ButtonComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ButtonComponent.java index 587f065a..7e8e00d8 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ButtonComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ButtonComponent.java @@ -1,5 +1,9 @@ package dev.anvilcraft.lib.v2.ui.component; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; + import dev.anvilcraft.lib.v2.ui.Constraints; import dev.anvilcraft.lib.v2.ui.LayoutRect; import dev.anvilcraft.lib.v2.ui.MeasuredSize; @@ -16,6 +20,7 @@ /** * 可点击按钮。原版 fill() 背景 + 手动居中 text() 文字。 */ +@Accessors(fluent = true) public class ButtonComponent implements UIComponent { // 原版按钮配色 @@ -25,7 +30,9 @@ public class ButtonComponent implements UIComponent { private static final float PADDING_H = 12; private static final float PADDING_V = 6; + @Getter @Setter private Modifier modifier; + @Setter private String label; @Nullable private Runnable onClick; @@ -38,12 +45,9 @@ public ButtonComponent(Modifier modifier, String label) { this.label = label; } - public ButtonComponent label(String label) { this.label = label; return this; } - public ButtonComponent onClick(@Nullable Runnable onClick) { this.onClick = onClick; return this; } - public ButtonComponent modifier(Modifier m) { this.modifier = m; return this; } + public ButtonComponent onClick(@Nullable Runnable onClick) { this.onClick = onClick; return this; } public void setHovered(boolean hovered) { this.hovered = hovered; } - @Override public Modifier modifier() { return modifier; } @Override public List children() { return Collections.emptyList(); } @Override diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/CheckboxComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/CheckboxComponent.java index b661bc4a..36fe8690 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/CheckboxComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/CheckboxComponent.java @@ -1,5 +1,9 @@ package dev.anvilcraft.lib.v2.ui.component; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; + import dev.anvilcraft.lib.v2.ui.Constraints; import dev.anvilcraft.lib.v2.ui.LayoutRect; import dev.anvilcraft.lib.v2.ui.MeasuredSize; @@ -14,6 +18,7 @@ * 复选框。点击切换 boolean 状态。 * 16x16 方框,未选中=深色空心,选中=浅色填充。 */ +@Accessors(fluent = true) public class CheckboxComponent implements UIComponent { private static final int BOX_COLOR = 0xFF404040; @@ -21,9 +26,11 @@ public class CheckboxComponent implements UIComponent { private static final float SIZE = 16; private static final float INSET = 3; + @Getter @Setter private Modifier modifier; private String label; private boolean checked; + @Setter private Runnable onToggle; private float x, y, width, height; @@ -34,10 +41,7 @@ public CheckboxComponent(Modifier modifier, String label, boolean checked) { this.checked = checked; } - public CheckboxComponent modifier(Modifier m) { this.modifier = m; return this; } - public CheckboxComponent onToggle(Runnable onToggle) { this.onToggle = onToggle; return this; } - - @Override public Modifier modifier() { return modifier; } + @Override public List children() { return Collections.emptyList(); } @Override diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ColumnComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ColumnComponent.java index 6101647f..8b280ad2 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ColumnComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ColumnComponent.java @@ -1,5 +1,9 @@ package dev.anvilcraft.lib.v2.ui.component; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; + import dev.anvilcraft.lib.v2.ui.*; import net.minecraft.client.gui.GuiGraphicsExtractor; @@ -10,14 +14,20 @@ /** * 纵向线性布局。子组件自上而下排列。主轴=垂直,交叉轴=水平。 */ +@Accessors(fluent = true) public class ColumnComponent implements UIComponent { + @Getter @Setter private Modifier modifier; + @Getter private List children = Collections.emptyList(); private List childSizes = Collections.emptyList(); + @Setter private Arrangement.Vertical verticalArrangement = Arrangement.Vertical.Top; + @Setter private Alignment.Horizontal horizontalAlignment = Alignment.Horizontal.Start; + @Setter private float spacing; // 布局状态 @@ -33,33 +43,11 @@ public void setChildren(List children) { // ── 链式 setter ── - public ColumnComponent verticalArrangement(Arrangement.Vertical va) { - this.verticalArrangement = va; - return this; - } - - public ColumnComponent horizontalAlignment(Alignment.Horizontal ha) { - this.horizontalAlignment = ha; - return this; - } - - public ColumnComponent spacing(float spacing) { - this.spacing = spacing; - return this; - } - - public ColumnComponent modifier(Modifier m) { - this.modifier = m; - return this; - } - - @Override - public Modifier modifier() { return modifier; } + + + - @Override - public List children() { return children; } - @Override public MeasuredSize measure(Constraints constraints) { if (children.isEmpty()) return MeasuredSize.ZERO; @@ -87,7 +75,6 @@ public MeasuredSize measure(Constraints constraints) { ); } - @Override public void layout(float x, float y, float width, float height) { this.x = x; this.y = y; @@ -106,7 +93,6 @@ public void layout(float x, float y, float width, float height) { } } - @Override public void extractRenderState(GuiGraphicsExtractor extractor) { for (UIComponent child : children) { child.extractRenderState(extractor); diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/DropdownComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/DropdownComponent.java index eb4ca4fa..4b66291a 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/DropdownComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/DropdownComponent.java @@ -1,5 +1,9 @@ package dev.anvilcraft.lib.v2.ui.component; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; + import dev.anvilcraft.lib.v2.rendering.sdf.SdfGraphics; import dev.anvilcraft.lib.v2.ui.Constraints; import dev.anvilcraft.lib.v2.ui.LayoutRect; @@ -19,6 +23,7 @@ * 下拉菜单。点击展开选项列表,选择后收起。 * 弹出层延迟渲染以确保 z-order 正确,支持最大高度 + 滚动。 */ +@Accessors(fluent = true) public class DropdownComponent implements UIComponent { private static final int BG_COLOR = 0xFF404040; @@ -33,15 +38,20 @@ public class DropdownComponent implements UIComponent { private static final float PADDING_V = 4; private static final float ARROW_SIZE = 6; + @Getter @Setter private Modifier modifier; private final String[] options; private int selectedIndex; + @Getter private boolean open; private final float maxPopupHeight; + @Getter private float popupScrollY; + @Setter private Consumer onChange; // popup 拖拽 + @Getter private boolean scrollbarDragging; private float dragAnchorY; @@ -54,16 +64,11 @@ public DropdownComponent(Modifier modifier, String[] options, int selectedIndex, this.maxPopupHeight = maxPopupHeight; } - public DropdownComponent modifier(Modifier m) { this.modifier = m; return this; } - public DropdownComponent onChange(Consumer onChange) { this.onChange = onChange; return this; } - + public String selectedOption() { return options.length > 0 ? options[selectedIndex] : ""; } public void setOpen(boolean open) { this.open = open; if (!open) popupScrollY = 0; } - public boolean isOpen() { return open; } - public float getPopupScrollY() { return popupScrollY; } - public void setPopupScrollY(float y) { this.popupScrollY = y; } + public void setPopupScrollY(float y) { this.popupScrollY = y; } - @Override public Modifier modifier() { return modifier; } @Override public List children() { return Collections.emptyList(); } @Override @@ -256,8 +261,7 @@ public void onPopupScrollbarDrag(float my) { } public void stopPopupScrollbarDrag() { scrollbarDragging = false; } - public boolean isScrollbarDragging() { return scrollbarDragging; } - + private void select(int idx) { if (idx != selectedIndex) { selectedIndex = idx; diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/GridComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/GridComponent.java index 8d5dd314..db57de44 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/GridComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/GridComponent.java @@ -1,5 +1,9 @@ package dev.anvilcraft.lib.v2.ui.component; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; + import dev.anvilcraft.lib.v2.ui.Constraints; import dev.anvilcraft.lib.v2.ui.MeasuredSize; import dev.anvilcraft.lib.v2.ui.Modifier; @@ -13,10 +17,13 @@ /** * 网格布局。子组件按列数排列,每格大小由最大子组件决定。 */ +@Accessors(fluent = true) public class GridComponent implements UIComponent { + @Getter @Setter private Modifier modifier; private final int columns; + @Getter private List children = Collections.emptyList(); private List childSizes = Collections.emptyList(); private float hSpacing, vSpacing; @@ -33,13 +40,8 @@ public void setChildren(List children) { this.children = List.copyOf(children); } - public GridComponent modifier(Modifier m) { this.modifier = m; return this; } public GridComponent spacing(float h, float v) { this.hSpacing = h; this.vSpacing = v; return this; } - @Override public Modifier modifier() { return modifier; } - @Override public List children() { return children; } - - @Override public MeasuredSize measure(Constraints constraints) { if (children.isEmpty()) return MeasuredSize.ZERO; @@ -65,7 +67,6 @@ public MeasuredSize measure(Constraints constraints) { return MeasuredSize.of(constraints.constrainWidth(totalW), constraints.constrainHeight(totalH)); } - @Override public void layout(float x, float y, float width, float height) { this.x = x; this.y = y; this.width = width; this.height = height; @@ -78,7 +79,6 @@ public void layout(float x, float y, float width, float height) { } } - @Override public void extractRenderState(GuiGraphicsExtractor extractor) { for (UIComponent child : children) child.extractRenderState(extractor); } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ImageComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ImageComponent.java index bb14d89c..16eb98fa 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ImageComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ImageComponent.java @@ -1,5 +1,9 @@ package dev.anvilcraft.lib.v2.ui.component; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; + import dev.anvilcraft.lib.v2.ui.Constraints; import dev.anvilcraft.lib.v2.ui.MeasuredSize; import dev.anvilcraft.lib.v2.ui.Modifier; @@ -15,11 +19,16 @@ * 渲染一个材质精灵(sprite)。 * 通过 {@link GuiGraphicsExtractor#blitSprite} 使用原版纹理管线。 */ +@Accessors(fluent = true) public class ImageComponent implements UIComponent { + @Getter @Setter private Modifier modifier; + @Setter private Identifier sprite; + @Getter private float imageWidth; + @Getter private float imageHeight; private float x, y, width, height; @@ -31,10 +40,7 @@ public ImageComponent(Modifier modifier, Identifier sprite, float imageWidth, fl this.imageHeight = imageHeight; } - public ImageComponent sprite(Identifier sprite) { this.sprite = sprite; return this; } - public ImageComponent modifier(Modifier m) { this.modifier = m; return this; } - - @Override public Modifier modifier() { return modifier; } + @Override public List children() { return Collections.emptyList(); } @Override @@ -62,9 +68,4 @@ public void extractRenderState(GuiGraphicsExtractor extractor) { (int) width, (int) height ); } - - /** 获取组件逻辑宽度(可能被 modifier 修改后不同)。 */ - float imageWidth() { return imageWidth; } - /** 获取组件逻辑高度。 */ - float imageHeight() { return imageHeight; } } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/RowComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/RowComponent.java index 88dfab93..5ba0c446 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/RowComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/RowComponent.java @@ -1,5 +1,9 @@ package dev.anvilcraft.lib.v2.ui.component; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; + import dev.anvilcraft.lib.v2.ui.*; import net.minecraft.client.gui.GuiGraphicsExtractor; @@ -10,14 +14,20 @@ /** * 横向线性布局。子组件自左而右排列。主轴=水平,交叉轴=垂直。 */ +@Accessors(fluent = true) public class RowComponent implements UIComponent { + @Getter @Setter private Modifier modifier; + @Getter private List children = Collections.emptyList(); private List childSizes = Collections.emptyList(); + @Setter private Arrangement.Horizontal horizontalArrangement = Arrangement.Horizontal.Start; + @Setter private Alignment.Vertical verticalAlignment = Alignment.Vertical.Top; + @Setter private float spacing; private float x, y, width, height; @@ -30,33 +40,11 @@ public void setChildren(List children) { this.children = List.copyOf(children); } - public RowComponent horizontalArrangement(Arrangement.Horizontal ha) { - this.horizontalArrangement = ha; - return this; - } - - public RowComponent verticalAlignment(Alignment.Vertical va) { - this.verticalAlignment = va; - return this; - } - - public RowComponent spacing(float spacing) { - this.spacing = spacing; - return this; - } - - public RowComponent modifier(Modifier m) { - this.modifier = m; - return this; - } - - @Override - public Modifier modifier() { return modifier; } + + + - @Override - public List children() { return children; } - @Override public MeasuredSize measure(Constraints constraints) { if (children.isEmpty()) return MeasuredSize.ZERO; @@ -84,7 +72,6 @@ public MeasuredSize measure(Constraints constraints) { ); } - @Override public void layout(float x, float y, float width, float height) { this.x = x; this.y = y; @@ -103,7 +90,6 @@ public void layout(float x, float y, float width, float height) { } } - @Override public void extractRenderState(GuiGraphicsExtractor extractor) { for (UIComponent child : children) { child.extractRenderState(extractor); diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ScrollableComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ScrollableComponent.java index 456ae01c..e483e231 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ScrollableComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ScrollableComponent.java @@ -1,5 +1,9 @@ package dev.anvilcraft.lib.v2.ui.component; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; + import dev.anvilcraft.lib.v2.ui.Constraints; import dev.anvilcraft.lib.v2.ui.LayoutRect; import dev.anvilcraft.lib.v2.ui.MeasuredSize; @@ -16,19 +20,24 @@ * 可滚动容器。限制最大高度,超出部分可垂直滚动。 * 渲染时自动裁剪,并绘制滚动条。 */ +@Accessors(fluent = true) public class ScrollableComponent implements UIComponent { private static final int SCROLLBAR_COLOR = 0xFF888888; private static final int SCROLLBAR_BG = 0xFF333333; private static final int SCROLLBAR_W = 4; + @Getter @Setter private Modifier modifier; private final float maxHeight; + @Getter private List children = Collections.emptyList(); private List childSizes = Collections.emptyList(); + @Getter private float scrollY; private float contentHeight; + @Getter private boolean scrollbarDragging; private float dragAnchorY; @@ -43,12 +52,7 @@ public void setChildren(List children) { this.children = List.copyOf(children); } - public ScrollableComponent modifier(Modifier m) { this.modifier = m; return this; } - - @Override public Modifier modifier() { return modifier; } - @Override public List children() { return children; } - @Override public MeasuredSize measure(Constraints constraints) { if (children.isEmpty()) return MeasuredSize.ZERO; @@ -75,7 +79,6 @@ public MeasuredSize measure(Constraints constraints) { ); } - @Override public void layout(float x, float y, float width, float height) { this.x = x; this.y = y; @@ -92,7 +95,6 @@ public void layout(float x, float y, float width, float height) { } } - @Override public void extractRenderState(GuiGraphicsExtractor extractor) { int ix = (int) x, iy = (int) y, iw = (int) width, ih = (int) height; @@ -154,8 +156,7 @@ public void stopScrollbarDrag() { scrollbarDragging = false; } - public boolean isScrollbarDragging() { return scrollbarDragging; } - + private float barH() { return Math.max(16, height * height / contentHeight); } @@ -165,8 +166,7 @@ private float barY() { return y + (-scrollY / maxScroll) * (height - barH()); } - public float getScrollY() { return scrollY; } - public void setScrollY(float scrollY) { this.scrollY = scrollY; } + public void setScrollY(float scrollY) { this.scrollY = scrollY; } /** 命中测试包围盒。 */ public LayoutRect hitRect() { diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/SliderComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/SliderComponent.java index 74d50d9e..804029e7 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/SliderComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/SliderComponent.java @@ -1,5 +1,9 @@ package dev.anvilcraft.lib.v2.ui.component; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; + import dev.anvilcraft.lib.v2.ui.Constraints; import dev.anvilcraft.lib.v2.ui.LayoutRect; import dev.anvilcraft.lib.v2.ui.MeasuredSize; @@ -16,6 +20,7 @@ * 滑块。水平拖拽选择范围内的值。 * 原版风格:深色轨道 + 浅色滑块按钮。 */ +@Accessors(fluent = true) public class SliderComponent implements UIComponent { private static final int TRACK_COLOR = 0xFF404040; @@ -24,10 +29,13 @@ public class SliderComponent implements UIComponent { private static final float THUMB_W = 8; private static final float THUMB_H = 16; + @Getter @Setter private Modifier modifier; + @Getter private float value; private final float min, max; private final float trackWidth; + @Setter private Consumer onChange; private float x, y, width, height; @@ -41,11 +49,7 @@ public SliderComponent(Modifier modifier, float value, float min, float max, flo this.onChange = onChange; } - public SliderComponent modifier(Modifier m) { this.modifier = m; return this; } - public SliderComponent onChange(Consumer onChange) { this.onChange = onChange; return this; } - public float value() { return value; } - - @Override public Modifier modifier() { return modifier; } + @Override public List children() { return Collections.emptyList(); } @Override diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/SpacerComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/SpacerComponent.java index 6c16d316..cffcf9ec 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/SpacerComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/SpacerComponent.java @@ -1,5 +1,9 @@ package dev.anvilcraft.lib.v2.ui.component; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; + import dev.anvilcraft.lib.v2.ui.Constraints; import dev.anvilcraft.lib.v2.ui.MeasuredSize; import dev.anvilcraft.lib.v2.ui.Modifier; @@ -12,8 +16,10 @@ /** * 固定尺寸的空白占位组件,不渲染任何内容。 */ +@Accessors(fluent = true) public class SpacerComponent implements UIComponent { + @Getter @Setter private Modifier modifier; private final float spacerWidth; private final float spacerHeight; @@ -24,10 +30,8 @@ public SpacerComponent(Modifier modifier, float width, float height) { this.spacerHeight = height; } - @Override public Modifier modifier() { return modifier; } @Override public List children() { return Collections.emptyList(); } - public SpacerComponent modifier(Modifier m) { this.modifier = m; return this; } @Override public MeasuredSize measure(Constraints constraints) { diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextComponent.java index 69c11ff7..92347c1a 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextComponent.java @@ -1,5 +1,9 @@ package dev.anvilcraft.lib.v2.ui.component; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; + import dev.anvilcraft.lib.v2.ui.Constraints; import dev.anvilcraft.lib.v2.ui.MeasuredSize; import dev.anvilcraft.lib.v2.ui.Modifier; @@ -14,6 +18,7 @@ /** * 单行文字渲染。默认样式与原版一致:白色带阴影、左对齐。 */ +@Accessors(fluent = true) public class TextComponent implements UIComponent { /** 文字水平对齐方式 */ @@ -21,10 +26,15 @@ public enum Align { LEFT, CENTER, RIGHT } private static final int VANILLA_TEXT_COLOR = 0xFFFFFFFF; + @Getter @Setter private Modifier modifier; + @Setter private String text; + @Setter private int color = VANILLA_TEXT_COLOR; - private boolean dropShadow; + @Setter + private boolean shadow; + @Setter private Align align = Align.LEFT; private float x, y, width, height; @@ -34,13 +44,7 @@ public TextComponent(Modifier modifier, String text) { this.text = text; } - public TextComponent text(String text) { this.text = text; return this; } - public TextComponent color(int color) { this.color = color; return this; } - public TextComponent shadow(boolean enable) { this.dropShadow = enable; return this; } - public TextComponent align(Align align) { this.align = align; return this; } - public TextComponent modifier(Modifier m) { this.modifier = m; return this; } - - @Override public Modifier modifier() { return modifier; } + @Override public List children() { return Collections.emptyList(); } @Override @@ -71,7 +75,7 @@ public void extractRenderState(GuiGraphicsExtractor extractor) { case RIGHT -> x + width - textW; }; - extractor.text(font, text, (int) renderX, (int) y, color, dropShadow); + extractor.text(font, text, (int) renderX, (int) y, color, this.shadow); } } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextInputComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextInputComponent.java index 5362c4ba..b3549b08 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextInputComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextInputComponent.java @@ -1,5 +1,9 @@ package dev.anvilcraft.lib.v2.ui.component; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; + import dev.anvilcraft.lib.v2.ui.Constraints; import dev.anvilcraft.lib.v2.ui.LayoutRect; import dev.anvilcraft.lib.v2.ui.MeasuredSize; @@ -24,6 +28,7 @@ * placeholder 作为占位提示(灰色),开始输入后直接替换为输入内容。 * 字符输入通过 {@link CharacterEvent} 处理,支持所有语言和输入法。 */ +@Accessors(fluent = true) public class TextInputComponent implements UIComponent, KeyInputHandler { private static final int BG_COLOR = 0xFF202020; @@ -34,12 +39,17 @@ public class TextInputComponent implements UIComponent, KeyInputHandler { private static final float PADDING_V = 4; private static final float WIDTH = 160; + @Getter @Setter private Modifier modifier; + @Getter private String value = ""; private final String placeholder; + @Getter private boolean focused; + @Setter private Consumer onChange; private int displayPos; + @Getter private int cursorPos; private float x, y, width, height; @@ -49,16 +59,10 @@ public TextInputComponent(Modifier modifier, String placeholder) { this.placeholder = placeholder != null ? placeholder : ""; } - public TextInputComponent modifier(Modifier m) { this.modifier = m; return this; } - public TextInputComponent onChange(Consumer onChange) { this.onChange = onChange; return this; } - public String getValue() { return value; } - public void setValue(String value) { this.value = value != null ? value : ""; this.cursorPos = this.value.length(); } - public int getCursorPos() { return cursorPos; } - public void setCursorPos(int pos) { this.cursorPos = Math.clamp(pos, 0, value.length()); } + public void setValue(String value) { this.value = value != null ? value : ""; this.cursorPos = this.value.length(); } + public void setCursorPos(int pos) { this.cursorPos = Math.clamp(pos, 0, value.length()); } public void setFocused(boolean focused) { this.focused = focused; } - public boolean isFocused() { return focused; } - - @Override public Modifier modifier() { return modifier; } + @Override public List children() { return Collections.emptyList(); } @Override From bcff1c2ecaadc8f09160c611485a1f046a1c872d Mon Sep 17 00:00:00 2001 From: Gugle Date: Tue, 19 May 2026 02:52:31 +0800 Subject: [PATCH 42/67] feat(ui): improve code readability by formatting comments and adjusting line breaks --- .../dev/anvilcraft/lib/v2/ui/Alignment.java | 11 +- .../dev/anvilcraft/lib/v2/ui/Animatable.java | 32 +++-- .../dev/anvilcraft/lib/v2/ui/Arrangement.java | 19 ++- .../dev/anvilcraft/lib/v2/ui/Composition.java | 106 +++++++------- .../lib/v2/ui/DeclarativeScreen.java | 124 ++++++++++------- .../dev/anvilcraft/lib/v2/ui/ForEach.java | 7 +- .../dev/anvilcraft/lib/v2/ui/LayoutRect.java | 8 +- .../dev/anvilcraft/lib/v2/ui/Modifier.java | 42 +++--- .../java/dev/anvilcraft/lib/v2/ui/Ref.java | 10 +- .../dev/anvilcraft/lib/v2/ui/UIComponent.java | 8 +- .../dev/anvilcraft/lib/v2/ui/UIScope.java | 73 ++++++---- .../lib/v2/ui/component/BoxComponent.java | 14 +- .../lib/v2/ui/component/ButtonComponent.java | 48 ++++--- .../v2/ui/component/CheckboxComponent.java | 37 +++-- .../lib/v2/ui/component/ColumnComponent.java | 23 ++-- .../v2/ui/component/DropdownComponent.java | 129 +++++++++++------- .../lib/v2/ui/component/GridComponent.java | 23 ++-- .../lib/v2/ui/component/GridScope.java | 7 - .../lib/v2/ui/component/ImageComponent.java | 31 +++-- .../lib/v2/ui/component/RowComponent.java | 23 ++-- .../v2/ui/component/ScrollableComponent.java | 53 ++++--- .../lib/v2/ui/component/ScrollableScope.java | 7 - .../lib/v2/ui/component/SliderComponent.java | 34 +++-- .../lib/v2/ui/component/SpacerComponent.java | 21 +-- .../lib/v2/ui/component/TextComponent.java | 26 ++-- .../v2/ui/component/TextInputComponent.java | 88 ++++++++---- .../v2/ui/component/{ => scope}/BoxScope.java | 3 +- .../ui/component/{ => scope}/ColumnScope.java | 3 +- .../lib/v2/ui/component/scope/GridScope.java | 10 ++ .../v2/ui/component/{ => scope}/RowScope.java | 3 +- .../ui/component/scope/ScrollableScope.java | 10 ++ .../lib/v2/ui/input/KeyInputHandler.java | 4 +- .../lib/v2/ui/modifier/BackgroundElement.java | 14 +- .../lib/v2/ui/modifier/BorderElement.java | 14 +- .../lib/v2/ui/modifier/ModifierElement.java | 34 ++--- .../lib/v2/ui/modifier/PaddingElement.java | 12 +- .../lib/v2/ui/modifier/SizeElement.java | 2 +- 37 files changed, 672 insertions(+), 441 deletions(-) delete mode 100644 module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/GridScope.java delete mode 100644 module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ScrollableScope.java rename module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/{ => scope}/BoxScope.java (56%) rename module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/{ => scope}/ColumnScope.java (57%) create mode 100644 module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/scope/GridScope.java rename module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/{ => scope}/RowScope.java (56%) create mode 100644 module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/scope/ScrollableScope.java diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Alignment.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Alignment.java index da5140af..e0d4a53f 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Alignment.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Alignment.java @@ -5,9 +5,12 @@ */ public final class Alignment { - private Alignment() {} + private Alignment() { + } - /** 水平对齐(Column 中每个子组件的 X 定位)。 */ + /** + * 水平对齐(Column 中每个子组件的 X 定位)。 + */ public enum Horizontal { Start, Center, End; @@ -25,7 +28,9 @@ public float align(float totalWidth, float childWidth) { } } - /** 垂直对齐(Row 中每个子组件的 Y 定位)。 */ + /** + * 垂直对齐(Row 中每个子组件的 Y 定位)。 + */ public enum Vertical { Top, Center, Bottom; diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Animatable.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Animatable.java index 13c29b8d..fe8e6e36 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Animatable.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Animatable.java @@ -19,12 +19,25 @@ public Animatable(float initialValue) { this.targetValue = initialValue; } - /** 获取当前动画值。 */ + /** + * 获取当前动画值。 + */ public float getValue() { return value; } - /** 动画到目标值,durationTicks 帧内完成。 */ + /** + * 直接设置值(无动画)。 + */ + public void setValue(float value) { + this.value = value; + this.targetValue = value; + this.elapsed = 0; + } + + /** + * 动画到目标值,durationTicks 帧内完成。 + */ public void animateTo(float target, int durationTicks) { if (durationTicks <= 0) { this.value = target; @@ -38,14 +51,9 @@ public void animateTo(float target, int durationTicks) { this.elapsed = 0; } - /** 直接设置值(无动画)。 */ - public void setValue(float value) { - this.value = value; - this.targetValue = value; - this.elapsed = 0; - } - - /** 每 tick 调用一次以推进动画。返回 true 表示动画进行中。 */ + /** + * 每 tick 调用一次以推进动画。返回 true 表示动画进行中。 + */ public boolean tick() { if (elapsed >= durationTicks) return false; elapsed++; @@ -56,7 +64,9 @@ public boolean tick() { return elapsed < durationTicks; } - /** 动画是否进行中。 */ + /** + * 动画是否进行中。 + */ public boolean isRunning() { return elapsed < durationTicks; } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Arrangement.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Arrangement.java index 9c6adee6..30bc6207 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Arrangement.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Arrangement.java @@ -7,16 +7,19 @@ */ public final class Arrangement { - private Arrangement() {} + private Arrangement() { + } - /** 纵向排列(Column 主轴)。 */ + /** + * 纵向排列(Column 主轴)。 + */ public enum Vertical { Top, Center, Bottom, SpaceBetween, SpaceAround, SpaceEvenly; /** - * @param totalHeight 可用总高度 + * @param totalHeight 可用总高度 * @param childHeights 各子组件高度 - * @param spacing 间距 + * @param spacing 间距 * @return 各子组件的 y 偏移 */ public float[] arrange(float totalHeight, List childHeights, float spacing) { @@ -80,14 +83,16 @@ public float[] arrange(float totalHeight, List childHeights, float spacin } } - /** 横向排列(Row 主轴)。 */ + /** + * 横向排列(Row 主轴)。 + */ public enum Horizontal { Start, Center, End, SpaceBetween, SpaceAround, SpaceEvenly; /** - * @param totalWidth 可用总宽度 + * @param totalWidth 可用总宽度 * @param childWidths 各子组件宽度 - * @param spacing 间距 + * @param spacing 间距 * @return 各子组件的 x 偏移 */ public float[] arrange(float totalWidth, List childWidths, float spacing) { diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Composition.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Composition.java index ec1ac426..c1ed8618 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Composition.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Composition.java @@ -4,13 +4,17 @@ import dev.anvilcraft.lib.v2.ui.component.ScrollableComponent; import dev.anvilcraft.lib.v2.ui.component.TextInputComponent; import net.minecraft.client.gui.GuiGraphicsExtractor; +import org.jspecify.annotations.Nullable; -import java.util.*; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; import java.util.function.Consumer; import java.util.function.Supplier; -import org.jspecify.annotations.Nullable; - /** * 组合引擎,驱动 recompose、状态追踪和渲染。 *

@@ -24,14 +28,38 @@ public class Composition { private static final ThreadLocal CURRENT = new ThreadLocal<>(); + private final List slots = new ArrayList<>(); + private final Map rememberedValues = new HashMap<>(); - /** 返回当前线程上的组合实例,可能为 null。 */ + // ── slot table ── + private final List animatables = new ArrayList<>(); + /** + * 当前正在 emit 的 slot(在 {@link #emit} 期间设置)。 + */ + @Nullable + Slot currentSlot; + private int currentIndex; + private int currentRememberKey; + private boolean dirty = true; + + // ── 状态 ── + private Consumer content; + private UIScope rootScope; + public Composition(UIScope rootScope) { + this.rootScope = rootScope; + } + + /** + * 返回当前线程上的组合实例,可能为 null。 + */ @Nullable public static Composition currentOrNull() { return CURRENT.get(); } - /** 返回当前线程上的组合实例,不存在则抛出异常。 */ + /** + * 返回当前线程上的组合实例,不存在则抛出异常。 + */ public static Composition current() { Composition c = CURRENT.get(); if (c == null) { @@ -40,38 +68,20 @@ public static Composition current() { return c; } - // ── slot table ── - - private final List slots = new ArrayList<>(); - private int currentIndex; - private int currentRememberKey; - private final Map rememberedValues = new HashMap<>(); - - /** 当前正在 emit 的 slot(在 {@link #emit} 期间设置)。 */ - @Nullable - Slot currentSlot; - - // ── 状态 ── - - private boolean dirty = true; - private Consumer content; - private UIScope rootScope; - private final List animatables = new ArrayList<>(); - - public Composition(UIScope rootScope) { - this.rootScope = rootScope; - } - public void setContent(Consumer content) { this.content = content; } - /** 标记组合需要在下一帧 recompose。 */ + /** + * 标记组合需要在下一帧 recompose。 + */ public void invalidate() { dirty = true; } - /** 注册动画值,每帧自动 tick。动画进行中时自动触发 recompose。 */ + /** + * 注册动画值,每帧自动 tick。动画进行中时自动触发 recompose。 + */ public void watch(Animatable anim) { if (!animatables.contains(anim)) { animatables.add(anim); @@ -95,7 +105,9 @@ public T remember(Supplier init) { return value; } - /** {@code comp.ref(0)} 等价于 {@code comp.remember(() -> new Ref<>(0))}。 */ + /** + * {@code comp.ref(0)} 等价于 {@code comp.remember(() -> new Ref<>(0))}。 + */ public Ref ref(T initialValue) { return remember(() -> new Ref<>(initialValue)); } @@ -124,20 +136,22 @@ public void emit(UIComponent component) { currentIndex++; } - /** 将旧组件的运行时状态复制到新组件。 */ + /** + * 将旧组件的运行时状态复制到新组件。 + */ private void copyRuntimeState(UIComponent old, UIComponent replacement) { if (old instanceof ScrollableComponent oldSc - && replacement instanceof ScrollableComponent newSc) { + && replacement instanceof ScrollableComponent newSc) { newSc.setScrollY(oldSc.scrollY()); } if (old instanceof TextInputComponent oldTi - && replacement instanceof TextInputComponent newTi) { + && replacement instanceof TextInputComponent newTi) { newTi.setValue(oldTi.value()); newTi.setCursorPos(oldTi.cursorPos()); newTi.setFocused(oldTi.focused()); } if (old instanceof DropdownComponent oldDd - && replacement instanceof DropdownComponent newDd) { + && replacement instanceof DropdownComponent newDd) { newDd.setOpen(oldDd.open()); newDd.setPopupScrollY(oldDd.popupScrollY()); } @@ -203,33 +217,33 @@ private boolean hasDirtySlots() { private void renderTree(UIComponent component, GuiGraphicsExtractor extractor, Constraints constraints) { // 1. Apply modifier to constraints Constraints modConstraints = component.modifier().foldIn( - constraints, - (c, el) -> el.modifyConstraints(c) + constraints, + (c, el) -> el.modifyConstraints(c) ); // 2. Measure MeasuredSize size = component.measure(modConstraints); size = component.modifier().foldOut( - size, - (el, s) -> el.modifyMeasuredSize(component, modConstraints, s) + size, + (el, s) -> el.modifyMeasuredSize(component, modConstraints, s) ); // 3. Layout LayoutRect rect = LayoutRect.of(0, 0, size.width(), size.height()); rect = component.modifier().foldOut( - rect, - (el, r) -> el.modifyLayout(r) + rect, + (el, r) -> el.modifyLayout(r) ); component.layout(rect.x(), rect.y(), rect.width(), rect.height()); // 4. Emit modifier render states (background, border, etc.) final LayoutRect finalRect = rect; component.modifier().foldOut( - extractor, - (el, e) -> { - el.emitRenderState(e, finalRect); - return e; - } + extractor, + (el, e) -> { + el.emitRenderState(e, finalRect); + return e; + } ); // 5. Emit component's own render states @@ -247,9 +261,9 @@ private void renderTree(UIComponent component, GuiGraphicsExtractor extractor, C * 并追踪它读取了哪些状态,以便精确标记脏。 */ public static class Slot { + final Set> readStates = new HashSet<>(); UIComponent component; boolean dirty = true; - final Set> readStates = new HashSet<>(); void addReadState(Ref state) { readStates.add(state); diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java index 180953d8..d6f2e94f 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java @@ -27,9 +27,9 @@ */ public abstract class DeclarativeScreen extends Screen { + private final UIScope rootScope = new RootScope(); @Nullable private Composition composition; - private final UIScope rootScope = new RootScope(); @Nullable private KeyInputHandler focusOwner; @@ -37,17 +37,11 @@ protected DeclarativeScreen(Component title) { super(title); } - @Override - protected void init() { - composition = new Composition(rootScope); - composition.setContent(this::content); - } - - /** 声明 UI 内容。初始组合和每次 recompose 时调用。 */ + /** + * 声明 UI 内容。初始组合和每次 recompose 时调用。 + */ protected abstract void content(UIScope scope); - // ── 每帧渲染 ── - @Override public void extractRenderState(GuiGraphicsExtractor extractor, int mouseX, int mouseY, float partialTick) { super.extractRenderState(extractor, mouseX, mouseY, partialTick); @@ -62,12 +56,41 @@ public void extractRenderState(GuiGraphicsExtractor extractor, int mouseX, int m } } + // ── 每帧渲染 ── + + @Override + public boolean keyPressed(KeyEvent event) { + if (event.key() == 256) { + onClose(); + return true; + } + if (focusOwner != null && focusOwner.onKeyPressed(event)) { + return true; + } + return super.keyPressed(event); + } + + @Override + public void onClose() { + super.onClose(); + } + + @Override + protected void init() { + composition = new Composition(rootScope); + composition.setContent(this::content); + } + private void renderPopups(UIComponent component, GuiGraphicsExtractor extractor) { if (component instanceof DropdownComponent dd) dd.renderPopup(extractor); for (UIComponent child : component.children()) renderPopups(child, extractor); } - /** recompose 后重新绑定 focusOwner(旧实例可能已被替换)。 */ + // ── 鼠标输入 ── + + /** + * recompose 后重新绑定 focusOwner(旧实例可能已被替换)。 + */ private void refreshFocus() { if (focusOwner == null) return; for (UIComponent child : rootScope.getChildren()) { @@ -89,8 +112,6 @@ private KeyInputHandler findFocused(UIComponent component) { return null; } - // ── 鼠标输入 ── - @Override public boolean mouseClicked(MouseButtonEvent event, boolean isDoubleClick) { if (event.button() == 0) { @@ -107,7 +128,10 @@ public boolean mouseClicked(MouseButtonEvent event, boolean isDoubleClick) { // 命中测试 boolean hit = false; for (UIComponent child : rootScope.getChildren()) { - if (hitTestClick(child, mx, my)) { hit = true; break; } + if (hitTestClick(child, mx, my)) { + hit = true; + break; + } } // 未命中任何 dropdown 时关闭所有 @@ -125,6 +149,16 @@ public boolean mouseClicked(MouseButtonEvent event, boolean isDoubleClick) { return super.mouseClicked(event, isDoubleClick); } + @Override + public boolean mouseReleased(MouseButtonEvent event) { + for (UIComponent child : rootScope.getChildren()) { + stopDragRecursive(child); + } + return super.mouseReleased(event); + } + + // ── 键盘输入 ── + @Override public boolean mouseDragged(MouseButtonEvent event, double deltaX, double deltaY) { var mc = Minecraft.getInstance(); @@ -136,14 +170,6 @@ public boolean mouseDragged(MouseButtonEvent event, double deltaX, double deltaY return super.mouseDragged(event, deltaX, deltaY); } - @Override - public boolean mouseReleased(MouseButtonEvent event) { - for (UIComponent child : rootScope.getChildren()) { - stopDragRecursive(child); - } - return super.mouseReleased(event); - } - @Override public boolean mouseScrolled(double mouseX, double mouseY, double scrollX, double scrollY) { for (UIComponent child : rootScope.getChildren()) { @@ -154,20 +180,6 @@ public boolean mouseScrolled(double mouseX, double mouseY, double scrollX, doubl return super.mouseScrolled(mouseX, mouseY, scrollX, scrollY); } - // ── 键盘输入 ── - - @Override - public boolean keyPressed(KeyEvent event) { - if (event.key() == 256) { - onClose(); - return true; - } - if (focusOwner != null && focusOwner.onKeyPressed(event)) { - return true; - } - return super.keyPressed(event); - } - @Override public boolean charTyped(CharacterEvent event) { if (focusOwner != null && focusOwner.onCharTyped(event)) { @@ -176,14 +188,11 @@ public boolean charTyped(CharacterEvent event) { return super.charTyped(event); } - @Override - public void onClose() { - super.onClose(); - } - // ── 命中测试 ── - /** 命中测试 + 点击触发。子组件优先。 */ + /** + * 命中测试 + 点击触发。子组件优先。 + */ private boolean hitTestClick(UIComponent component, float px, float py) { var children = component.children(); for (int i = children.size() - 1; i >= 0; i--) { @@ -207,7 +216,10 @@ private boolean hitTestClick(UIComponent component, float px, float py) { return true; } if (component instanceof DropdownComponent dd) { - if (dd.isOnPopupScrollbar(px, py)) { dd.startPopupScrollbarDrag(py); return true; } + if (dd.isOnPopupScrollbar(px, py)) { + dd.startPopupScrollbarDrag(py); + return true; + } if (dd.clickPopup(px, py)) return true; if (dd.clickTrigger(px, py)) return true; return false; @@ -219,7 +231,9 @@ private boolean hitTestClick(UIComponent component, float px, float py) { return false; } - /** 拖拽命中测试(Slider + Scrollable 滚动条)。 */ + /** + * 拖拽命中测试(Slider + Scrollable 滚动条)。 + */ private boolean hitTestDrag(UIComponent component, float px, float py) { var children = component.children(); for (int i = children.size() - 1; i >= 0; i--) { @@ -240,20 +254,26 @@ private boolean hitTestDrag(UIComponent component, float px, float py) { return false; } - /** 递归关闭所有 Dropdown。 */ + /** + * 递归关闭所有 Dropdown。 + */ private void closeDropdownsRecursive(UIComponent component) { if (component instanceof DropdownComponent dd) dd.setOpen(false); for (UIComponent child : component.children()) closeDropdownsRecursive(child); } - /** 递归停止拖拽状态。 */ + /** + * 递归停止拖拽状态。 + */ private void stopDragRecursive(UIComponent component) { if (component instanceof ScrollableComponent sc) sc.stopScrollbarDrag(); if (component instanceof DropdownComponent dd) dd.stopPopupScrollbarDrag(); for (UIComponent child : component.children()) stopDragRecursive(child); } - /** 滚轮命中测试(ScrollableComponent + Dropdown 弹出层)。 */ + /** + * 滚轮命中测试(ScrollableComponent + Dropdown 弹出层)。 + */ private boolean hitTestScroll(UIComponent component, float px, float py, float amount) { var children = component.children(); for (int i = children.size() - 1; i >= 0; i--) { @@ -263,19 +283,23 @@ private boolean hitTestScroll(UIComponent component, float px, float py, float a return sc.onScroll(amount); } if (component instanceof DropdownComponent dd && dd.open() - && dd.popupRect().contains(px, py)) { + && dd.popupRect().contains(px, py)) { return dd.onPopupScroll(amount); } return false; } - /** 清除组件树中所有 TextField 的焦点。 */ + /** + * 清除组件树中所有 TextField 的焦点。 + */ private void clearFocusRecursive(UIComponent component) { if (component instanceof TextInputComponent tf) tf.setFocused(false); for (UIComponent child : component.children()) clearFocusRecursive(child); } - /** 遍历组件树,更新 ButtonComponent 的 hover 状态。 */ + /** + * 遍历组件树,更新 ButtonComponent 的 hover 状态。 + */ private void updateHover(float mouseX, float mouseY) { for (UIComponent child : rootScope.getChildren()) { updateHoverRecursive(child, mouseX, mouseY); diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/ForEach.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/ForEach.java index 532012d2..e670db80 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/ForEach.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/ForEach.java @@ -15,9 +15,12 @@ */ public final class ForEach { - private ForEach() {} + private ForEach() { + } - /** 对列表中的每个元素执行内容函数。 */ + /** + * 对列表中的每个元素执行内容函数。 + */ public static void of(UIScope scope, List items, BiConsumer content) { for (T item : items) { content.accept(scope, item); diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/LayoutRect.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/LayoutRect.java index 4f2ec29c..d2a04f74 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/LayoutRect.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/LayoutRect.java @@ -9,9 +9,13 @@ public static LayoutRect of(float x, float y, float width, float height) { return new LayoutRect(x, y, width, height); } - public float right() { return x + width; } + public float right() { + return x + width; + } - public float bottom() { return y + height; } + public float bottom() { + return y + height; + } public boolean contains(float px, float py) { return px >= x && px < x + width && py >= y && py < y + height; diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Modifier.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Modifier.java index d53043b6..3461e2b0 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Modifier.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Modifier.java @@ -13,19 +13,38 @@ */ public interface Modifier { + Modifier NONE = new Modifier() { + @Override + public Modifier then(Modifier other) { + return other; + } + + @Override + public R foldIn(R initial, BiFunction operation) { + return initial; + } + + @Override + public R foldOut(R initial, BiFunction operation) { + return initial; + } + }; + Modifier then(Modifier other); R foldIn(R initial, BiFunction operation); R foldOut(R initial, BiFunction operation); - /** 返回一个前置了给定元素的新链。 */ + // ── 工厂快捷方法 ── + + /** + * 返回一个前置了给定元素的新链。 + */ default Modifier prepend(ModifierElement element) { return then(new SingleElementModifier(element)); } - // ── 工厂快捷方法 ── - default Modifier size(float width, float height) { return prepend(ModifierElement.size(width, height)); } @@ -69,21 +88,4 @@ default Modifier border(float width, int color) { default Modifier border(float width, int color, float round) { return prepend(ModifierElement.border(width, color, round)); } - - Modifier NONE = new Modifier() { - @Override - public Modifier then(Modifier other) { - return other; - } - - @Override - public R foldIn(R initial, BiFunction operation) { - return initial; - } - - @Override - public R foldOut(R initial, BiFunction operation) { - return initial; - } - }; } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Ref.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Ref.java index 4d2a4440..31e09a68 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Ref.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Ref.java @@ -1,11 +1,11 @@ package dev.anvilcraft.lib.v2.ui; +import org.jspecify.annotations.Nullable; + import java.util.HashSet; import java.util.Objects; import java.util.Set; -import org.jspecify.annotations.Nullable; - /** * 可观察状态持有者。 *

@@ -16,8 +16,8 @@ */ public class Ref { - private T value; final Set readers = new HashSet<>(); + private T value; public Ref(@Nullable T initialValue) { this.value = initialValue; @@ -36,7 +36,9 @@ public T getValue() { return value; } - /** 设置新值。若值发生变化,标记所有 reader slot 为脏。 */ + /** + * 设置新值。若值发生变化,标记所有 reader slot 为脏。 + */ public void setValue(@Nullable T newValue) { if (!Objects.equals(value, newValue)) { value = newValue; diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIComponent.java index b303ad7a..e6909120 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIComponent.java @@ -16,10 +16,14 @@ */ public interface UIComponent { - /** 应用于此组件的修饰符链。 */ + /** + * 应用于此组件的修饰符链。 + */ Modifier modifier(); - /** 子组件列表,叶子组件返回空列表。 */ + /** + * 子组件列表,叶子组件返回空列表。 + */ List children(); /** diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIScope.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIScope.java index a51514d7..8abf5e16 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIScope.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIScope.java @@ -1,24 +1,23 @@ package dev.anvilcraft.lib.v2.ui; +import dev.anvilcraft.lib.v2.ui.component.BoxComponent; +import dev.anvilcraft.lib.v2.ui.component.scope.BoxScope; import dev.anvilcraft.lib.v2.ui.component.ButtonComponent; import dev.anvilcraft.lib.v2.ui.component.CheckboxComponent; -import dev.anvilcraft.lib.v2.ui.component.SliderComponent; -import dev.anvilcraft.lib.v2.ui.component.TextInputComponent; -import dev.anvilcraft.lib.v2.ui.component.BoxComponent; -import dev.anvilcraft.lib.v2.ui.component.BoxScope; import dev.anvilcraft.lib.v2.ui.component.ColumnComponent; -import dev.anvilcraft.lib.v2.ui.component.ColumnScope; +import dev.anvilcraft.lib.v2.ui.component.scope.ColumnScope; import dev.anvilcraft.lib.v2.ui.component.DropdownComponent; import dev.anvilcraft.lib.v2.ui.component.GridComponent; -import dev.anvilcraft.lib.v2.ui.component.GridScope; +import dev.anvilcraft.lib.v2.ui.component.scope.GridScope; import dev.anvilcraft.lib.v2.ui.component.ImageComponent; import dev.anvilcraft.lib.v2.ui.component.RowComponent; -import dev.anvilcraft.lib.v2.ui.component.RowScope; +import dev.anvilcraft.lib.v2.ui.component.scope.RowScope; import dev.anvilcraft.lib.v2.ui.component.ScrollableComponent; -import dev.anvilcraft.lib.v2.ui.component.ScrollableScope; +import dev.anvilcraft.lib.v2.ui.component.scope.ScrollableScope; +import dev.anvilcraft.lib.v2.ui.component.SliderComponent; import dev.anvilcraft.lib.v2.ui.component.SpacerComponent; import dev.anvilcraft.lib.v2.ui.component.TextComponent; - +import dev.anvilcraft.lib.v2.ui.component.TextInputComponent; import net.minecraft.resources.Identifier; import java.util.ArrayList; @@ -26,8 +25,6 @@ import java.util.List; import java.util.function.Consumer; -import org.jspecify.annotations.Nullable; - /** * 组件树的构建作用域。 *

@@ -40,17 +37,23 @@ public abstract class UIScope { final List children = new ArrayList<>(); - /** 向当前 scope 添加一个子组件。 */ + /** + * 向当前 scope 添加一个子组件。 + */ public void addChild(UIComponent child) { children.add(child); } - /** 返回当前 scope 中已收集的子组件列表(只读)。 */ + /** + * 返回当前 scope 中已收集的子组件列表(只读)。 + */ public List getChildren() { return Collections.unmodifiableList(children); } - /** 内部方法:recompose 前清空子组件。 */ + /** + * 内部方法:recompose 前清空子组件。 + */ void clearChildren() { children.clear(); } @@ -59,6 +62,7 @@ void clearChildren() { /** * 创建一行文字。 + * * @param text 显示文本 * @return TextComponent 实例,可链式设置颜色、对齐、阴影等 */ @@ -71,7 +75,8 @@ public TextComponent Text(String text) { /** * 创建固定尺寸的空白占位。 - * @param width 宽度(像素) + * + * @param width 宽度(像素) * @param height 高度(像素) */ public SpacerComponent Spacer(float width, float height) { @@ -83,8 +88,9 @@ public SpacerComponent Spacer(float width, float height) { /** * 创建一个纹理精灵图片。 + * * @param sprite 纹理标识符 - * @param width 显示宽度 + * @param width 显示宽度 * @param height 显示高度 */ public ImageComponent Image(Identifier sprite, float width, float height) { @@ -96,7 +102,8 @@ public ImageComponent Image(Identifier sprite, float width, float height) { /** * 创建复选框。 - * @param label 标签文字 + * + * @param label 标签文字 * @param checked 初始选中状态 */ public CheckboxComponent Checkbox(String label, boolean checked) { @@ -108,7 +115,8 @@ public CheckboxComponent Checkbox(String label, boolean checked) { /** * 创建复选框(vModel 双向绑定)。 - * @param label 标签文字 + * + * @param label 标签文字 * @param vModel {@link Ref}<{@link Boolean}>,点击时自动同步值,无需手动 onToggle */ public CheckboxComponent Checkbox(String label, Ref vModel) { @@ -121,8 +129,9 @@ public CheckboxComponent Checkbox(String label, Ref vModel) { /** * 创建下拉菜单。 - * @param options 选项列表 - * @param selectedIndex 初始选中索引 + * + * @param options 选项列表 + * @param selectedIndex 初始选中索引 * @param maxPopupHeight 弹出层最大高度 */ public DropdownComponent Dropdown(String[] options, int selectedIndex, float maxPopupHeight) { @@ -134,9 +143,10 @@ public DropdownComponent Dropdown(String[] options, int selectedIndex, float max /** * 创建滑块(手动值)。 + * * @param value 初始值 - * @param min 最小值 - * @param max 最大值 + * @param min 最小值 + * @param max 最大值 * @param width 轨道宽度(像素) */ public SliderComponent Slider(float value, float min, float max, float width) { @@ -148,9 +158,10 @@ public SliderComponent Slider(float value, float min, float max, float width) { /** * 创建滑块(vModel 双向绑定)。 - * @param min 最小值 - * @param max 最大值 - * @param width 轨道宽度(像素) + * + * @param min 最小值 + * @param max 最大值 + * @param width 轨道宽度(像素) * @param vModel {@link Ref}<{@link Float}>,拖拽时自动同步值 */ public SliderComponent Slider(float min, float max, float width, Ref vModel) { @@ -163,6 +174,7 @@ public SliderComponent Slider(float min, float max, float width, Ref vMod /** * 创建单行文本输入框。 + * * @param placeholder 占位提示文字(灰色,仅在无输入时显示) */ public TextInputComponent TextInput(String placeholder) { @@ -174,8 +186,9 @@ public TextInputComponent TextInput(String placeholder) { /** * 创建单行文本输入框(vModel 双向绑定)。 + * * @param placeholder 占位提示文字 - * @param vModel {@link Ref}<{@link String}>,输入时自动同步值 + * @param vModel {@link Ref}<{@link String}>,输入时自动同步值 */ public TextInputComponent TextInput(String placeholder, Ref vModel) { TextInputComponent c = new TextInputComponent(Modifier.NONE, placeholder); @@ -187,6 +200,7 @@ public TextInputComponent TextInput(String placeholder, Ref vModel) { /** * 创建可点击按钮。 + * * @param label 按钮文字 */ public ButtonComponent Button(String label) { @@ -198,6 +212,7 @@ public ButtonComponent Button(String label) { /** * 创建纵向线性布局容器。 + * * @param content 子组件声明 lambda * @return ColumnComponent 实例,可链式设置 spacing、alignment 等 */ @@ -213,6 +228,7 @@ public ColumnComponent Column(Consumer content) { /** * 创建横向线性布局容器。 + * * @param content 子组件声明 lambda * @return RowComponent 实例,可链式设置 spacing、alignment 等 */ @@ -228,6 +244,7 @@ public RowComponent Row(Consumer content) { /** * 创建层叠布局容器。子组件按声明顺序从底到顶重叠。 + * * @param content 子组件声明 lambda */ public BoxComponent Box(Consumer content) { @@ -242,6 +259,7 @@ public BoxComponent Box(Consumer content) { /** * 创建网格布局容器。 + * * @param columns 列数 * @param content 子组件声明 lambda * @return GridComponent 实例,可链式设置 spacing @@ -258,8 +276,9 @@ public GridComponent Grid(int columns, Consumer content) { /** * 创建可滚动容器。内容超过 maxHeight 时可垂直滚动,自动裁剪并显示滚动条。 + * * @param maxHeight 容器最大可见高度(像素) - * @param content 子组件声明 lambda + * @param content 子组件声明 lambda */ public ScrollableComponent Scrollable(float maxHeight, Consumer content) { ScrollableComponent c = new ScrollableComponent(Modifier.NONE, maxHeight); diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/BoxComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/BoxComponent.java index bef97363..a7a73e78 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/BoxComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/BoxComponent.java @@ -1,10 +1,13 @@ package dev.anvilcraft.lib.v2.ui.component; +import dev.anvilcraft.lib.v2.ui.Alignment; +import dev.anvilcraft.lib.v2.ui.Constraints; +import dev.anvilcraft.lib.v2.ui.MeasuredSize; +import dev.anvilcraft.lib.v2.ui.Modifier; +import dev.anvilcraft.lib.v2.ui.UIComponent; import lombok.Getter; import lombok.Setter; import lombok.experimental.Accessors; - -import dev.anvilcraft.lib.v2.ui.*; import net.minecraft.client.gui.GuiGraphicsExtractor; import java.util.ArrayList; @@ -18,7 +21,8 @@ @Accessors(fluent = true) public class BoxComponent implements UIComponent { - @Getter @Setter + @Getter + @Setter private Modifier modifier; @Getter private List children = Collections.emptyList(); @@ -61,8 +65,8 @@ public MeasuredSize measure(Constraints constraints) { this.childSizes = sizes; return MeasuredSize.of( - constraints.constrainWidth(maxWidth), - constraints.constrainHeight(maxHeight) + constraints.constrainWidth(maxWidth), + constraints.constrainHeight(maxHeight) ); } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ButtonComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ButtonComponent.java index 7e8e00d8..1d6316fc 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ButtonComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ButtonComponent.java @@ -1,22 +1,20 @@ package dev.anvilcraft.lib.v2.ui.component; -import lombok.Getter; -import lombok.Setter; -import lombok.experimental.Accessors; - import dev.anvilcraft.lib.v2.ui.Constraints; import dev.anvilcraft.lib.v2.ui.LayoutRect; import dev.anvilcraft.lib.v2.ui.MeasuredSize; import dev.anvilcraft.lib.v2.ui.Modifier; import dev.anvilcraft.lib.v2.ui.UIComponent; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiGraphicsExtractor; +import org.jspecify.annotations.Nullable; import java.util.Collections; import java.util.List; -import org.jspecify.annotations.Nullable; - /** * 可点击按钮。原版 fill() 背景 + 手动居中 text() 文字。 */ @@ -24,13 +22,14 @@ public class ButtonComponent implements UIComponent { // 原版按钮配色 - private static final int BG_COLOR = 0xFF404040; + private static final int BG_COLOR = 0xFF404040; private static final int BG_HOVER_COLOR = 0xFF606060; - private static final int TEXT_COLOR = 0xFFFFFFFF; - private static final float PADDING_H = 12; - private static final float PADDING_V = 6; + private static final int TEXT_COLOR = 0xFFFFFFFF; + private static final float PADDING_H = 12; + private static final float PADDING_V = 6; - @Getter @Setter + @Getter + @Setter private Modifier modifier; @Setter private String label; @@ -45,10 +44,19 @@ public ButtonComponent(Modifier modifier, String label) { this.label = label; } - public ButtonComponent onClick(@Nullable Runnable onClick) { this.onClick = onClick; return this; } - public void setHovered(boolean hovered) { this.hovered = hovered; } + public ButtonComponent onClick(@Nullable Runnable onClick) { + this.onClick = onClick; + return this; + } + + public void setHovered(boolean hovered) { + this.hovered = hovered; + } - @Override public List children() { return Collections.emptyList(); } + @Override + public List children() { + return Collections.emptyList(); + } @Override public MeasuredSize measure(Constraints constraints) { @@ -56,8 +64,8 @@ public MeasuredSize measure(Constraints constraints) { float textW = font.width(label); float textH = font.lineHeight; return MeasuredSize.of( - constraints.constrainWidth(textW + PADDING_H * 2), - constraints.constrainHeight(textH + PADDING_V * 2) + constraints.constrainWidth(textW + PADDING_H * 2), + constraints.constrainHeight(textH + PADDING_V * 2) ); } @@ -87,12 +95,16 @@ public void extractRenderState(GuiGraphicsExtractor extractor) { extractor.text(font, txt, textX, textY, TEXT_COLOR, true); } - /** 按钮包围盒,用于命中测试。 */ + /** + * 按钮包围盒,用于命中测试。 + */ public LayoutRect hitRect() { return LayoutRect.of(x, y, width, height); } - /** 触发点击回调。 */ + /** + * 触发点击回调。 + */ public void click() { if (onClick != null) onClick.run(); } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/CheckboxComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/CheckboxComponent.java index 36fe8690..2bb9d57f 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/CheckboxComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/CheckboxComponent.java @@ -1,14 +1,13 @@ package dev.anvilcraft.lib.v2.ui.component; -import lombok.Getter; -import lombok.Setter; -import lombok.experimental.Accessors; - import dev.anvilcraft.lib.v2.ui.Constraints; import dev.anvilcraft.lib.v2.ui.LayoutRect; import dev.anvilcraft.lib.v2.ui.MeasuredSize; import dev.anvilcraft.lib.v2.ui.Modifier; import dev.anvilcraft.lib.v2.ui.UIComponent; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; import net.minecraft.client.gui.GuiGraphicsExtractor; import java.util.Collections; @@ -21,12 +20,13 @@ @Accessors(fluent = true) public class CheckboxComponent implements UIComponent { - private static final int BOX_COLOR = 0xFF404040; - private static final int CHECKED_COLOR = 0xFFFFFFFF; - private static final float SIZE = 16; - private static final float INSET = 3; + private static final int BOX_COLOR = 0xFF404040; + private static final int CHECKED_COLOR = 0xFFFFFFFF; + private static final float SIZE = 16; + private static final float INSET = 3; - @Getter @Setter + @Getter + @Setter private Modifier modifier; private String label; private boolean checked; @@ -41,16 +41,19 @@ public CheckboxComponent(Modifier modifier, String label, boolean checked) { this.checked = checked; } - - @Override public List children() { return Collections.emptyList(); } + + @Override + public List children() { + return Collections.emptyList(); + } @Override public MeasuredSize measure(Constraints constraints) { // 方框 + 间距 + 标签文字宽度(简化:估算每字符 7px 宽) float labelW = label.length() * 7f; return MeasuredSize.of( - constraints.constrainWidth(SIZE + 4 + labelW), - constraints.constrainHeight(SIZE) + constraints.constrainWidth(SIZE + 4 + labelW), + constraints.constrainHeight(SIZE) ); } @@ -82,13 +85,17 @@ public void extractRenderState(GuiGraphicsExtractor extractor) { // TODO: 用 font.text() 渲染标签 } - /** 切换状态。 */ + /** + * 切换状态。 + */ public void toggle() { checked = !checked; if (onToggle != null) onToggle.run(); } - /** 命中测试包围盒。 */ + /** + * 命中测试包围盒。 + */ public LayoutRect hitRect() { return LayoutRect.of(x, y, SIZE, SIZE); } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ColumnComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ColumnComponent.java index 8b280ad2..729dcf40 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ColumnComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ColumnComponent.java @@ -1,10 +1,14 @@ package dev.anvilcraft.lib.v2.ui.component; +import dev.anvilcraft.lib.v2.ui.Alignment; +import dev.anvilcraft.lib.v2.ui.Arrangement; +import dev.anvilcraft.lib.v2.ui.Constraints; +import dev.anvilcraft.lib.v2.ui.MeasuredSize; +import dev.anvilcraft.lib.v2.ui.Modifier; +import dev.anvilcraft.lib.v2.ui.UIComponent; import lombok.Getter; import lombok.Setter; import lombok.experimental.Accessors; - -import dev.anvilcraft.lib.v2.ui.*; import net.minecraft.client.gui.GuiGraphicsExtractor; import java.util.ArrayList; @@ -17,7 +21,8 @@ @Accessors(fluent = true) public class ColumnComponent implements UIComponent { - @Getter @Setter + @Getter + @Setter private Modifier modifier; @Getter private List children = Collections.emptyList(); @@ -43,10 +48,6 @@ public void setChildren(List children) { // ── 链式 setter ── - - - - public MeasuredSize measure(Constraints constraints) { if (children.isEmpty()) return MeasuredSize.ZERO; @@ -56,8 +57,8 @@ public MeasuredSize measure(Constraints constraints) { List sizes = new ArrayList<>(children.size()); Constraints childConstraints = new Constraints( - constraints.minWidth(), constraints.maxWidth(), - 0, Float.MAX_VALUE + constraints.minWidth(), constraints.maxWidth(), + 0, Float.MAX_VALUE ); for (UIComponent child : children) { @@ -70,8 +71,8 @@ public MeasuredSize measure(Constraints constraints) { this.childSizes = sizes; return MeasuredSize.of( - constraints.constrainWidth(maxWidth), - constraints.constrainHeight(totalHeight) + constraints.constrainWidth(maxWidth), + constraints.constrainHeight(totalHeight) ); } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/DropdownComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/DropdownComponent.java index 4b66291a..89c19570 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/DropdownComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/DropdownComponent.java @@ -1,19 +1,17 @@ package dev.anvilcraft.lib.v2.ui.component; -import lombok.Getter; -import lombok.Setter; -import lombok.experimental.Accessors; - import dev.anvilcraft.lib.v2.rendering.sdf.SdfGraphics; import dev.anvilcraft.lib.v2.ui.Constraints; import dev.anvilcraft.lib.v2.ui.LayoutRect; import dev.anvilcraft.lib.v2.ui.MeasuredSize; import dev.anvilcraft.lib.v2.ui.Modifier; import dev.anvilcraft.lib.v2.ui.UIComponent; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiGraphicsExtractor; import net.minecraft.util.Mth; -import org.jspecify.annotations.Nullable; import java.util.Collections; import java.util.List; @@ -26,25 +24,25 @@ @Accessors(fluent = true) public class DropdownComponent implements UIComponent { - private static final int BG_COLOR = 0xFF404040; - private static final int HOVER_COLOR = 0xFF606060; - private static final int TEXT_COLOR = 0xFFFFFFFF; - private static final int POPUP_BG = 0xFF303030; - private static final int POPUP_HOVER = 0xFF505050; + private static final int BG_COLOR = 0xFF404040; + private static final int HOVER_COLOR = 0xFF606060; + private static final int TEXT_COLOR = 0xFFFFFFFF; + private static final int POPUP_BG = 0xFF303030; + private static final int POPUP_HOVER = 0xFF505050; private static final int SCROLLBAR_COLOR = 0xFF888888; - private static final int SCROLLBAR_BG = 0xFF222222; - private static final int SCROLLBAR_W = 4; - private static final float PADDING_H = 8; - private static final float PADDING_V = 4; - private static final float ARROW_SIZE = 6; - - @Getter @Setter - private Modifier modifier; + private static final int SCROLLBAR_BG = 0xFF222222; + private static final int SCROLLBAR_W = 4; + private static final float PADDING_H = 8; + private static final float PADDING_V = 4; + private static final float ARROW_SIZE = 6; private final String[] options; + private final float maxPopupHeight; + @Getter + @Setter + private Modifier modifier; private int selectedIndex; @Getter private boolean open; - private final float maxPopupHeight; @Getter private float popupScrollY; @Setter @@ -64,12 +62,24 @@ public DropdownComponent(Modifier modifier, String[] options, int selectedIndex, this.maxPopupHeight = maxPopupHeight; } - - public String selectedOption() { return options.length > 0 ? options[selectedIndex] : ""; } - public void setOpen(boolean open) { this.open = open; if (!open) popupScrollY = 0; } - public void setPopupScrollY(float y) { this.popupScrollY = y; } - @Override public List children() { return Collections.emptyList(); } + public String selectedOption() { + return options.length > 0 ? options[selectedIndex] : ""; + } + + public void setOpen(boolean open) { + this.open = open; + if (!open) popupScrollY = 0; + } + + public void setPopupScrollY(float y) { + this.popupScrollY = y; + } + + @Override + public List children() { + return Collections.emptyList(); + } @Override public MeasuredSize measure(Constraints constraints) { @@ -83,7 +93,10 @@ public MeasuredSize measure(Constraints constraints) { @Override public void layout(float x, float y, float width, float height) { - this.x = x; this.y = y; this.width = width; this.height = height; + this.x = x; + this.y = y; + this.width = width; + this.height = height; } // ── 触发器渲染 ── @@ -110,43 +123,55 @@ private void renderTrigger(GuiGraphicsExtractor extractor) { float hs = ARROW_SIZE / 2f; if (open) { SdfGraphics.instance - .triangle(arrowCx - hs, arrowCy + hs * 0.6f, - arrowCx + hs, arrowCy + hs * 0.6f, - arrowCx, arrowCy - hs * 0.8f) - .color(TEXT_COLOR) - .fill() - .draw(extractor); + .triangle( + arrowCx - hs, arrowCy + hs * 0.6f, + arrowCx + hs, arrowCy + hs * 0.6f, + arrowCx, arrowCy - hs * 0.8f + ) + .color(TEXT_COLOR) + .fill() + .draw(extractor); } else { SdfGraphics.instance - .triangle(arrowCx - hs, arrowCy - hs * 0.6f, - arrowCx + hs, arrowCy - hs * 0.6f, - arrowCx, arrowCy + hs * 0.8f) - .color(TEXT_COLOR) - .fill() - .draw(extractor); + .triangle( + arrowCx - hs, arrowCy - hs * 0.6f, + arrowCx + hs, arrowCy - hs * 0.6f, + arrowCx, arrowCy + hs * 0.8f + ) + .color(TEXT_COLOR) + .fill() + .draw(extractor); } } // ── 弹出层渲染(延迟调用,确保 z-order) ── - /** 弹出层 item 高度。 */ + /** + * 弹出层 item 高度。 + */ public float itemHeight() { return Minecraft.getInstance().font.lineHeight + 4; } - /** 实际弹出层高度(min 内容高度, maxPopupHeight)。 */ + /** + * 实际弹出层高度(min 内容高度, maxPopupHeight)。 + */ public float popupHeight() { if (options.length == 0) return 0; return Math.min(options.length * itemHeight(), maxPopupHeight); } - /** 弹出层包围盒。 */ + /** + * 弹出层包围盒。 + */ public LayoutRect popupRect() { float ph = popupHeight(); return LayoutRect.of(x, y + height, width, ph); } - /** 延迟渲染弹出层(在所有组件之后调用)。 */ + /** + * 延迟渲染弹出层(在所有组件之后调用)。 + */ public void renderPopup(GuiGraphicsExtractor extractor) { if (!open || options.length == 0) return; @@ -189,7 +214,9 @@ public void renderPopup(GuiGraphicsExtractor extractor) { // ── 交互 ── - /** 点击触发器区域 → 切换展开。 */ + /** + * 点击触发器区域 → 切换展开。 + */ public boolean clickTrigger(float px, float py) { if (triggerRect().contains(px, py)) { open = !open; @@ -199,7 +226,9 @@ public boolean clickTrigger(float px, float py) { return false; } - /** 点击弹出层选项 → 选中并收起。返回 true 表示命中。 */ + /** + * 点击弹出层选项 → 选中并收起。返回 true 表示命中。 + */ public boolean clickPopup(float px, float py) { if (!open || options.length == 0) return false; float ph = popupHeight(); @@ -215,7 +244,9 @@ public boolean clickPopup(float px, float py) { return false; } - /** 滚轮滚动弹出层。 */ + /** + * 滚轮滚动弹出层。 + */ public boolean onPopupScroll(float amount) { if (!open) return false; float ph = popupHeight(); @@ -226,7 +257,9 @@ public boolean onPopupScroll(float amount) { return true; } - /** 弹出层滚动条命中测试。 */ + /** + * 弹出层滚动条命中测试。 + */ public boolean isOnPopupScrollbar(float mx, float my) { if (!open) return false; float ph = popupHeight(); @@ -260,8 +293,10 @@ public void onPopupScrollbarDrag(float my) { popupScrollY = -(ratio * maxScroll); } - public void stopPopupScrollbarDrag() { scrollbarDragging = false; } - + public void stopPopupScrollbarDrag() { + scrollbarDragging = false; + } + private void select(int idx) { if (idx != selectedIndex) { selectedIndex = idx; diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/GridComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/GridComponent.java index db57de44..b1244225 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/GridComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/GridComponent.java @@ -1,13 +1,12 @@ package dev.anvilcraft.lib.v2.ui.component; -import lombok.Getter; -import lombok.Setter; -import lombok.experimental.Accessors; - import dev.anvilcraft.lib.v2.ui.Constraints; import dev.anvilcraft.lib.v2.ui.MeasuredSize; import dev.anvilcraft.lib.v2.ui.Modifier; import dev.anvilcraft.lib.v2.ui.UIComponent; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; import net.minecraft.client.gui.GuiGraphicsExtractor; import java.util.ArrayList; @@ -20,10 +19,11 @@ @Accessors(fluent = true) public class GridComponent implements UIComponent { - @Getter @Setter - private Modifier modifier; private final int columns; @Getter + @Setter + private Modifier modifier; + @Getter private List children = Collections.emptyList(); private List childSizes = Collections.emptyList(); private float hSpacing, vSpacing; @@ -40,7 +40,11 @@ public void setChildren(List children) { this.children = List.copyOf(children); } - public GridComponent spacing(float h, float v) { this.hSpacing = h; this.vSpacing = v; return this; } + public GridComponent spacing(float h, float v) { + this.hSpacing = h; + this.vSpacing = v; + return this; + } public MeasuredSize measure(Constraints constraints) { if (children.isEmpty()) return MeasuredSize.ZERO; @@ -68,7 +72,10 @@ public MeasuredSize measure(Constraints constraints) { } public void layout(float x, float y, float width, float height) { - this.x = x; this.y = y; this.width = width; this.height = height; + this.x = x; + this.y = y; + this.width = width; + this.height = height; for (int i = 0; i < children.size(); i++) { int col = i % columns; diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/GridScope.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/GridScope.java deleted file mode 100644 index e63e7de1..00000000 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/GridScope.java +++ /dev/null @@ -1,7 +0,0 @@ -package dev.anvilcraft.lib.v2.ui.component; - -import dev.anvilcraft.lib.v2.ui.UIScope; - -/** {@link GridComponent} 子级作用域。 */ -public class GridScope extends UIScope { -} diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ImageComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ImageComponent.java index 16eb98fa..606a90ff 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ImageComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ImageComponent.java @@ -1,15 +1,14 @@ package dev.anvilcraft.lib.v2.ui.component; -import lombok.Getter; -import lombok.Setter; -import lombok.experimental.Accessors; - import dev.anvilcraft.lib.v2.ui.Constraints; import dev.anvilcraft.lib.v2.ui.MeasuredSize; import dev.anvilcraft.lib.v2.ui.Modifier; import dev.anvilcraft.lib.v2.ui.UIComponent; -import net.minecraft.client.renderer.RenderPipelines; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.renderer.RenderPipelines; import net.minecraft.resources.Identifier; import java.util.Collections; @@ -22,7 +21,8 @@ @Accessors(fluent = true) public class ImageComponent implements UIComponent { - @Getter @Setter + @Getter + @Setter private Modifier modifier; @Setter private Identifier sprite; @@ -40,14 +40,17 @@ public ImageComponent(Modifier modifier, Identifier sprite, float imageWidth, fl this.imageHeight = imageHeight; } - - @Override public List children() { return Collections.emptyList(); } + + @Override + public List children() { + return Collections.emptyList(); + } @Override public MeasuredSize measure(Constraints constraints) { return MeasuredSize.of( - constraints.constrainWidth(imageWidth), - constraints.constrainHeight(imageHeight) + constraints.constrainWidth(imageWidth), + constraints.constrainHeight(imageHeight) ); } @@ -62,10 +65,10 @@ public void layout(float x, float y, float width, float height) { @Override public void extractRenderState(GuiGraphicsExtractor extractor) { extractor.blitSprite( - RenderPipelines.GUI_TEXTURED, - sprite, - (int) x, (int) y, - (int) width, (int) height + RenderPipelines.GUI_TEXTURED, + sprite, + (int) x, (int) y, + (int) width, (int) height ); } } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/RowComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/RowComponent.java index 5ba0c446..e6b936a7 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/RowComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/RowComponent.java @@ -1,10 +1,14 @@ package dev.anvilcraft.lib.v2.ui.component; +import dev.anvilcraft.lib.v2.ui.Alignment; +import dev.anvilcraft.lib.v2.ui.Arrangement; +import dev.anvilcraft.lib.v2.ui.Constraints; +import dev.anvilcraft.lib.v2.ui.MeasuredSize; +import dev.anvilcraft.lib.v2.ui.Modifier; +import dev.anvilcraft.lib.v2.ui.UIComponent; import lombok.Getter; import lombok.Setter; import lombok.experimental.Accessors; - -import dev.anvilcraft.lib.v2.ui.*; import net.minecraft.client.gui.GuiGraphicsExtractor; import java.util.ArrayList; @@ -17,7 +21,8 @@ @Accessors(fluent = true) public class RowComponent implements UIComponent { - @Getter @Setter + @Getter + @Setter private Modifier modifier; @Getter private List children = Collections.emptyList(); @@ -40,10 +45,6 @@ public void setChildren(List children) { this.children = List.copyOf(children); } - - - - public MeasuredSize measure(Constraints constraints) { if (children.isEmpty()) return MeasuredSize.ZERO; @@ -53,8 +54,8 @@ public MeasuredSize measure(Constraints constraints) { List sizes = new ArrayList<>(children.size()); Constraints childConstraints = new Constraints( - 0, Float.MAX_VALUE, - constraints.minHeight(), constraints.maxHeight() + 0, Float.MAX_VALUE, + constraints.minHeight(), constraints.maxHeight() ); for (UIComponent child : children) { @@ -67,8 +68,8 @@ public MeasuredSize measure(Constraints constraints) { this.childSizes = sizes; return MeasuredSize.of( - constraints.constrainWidth(totalWidth), - constraints.constrainHeight(maxHeight) + constraints.constrainWidth(totalWidth), + constraints.constrainHeight(maxHeight) ); } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ScrollableComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ScrollableComponent.java index e483e231..3f73ac6d 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ScrollableComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ScrollableComponent.java @@ -1,14 +1,13 @@ package dev.anvilcraft.lib.v2.ui.component; -import lombok.Getter; -import lombok.Setter; -import lombok.experimental.Accessors; - import dev.anvilcraft.lib.v2.ui.Constraints; import dev.anvilcraft.lib.v2.ui.LayoutRect; import dev.anvilcraft.lib.v2.ui.MeasuredSize; import dev.anvilcraft.lib.v2.ui.Modifier; import dev.anvilcraft.lib.v2.ui.UIComponent; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; import net.minecraft.client.gui.GuiGraphicsExtractor; import net.minecraft.util.Mth; @@ -24,13 +23,13 @@ public class ScrollableComponent implements UIComponent { private static final int SCROLLBAR_COLOR = 0xFF888888; - private static final int SCROLLBAR_BG = 0xFF333333; - private static final int SCROLLBAR_W = 4; - - @Getter @Setter - private Modifier modifier; + private static final int SCROLLBAR_BG = 0xFF333333; + private static final int SCROLLBAR_W = 4; private final float maxHeight; @Getter + @Setter + private Modifier modifier; + @Getter private List children = Collections.emptyList(); private List childSizes = Collections.emptyList(); @@ -64,8 +63,10 @@ public MeasuredSize measure(Constraints constraints) { for (UIComponent child : children) { MeasuredSize s = child.measure(childC); // 修饰符会扩展尺寸(如 padding),需要计入内容高度 - s = child.modifier().foldOut(s, - (el, sz) -> el.modifyMeasuredSize(child, childC, sz)); + s = child.modifier().foldOut( + s, + (el, sz) -> el.modifyMeasuredSize(child, childC, sz) + ); sizes.add(s); totalH += s.height(); maxW = Math.max(maxW, s.width()); @@ -74,8 +75,8 @@ public MeasuredSize measure(Constraints constraints) { this.contentHeight = totalH; return MeasuredSize.of( - constraints.constrainWidth(maxW), - constraints.constrainHeight(Math.min(totalH, maxHeight)) + constraints.constrainWidth(maxW), + constraints.constrainHeight(Math.min(totalH, maxHeight)) ); } @@ -126,7 +127,9 @@ public boolean onScroll(float amount) { return true; } - /** 鼠标是否在滚动条滑块上。 */ + /** + * 鼠标是否在滚动条滑块上。 + */ public boolean isOnScrollbar(float mx, float my) { if (contentHeight <= height) return false; float bh = barH(); @@ -135,13 +138,17 @@ public boolean isOnScrollbar(float mx, float my) { return mx >= bx && mx < bx + SCROLLBAR_W && my >= by && my < by + bh; } - /** 开始拖拽滚动条。 */ + /** + * 开始拖拽滚动条。 + */ public void startScrollbarDrag(float my) { scrollbarDragging = true; dragAnchorY = my - barY(); } - /** 拖拽滚动条时更新位置。 */ + /** + * 拖拽滚动条时更新位置。 + */ public void onScrollbarDrag(float my) { if (!scrollbarDragging) return; float bh = barH(); @@ -151,12 +158,14 @@ public void onScrollbarDrag(float my) { scrollY = -(ratio * maxScroll); } - /** 停止拖拽。 */ + /** + * 停止拖拽。 + */ public void stopScrollbarDrag() { scrollbarDragging = false; } - + private float barH() { return Math.max(16, height * height / contentHeight); } @@ -166,9 +175,13 @@ private float barY() { return y + (-scrollY / maxScroll) * (height - barH()); } - public void setScrollY(float scrollY) { this.scrollY = scrollY; } + public void setScrollY(float scrollY) { + this.scrollY = scrollY; + } - /** 命中测试包围盒。 */ + /** + * 命中测试包围盒。 + */ public LayoutRect hitRect() { return LayoutRect.of(x, y, width, height); } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ScrollableScope.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ScrollableScope.java deleted file mode 100644 index b3b5cf79..00000000 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ScrollableScope.java +++ /dev/null @@ -1,7 +0,0 @@ -package dev.anvilcraft.lib.v2.ui.component; - -import dev.anvilcraft.lib.v2.ui.UIScope; - -/** {@link ScrollableComponent} 子级作用域。 */ -public class ScrollableScope extends UIScope { -} diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/SliderComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/SliderComponent.java index 804029e7..ec2ce4ae 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/SliderComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/SliderComponent.java @@ -1,14 +1,13 @@ package dev.anvilcraft.lib.v2.ui.component; -import lombok.Getter; -import lombok.Setter; -import lombok.experimental.Accessors; - import dev.anvilcraft.lib.v2.ui.Constraints; import dev.anvilcraft.lib.v2.ui.LayoutRect; import dev.anvilcraft.lib.v2.ui.MeasuredSize; import dev.anvilcraft.lib.v2.ui.Modifier; import dev.anvilcraft.lib.v2.ui.UIComponent; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; import net.minecraft.client.gui.GuiGraphicsExtractor; import net.minecraft.util.Mth; @@ -28,13 +27,13 @@ public class SliderComponent implements UIComponent { private static final float TRACK_H = 4; private static final float THUMB_W = 8; private static final float THUMB_H = 16; - - @Getter @Setter + private final float min, max; + private final float trackWidth; + @Getter + @Setter private Modifier modifier; @Getter private float value; - private final float min, max; - private final float trackWidth; @Setter private Consumer onChange; @@ -49,14 +48,17 @@ public SliderComponent(Modifier modifier, float value, float min, float max, flo this.onChange = onChange; } - - @Override public List children() { return Collections.emptyList(); } + + @Override + public List children() { + return Collections.emptyList(); + } @Override public MeasuredSize measure(Constraints constraints) { return MeasuredSize.of( - constraints.constrainWidth(trackWidth), - constraints.constrainHeight(THUMB_H) + constraints.constrainWidth(trackWidth), + constraints.constrainHeight(THUMB_H) ); } @@ -84,7 +86,9 @@ public void extractRenderState(GuiGraphicsExtractor extractor) { extractor.fill(tix, tiy, tix + (int) THUMB_W, tiy + (int) THUMB_H, THUMB_COLOR); } - /** 根据鼠标 X 坐标更新值。 */ + /** + * 根据鼠标 X 坐标更新值。 + */ public void setValueFromMouse(float mouseX) { float ratio = Mth.clamp((mouseX - x) / Math.max(width - 1, 1), 0f, 1f); float newValue = min + ratio * (max - min); @@ -94,7 +98,9 @@ public void setValueFromMouse(float mouseX) { } } - /** 命中测试包围盒(整个轨道+滑块区域)。 */ + /** + * 命中测试包围盒(整个轨道+滑块区域)。 + */ public LayoutRect hitRect() { return LayoutRect.of(x, y, width, height); } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/SpacerComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/SpacerComponent.java index cffcf9ec..ac366536 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/SpacerComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/SpacerComponent.java @@ -1,13 +1,12 @@ package dev.anvilcraft.lib.v2.ui.component; -import lombok.Getter; -import lombok.Setter; -import lombok.experimental.Accessors; - import dev.anvilcraft.lib.v2.ui.Constraints; import dev.anvilcraft.lib.v2.ui.MeasuredSize; import dev.anvilcraft.lib.v2.ui.Modifier; import dev.anvilcraft.lib.v2.ui.UIComponent; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; import net.minecraft.client.gui.GuiGraphicsExtractor; import java.util.Collections; @@ -19,10 +18,11 @@ @Accessors(fluent = true) public class SpacerComponent implements UIComponent { - @Getter @Setter - private Modifier modifier; private final float spacerWidth; private final float spacerHeight; + @Getter + @Setter + private Modifier modifier; public SpacerComponent(Modifier modifier, float width, float height) { this.modifier = modifier; @@ -30,14 +30,17 @@ public SpacerComponent(Modifier modifier, float width, float height) { this.spacerHeight = height; } - @Override public List children() { return Collections.emptyList(); } + @Override + public List children() { + return Collections.emptyList(); + } @Override public MeasuredSize measure(Constraints constraints) { return MeasuredSize.of( - constraints.constrainWidth(spacerWidth), - constraints.constrainHeight(spacerHeight) + constraints.constrainWidth(spacerWidth), + constraints.constrainHeight(spacerHeight) ); } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextComponent.java index 92347c1a..c2d24b37 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextComponent.java @@ -1,13 +1,12 @@ package dev.anvilcraft.lib.v2.ui.component; -import lombok.Getter; -import lombok.Setter; -import lombok.experimental.Accessors; - import dev.anvilcraft.lib.v2.ui.Constraints; import dev.anvilcraft.lib.v2.ui.MeasuredSize; import dev.anvilcraft.lib.v2.ui.Modifier; import dev.anvilcraft.lib.v2.ui.UIComponent; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiGraphicsExtractor; import net.minecraft.network.chat.Component; @@ -21,12 +20,9 @@ @Accessors(fluent = true) public class TextComponent implements UIComponent { - /** 文字水平对齐方式 */ - public enum Align { LEFT, CENTER, RIGHT } - private static final int VANILLA_TEXT_COLOR = 0xFFFFFFFF; - - @Getter @Setter + @Getter + @Setter private Modifier modifier; @Setter private String text; @@ -36,7 +32,6 @@ public enum Align { LEFT, CENTER, RIGHT } private boolean shadow; @Setter private Align align = Align.LEFT; - private float x, y, width, height; public TextComponent(Modifier modifier, String text) { @@ -44,8 +39,10 @@ public TextComponent(Modifier modifier, String text) { this.text = text; } - - @Override public List children() { return Collections.emptyList(); } + @Override + public List children() { + return Collections.emptyList(); + } @Override public MeasuredSize measure(Constraints constraints) { @@ -77,5 +74,10 @@ public void extractRenderState(GuiGraphicsExtractor extractor) { extractor.text(font, text, (int) renderX, (int) y, color, this.shadow); } + + /** + * 文字水平对齐方式 + */ + public enum Align {LEFT, CENTER, RIGHT} } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextInputComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextInputComponent.java index b3549b08..5c3a58c8 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextInputComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextInputComponent.java @@ -1,22 +1,19 @@ package dev.anvilcraft.lib.v2.ui.component; -import lombok.Getter; -import lombok.Setter; -import lombok.experimental.Accessors; - import dev.anvilcraft.lib.v2.ui.Constraints; import dev.anvilcraft.lib.v2.ui.LayoutRect; import dev.anvilcraft.lib.v2.ui.MeasuredSize; import dev.anvilcraft.lib.v2.ui.Modifier; import dev.anvilcraft.lib.v2.ui.UIComponent; import dev.anvilcraft.lib.v2.ui.input.KeyInputHandler; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiGraphicsExtractor; import net.minecraft.client.input.CharacterEvent; import net.minecraft.client.input.KeyEvent; -import net.minecraft.network.chat.Style; import net.minecraft.util.StringUtil; -import org.jspecify.annotations.Nullable; import java.util.Collections; import java.util.List; @@ -31,19 +28,19 @@ @Accessors(fluent = true) public class TextInputComponent implements UIComponent, KeyInputHandler { - private static final int BG_COLOR = 0xFF202020; - private static final int TEXT_COLOR = 0xFFFFFFFF; + private static final int BG_COLOR = 0xFF202020; + private static final int TEXT_COLOR = 0xFFFFFFFF; private static final int PLACEHOLDER_COLOR = 0xFF555555; - private static final int CURSOR_COLOR = 0xFFFFFFFF; - private static final float PADDING_H = 4; - private static final float PADDING_V = 4; - private static final float WIDTH = 160; - - @Getter @Setter + private static final int CURSOR_COLOR = 0xFFFFFFFF; + private static final float PADDING_H = 4; + private static final float PADDING_V = 4; + private static final float WIDTH = 160; + private final String placeholder; + @Getter + @Setter private Modifier modifier; @Getter private String value = ""; - private final String placeholder; @Getter private boolean focused; @Setter @@ -59,18 +56,30 @@ public TextInputComponent(Modifier modifier, String placeholder) { this.placeholder = placeholder != null ? placeholder : ""; } - public void setValue(String value) { this.value = value != null ? value : ""; this.cursorPos = this.value.length(); } - public void setCursorPos(int pos) { this.cursorPos = Math.clamp(pos, 0, value.length()); } - public void setFocused(boolean focused) { this.focused = focused; } - - @Override public List children() { return Collections.emptyList(); } + public void setValue(String value) { + this.value = value != null ? value : ""; + this.cursorPos = this.value.length(); + } + + public void setCursorPos(int pos) { + this.cursorPos = Math.clamp(pos, 0, value.length()); + } + + public void setFocused(boolean focused) { + this.focused = focused; + } + + @Override + public List children() { + return Collections.emptyList(); + } @Override public MeasuredSize measure(Constraints constraints) { var font = Minecraft.getInstance().font; return MeasuredSize.of( - constraints.constrainWidth(WIDTH), - constraints.constrainHeight(font.lineHeight + PADDING_V * 2) + constraints.constrainWidth(WIDTH), + constraints.constrainHeight(font.lineHeight + PADDING_V * 2) ); } @@ -109,21 +118,42 @@ public void extractRenderState(GuiGraphicsExtractor extractor) { public boolean onKeyPressed(KeyEvent event) { int key = event.key(); if (key == 259) { // 退格 - if (cursorPos > 0) { value = new StringBuilder(value).deleteCharAt(cursorPos - 1).toString(); cursorPos--; fireChange(); } + if (cursorPos > 0) { + value = new StringBuilder(value).deleteCharAt(cursorPos - 1).toString(); + cursorPos--; + fireChange(); + } return true; } if (key == 261) { // Delete - if (cursorPos < value.length()) { value = new StringBuilder(value).deleteCharAt(cursorPos).toString(); fireChange(); } + if (cursorPos < value.length()) { + value = new StringBuilder(value).deleteCharAt(cursorPos).toString(); + fireChange(); + } return true; } - if (key == 263) { if (cursorPos > 0) cursorPos--; return true; } // ← - if (key == 262) { if (cursorPos < value.length()) cursorPos++; return true; } // → - if (key == 268) { cursorPos = 0; return true; } // Home - if (key == 269) { cursorPos = value.length(); return true; } // End + if (key == 263) { + if (cursorPos > 0) cursorPos--; + return true; + } // ← + if (key == 262) { + if (cursorPos < value.length()) cursorPos++; + return true; + } // → + if (key == 268) { + cursorPos = 0; + return true; + } // Home + if (key == 269) { + cursorPos = value.length(); + return true; + } // End return false; } - /** 字符输入——支持所有语言、输入法、小键盘。参照原版 {@code EditBox.charTyped}。 */ + /** + * 字符输入——支持所有语言、输入法、小键盘。参照原版 {@code EditBox.charTyped}。 + */ @Override public boolean onCharTyped(CharacterEvent event) { if (!event.isAllowedChatCharacter()) return false; diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/BoxScope.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/scope/BoxScope.java similarity index 56% rename from module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/BoxScope.java rename to module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/scope/BoxScope.java index 3f0d5557..c2e1bfa8 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/BoxScope.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/scope/BoxScope.java @@ -1,6 +1,7 @@ -package dev.anvilcraft.lib.v2.ui.component; +package dev.anvilcraft.lib.v2.ui.component.scope; import dev.anvilcraft.lib.v2.ui.UIScope; +import dev.anvilcraft.lib.v2.ui.component.BoxComponent; /** * {@link BoxComponent} 的子级作用域。 diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ColumnScope.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/scope/ColumnScope.java similarity index 57% rename from module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ColumnScope.java rename to module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/scope/ColumnScope.java index 5c4bdbcb..e9a517cc 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ColumnScope.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/scope/ColumnScope.java @@ -1,6 +1,7 @@ -package dev.anvilcraft.lib.v2.ui.component; +package dev.anvilcraft.lib.v2.ui.component.scope; import dev.anvilcraft.lib.v2.ui.UIScope; +import dev.anvilcraft.lib.v2.ui.component.ColumnComponent; /** * {@link ColumnComponent} 的子级作用域。 diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/scope/GridScope.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/scope/GridScope.java new file mode 100644 index 00000000..3b2d9b58 --- /dev/null +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/scope/GridScope.java @@ -0,0 +1,10 @@ +package dev.anvilcraft.lib.v2.ui.component.scope; + +import dev.anvilcraft.lib.v2.ui.UIScope; +import dev.anvilcraft.lib.v2.ui.component.GridComponent; + +/** + * {@link GridComponent} 子级作用域。 + */ +public class GridScope extends UIScope { +} diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/RowScope.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/scope/RowScope.java similarity index 56% rename from module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/RowScope.java rename to module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/scope/RowScope.java index f810e42a..a97136d9 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/RowScope.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/scope/RowScope.java @@ -1,6 +1,7 @@ -package dev.anvilcraft.lib.v2.ui.component; +package dev.anvilcraft.lib.v2.ui.component.scope; import dev.anvilcraft.lib.v2.ui.UIScope; +import dev.anvilcraft.lib.v2.ui.component.RowComponent; /** * {@link RowComponent} 的子级作用域。 diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/scope/ScrollableScope.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/scope/ScrollableScope.java new file mode 100644 index 00000000..f25bf47e --- /dev/null +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/scope/ScrollableScope.java @@ -0,0 +1,10 @@ +package dev.anvilcraft.lib.v2.ui.component.scope; + +import dev.anvilcraft.lib.v2.ui.UIScope; +import dev.anvilcraft.lib.v2.ui.component.ScrollableComponent; + +/** + * {@link ScrollableComponent} 子级作用域。 + */ +public class ScrollableScope extends UIScope { +} diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/input/KeyInputHandler.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/input/KeyInputHandler.java index 41b0d3a4..55d79eeb 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/input/KeyInputHandler.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/input/KeyInputHandler.java @@ -9,7 +9,9 @@ */ public interface KeyInputHandler { - /** 控制键按下时调用。返回 true 表示已处理。 */ + /** + * 控制键按下时调用。返回 true 表示已处理。 + */ boolean onKeyPressed(KeyEvent event); /** diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/BackgroundElement.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/BackgroundElement.java index ae8019f3..25bcb66e 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/BackgroundElement.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/BackgroundElement.java @@ -4,7 +4,9 @@ import dev.anvilcraft.lib.v2.ui.LayoutRect; import net.minecraft.client.gui.GuiGraphicsExtractor; -/** 渲染填充圆角矩形背景。 */ +/** + * 渲染填充圆角矩形背景。 + */ public record BackgroundElement(int color, float round) implements ModifierElement { public BackgroundElement(int color) { @@ -14,10 +16,10 @@ public BackgroundElement(int color) { @Override public void emitRenderState(GuiGraphicsExtractor extractor, LayoutRect bounds) { SdfGraphics.instance - .box(bounds.x(), bounds.y(), bounds.width(), bounds.height()) - .color(color) - .round(round) - .fill() - .draw(extractor); + .box(bounds.x(), bounds.y(), bounds.width(), bounds.height()) + .color(color) + .round(round) + .fill() + .draw(extractor); } } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/BorderElement.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/BorderElement.java index 5f056c74..1d0b10c5 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/BorderElement.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/BorderElement.java @@ -4,7 +4,9 @@ import dev.anvilcraft.lib.v2.ui.LayoutRect; import net.minecraft.client.gui.GuiGraphicsExtractor; -/** 渲染描边圆角矩形边框。 */ +/** + * 渲染描边圆角矩形边框。 + */ public record BorderElement(float width, int color, float round) implements ModifierElement { public BorderElement(float width, int color) { @@ -14,10 +16,10 @@ public BorderElement(float width, int color) { @Override public void emitRenderState(GuiGraphicsExtractor extractor, LayoutRect bounds) { SdfGraphics.instance - .box(bounds.x(), bounds.y(), bounds.width(), bounds.height()) - .color(color) - .round(round) - .stroke(width) - .draw(extractor); + .box(bounds.x(), bounds.y(), bounds.width(), bounds.height()) + .color(color) + .round(round) + .stroke(width) + .draw(extractor); } } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/ModifierElement.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/ModifierElement.java index 7ad70e96..9abac25b 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/ModifierElement.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/ModifierElement.java @@ -12,23 +12,6 @@ */ public interface ModifierElement { - default Constraints modifyConstraints(Constraints constraints) { - return constraints; - } - - default MeasuredSize modifyMeasuredSize(UIComponent component, Constraints constraints, MeasuredSize childSize) { - return childSize; - } - - default LayoutRect modifyLayout(LayoutRect rect) { - return rect; - } - - default void emitRenderState(GuiGraphicsExtractor extractor, LayoutRect bounds) { - } - - // ── 工厂方法 ── - static ModifierElement size(float width, float height) { return new SizeElement(width, width, height, height); } @@ -45,6 +28,8 @@ static ModifierElement fillMaxWidth() { return new SizeElement(0, Float.MAX_VALUE, Float.MAX_VALUE, Float.MAX_VALUE); } + // ── 工厂方法 ── + static ModifierElement fillMaxHeight() { return new SizeElement(Float.MAX_VALUE, Float.MAX_VALUE, 0, Float.MAX_VALUE); } @@ -76,4 +61,19 @@ static ModifierElement border(float width, int color) { static ModifierElement border(float width, int color, float round) { return new BorderElement(width, color, round); } + + default Constraints modifyConstraints(Constraints constraints) { + return constraints; + } + + default MeasuredSize modifyMeasuredSize(UIComponent component, Constraints constraints, MeasuredSize childSize) { + return childSize; + } + + default LayoutRect modifyLayout(LayoutRect rect) { + return rect; + } + + default void emitRenderState(GuiGraphicsExtractor extractor, LayoutRect bounds) { + } } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/PaddingElement.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/PaddingElement.java index bcf7fc8b..2ac63005 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/PaddingElement.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/PaddingElement.java @@ -10,18 +10,18 @@ public record PaddingElement(float left, float top, float right, float bottom) i @Override public MeasuredSize modifyMeasuredSize(UIComponent component, Constraints constraints, MeasuredSize childSize) { return MeasuredSize.of( - childSize.width() + left + right, - childSize.height() + top + bottom + childSize.width() + left + right, + childSize.height() + top + bottom ); } @Override public LayoutRect modifyLayout(LayoutRect rect) { return LayoutRect.of( - rect.x() + left, - rect.y() + top, - rect.width() - left - right, - rect.height() - top - bottom + rect.x() + left, + rect.y() + top, + rect.width() - left - right, + rect.height() - top - bottom ); } } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/SizeElement.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/SizeElement.java index e41ff6bc..bf98681b 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/SizeElement.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/SizeElement.java @@ -3,7 +3,7 @@ import dev.anvilcraft.lib.v2.ui.Constraints; public record SizeElement(float minWidthHint, float maxWidthHint, float minHeightHint, float maxHeightHint) - implements ModifierElement { + implements ModifierElement { @Override public Constraints modifyConstraints(Constraints constraints) { From 84e6f19b1eeb7a3812c956260c8cfcee3c8eec0b Mon Sep 17 00:00:00 2001 From: Gugle Date: Tue, 19 May 2026 02:56:05 +0800 Subject: [PATCH 43/67] feat(ui): improve code readability by formatting comments and adjusting line breaks --- .../java/dev/anvilcraft/lib/v2/ui/UIScope.java | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIScope.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIScope.java index 8abf5e16..746c3046 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIScope.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIScope.java @@ -1,23 +1,23 @@ package dev.anvilcraft.lib.v2.ui; import dev.anvilcraft.lib.v2.ui.component.BoxComponent; -import dev.anvilcraft.lib.v2.ui.component.scope.BoxScope; import dev.anvilcraft.lib.v2.ui.component.ButtonComponent; import dev.anvilcraft.lib.v2.ui.component.CheckboxComponent; import dev.anvilcraft.lib.v2.ui.component.ColumnComponent; -import dev.anvilcraft.lib.v2.ui.component.scope.ColumnScope; import dev.anvilcraft.lib.v2.ui.component.DropdownComponent; import dev.anvilcraft.lib.v2.ui.component.GridComponent; -import dev.anvilcraft.lib.v2.ui.component.scope.GridScope; import dev.anvilcraft.lib.v2.ui.component.ImageComponent; import dev.anvilcraft.lib.v2.ui.component.RowComponent; -import dev.anvilcraft.lib.v2.ui.component.scope.RowScope; import dev.anvilcraft.lib.v2.ui.component.ScrollableComponent; -import dev.anvilcraft.lib.v2.ui.component.scope.ScrollableScope; import dev.anvilcraft.lib.v2.ui.component.SliderComponent; import dev.anvilcraft.lib.v2.ui.component.SpacerComponent; import dev.anvilcraft.lib.v2.ui.component.TextComponent; import dev.anvilcraft.lib.v2.ui.component.TextInputComponent; +import dev.anvilcraft.lib.v2.ui.component.scope.BoxScope; +import dev.anvilcraft.lib.v2.ui.component.scope.ColumnScope; +import dev.anvilcraft.lib.v2.ui.component.scope.GridScope; +import dev.anvilcraft.lib.v2.ui.component.scope.RowScope; +import dev.anvilcraft.lib.v2.ui.component.scope.ScrollableScope; import net.minecraft.resources.Identifier; import java.util.ArrayList; @@ -117,7 +117,7 @@ public CheckboxComponent Checkbox(String label, boolean checked) { * 创建复选框(vModel 双向绑定)。 * * @param label 标签文字 - * @param vModel {@link Ref}<{@link Boolean}>,点击时自动同步值,无需手动 onToggle + * @param vModel {@code Ref},点击时自动同步值,无需手动 onToggle */ public CheckboxComponent Checkbox(String label, Ref vModel) { CheckboxComponent c = new CheckboxComponent(Modifier.NONE, label, vModel.getValue()); @@ -162,7 +162,7 @@ public SliderComponent Slider(float value, float min, float max, float width) { * @param min 最小值 * @param max 最大值 * @param width 轨道宽度(像素) - * @param vModel {@link Ref}<{@link Float}>,拖拽时自动同步值 + * @param vModel {@code Ref},拖拽时自动同步值 */ public SliderComponent Slider(float min, float max, float width, Ref vModel) { SliderComponent c = new SliderComponent(Modifier.NONE, vModel.getValue(), min, max, width); @@ -188,7 +188,7 @@ public TextInputComponent TextInput(String placeholder) { * 创建单行文本输入框(vModel 双向绑定)。 * * @param placeholder 占位提示文字 - * @param vModel {@link Ref}<{@link String}>,输入时自动同步值 + * @param vModel {@code Ref},输入时自动同步值 */ public TextInputComponent TextInput(String placeholder, Ref vModel) { TextInputComponent c = new TextInputComponent(Modifier.NONE, placeholder); From abbe2f3cb41377d01b73417992d74e35d5562a7f Mon Sep 17 00:00:00 2001 From: Gugle Date: Tue, 19 May 2026 03:02:51 +0800 Subject: [PATCH 44/67] feat(ui): clean up code by removing unnecessary blank lines and improving formatting --- .../java/dev/anvilcraft/lib/v2/ui/Alignment.java | 1 - .../java/dev/anvilcraft/lib/v2/ui/Animatable.java | 10 ++-------- .../java/dev/anvilcraft/lib/v2/ui/Arrangement.java | 1 - .../java/dev/anvilcraft/lib/v2/ui/Composition.java | 13 +++++-------- .../java/dev/anvilcraft/lib/v2/ui/Constraints.java | 1 - .../dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java | 1 - .../main/java/dev/anvilcraft/lib/v2/ui/ForEach.java | 1 - .../java/dev/anvilcraft/lib/v2/ui/LayoutRect.java | 1 - .../java/dev/anvilcraft/lib/v2/ui/MeasuredSize.java | 1 - .../java/dev/anvilcraft/lib/v2/ui/Modifier.java | 1 - .../src/main/java/dev/anvilcraft/lib/v2/ui/Ref.java | 1 - .../java/dev/anvilcraft/lib/v2/ui/UIComponent.java | 1 - .../main/java/dev/anvilcraft/lib/v2/ui/UIScope.java | 1 - .../lib/v2/ui/component/BoxComponent.java | 1 - .../lib/v2/ui/component/ButtonComponent.java | 1 - .../lib/v2/ui/component/CheckboxComponent.java | 1 - .../lib/v2/ui/component/ColumnComponent.java | 1 - .../lib/v2/ui/component/DropdownComponent.java | 1 - .../lib/v2/ui/component/GridComponent.java | 1 - .../lib/v2/ui/component/ImageComponent.java | 3 +-- .../lib/v2/ui/component/RowComponent.java | 1 - .../lib/v2/ui/component/ScrollableComponent.java | 1 - .../lib/v2/ui/component/SliderComponent.java | 1 - .../lib/v2/ui/component/SpacerComponent.java | 1 - .../lib/v2/ui/component/TextComponent.java | 1 - .../lib/v2/ui/component/TextInputComponent.java | 6 +----- .../anvilcraft/lib/v2/ui/input/KeyInputHandler.java | 1 - .../lib/v2/ui/modifier/BackgroundElement.java | 1 - .../lib/v2/ui/modifier/BorderElement.java | 1 - .../lib/v2/ui/modifier/CombinedModifier.java | 1 - .../lib/v2/ui/modifier/ModifierElement.java | 1 - .../lib/v2/ui/modifier/PaddingElement.java | 1 - .../lib/v2/ui/modifier/SingleElementModifier.java | 1 - .../anvilcraft/lib/v2/ui/modifier/SizeElement.java | 1 - 34 files changed, 9 insertions(+), 53 deletions(-) diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Alignment.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Alignment.java index e0d4a53f..eeae3208 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Alignment.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Alignment.java @@ -4,7 +4,6 @@ * 子组件在交叉轴上的对齐方式。 */ public final class Alignment { - private Alignment() { } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Animatable.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Animatable.java index fe8e6e36..4cacad88 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Animatable.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Animatable.java @@ -1,5 +1,6 @@ package dev.anvilcraft.lib.v2.ui; +import lombok.Getter; import net.minecraft.util.Mth; /** @@ -7,7 +8,7 @@ * 每 tick 调用 {@link #tick()} 推进动画。 */ public class Animatable { - + @Getter private float value; private float startValue; private float targetValue; @@ -19,13 +20,6 @@ public Animatable(float initialValue) { this.targetValue = initialValue; } - /** - * 获取当前动画值。 - */ - public float getValue() { - return value; - } - /** * 直接设置值(无动画)。 */ diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Arrangement.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Arrangement.java index 30bc6207..17d60626 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Arrangement.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Arrangement.java @@ -6,7 +6,6 @@ * 子组件在主轴上的分布方式。 */ public final class Arrangement { - private Arrangement() { } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Composition.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Composition.java index c1ed8618..50751c4f 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Composition.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Composition.java @@ -3,6 +3,7 @@ import dev.anvilcraft.lib.v2.ui.component.DropdownComponent; import dev.anvilcraft.lib.v2.ui.component.ScrollableComponent; import dev.anvilcraft.lib.v2.ui.component.TextInputComponent; +import lombok.Setter; import net.minecraft.client.gui.GuiGraphicsExtractor; import org.jspecify.annotations.Nullable; @@ -26,8 +27,7 @@ * 状态读取按 slot 追踪,写入只标记受影响 slot 为脏——不会波及整棵树。 */ public class Composition { - - private static final ThreadLocal CURRENT = new ThreadLocal<>(); + private static final ThreadLocal<@Nullable Composition> CURRENT = new ThreadLocal<>(); private final List slots = new ArrayList<>(); private final Map rememberedValues = new HashMap<>(); @@ -43,8 +43,9 @@ public class Composition { private boolean dirty = true; // ── 状态 ── - private Consumer content; - private UIScope rootScope; + @Setter + private @Nullable Consumer content; + private @Nullable UIScope rootScope; public Composition(UIScope rootScope) { this.rootScope = rootScope; } @@ -68,10 +69,6 @@ public static Composition current() { return c; } - public void setContent(Consumer content) { - this.content = content; - } - /** * 标记组合需要在下一帧 recompose。 */ diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Constraints.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Constraints.java index 1a2abbc2..e716935d 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Constraints.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Constraints.java @@ -4,7 +4,6 @@ * 父容器传给子组件的 min/max 尺寸约束。 */ public record Constraints(float minWidth, float maxWidth, float minHeight, float maxHeight) { - public static final Constraints NONE = new Constraints(0, Float.MAX_VALUE, 0, Float.MAX_VALUE); public float constrainWidth(float w) { diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java index d6f2e94f..0118759f 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java @@ -26,7 +26,6 @@ * 输入事件(点击、按键、滚轮)通过命中测试路由到对应组件。 */ public abstract class DeclarativeScreen extends Screen { - private final UIScope rootScope = new RootScope(); @Nullable private Composition composition; diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/ForEach.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/ForEach.java index e670db80..03d5eafc 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/ForEach.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/ForEach.java @@ -14,7 +14,6 @@ * } */ public final class ForEach { - private ForEach() { } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/LayoutRect.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/LayoutRect.java index d2a04f74..a361117c 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/LayoutRect.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/LayoutRect.java @@ -4,7 +4,6 @@ * 布局阶段之后的定位矩形。 */ public record LayoutRect(float x, float y, float width, float height) { - public static LayoutRect of(float x, float y, float width, float height) { return new LayoutRect(x, y, width, height); } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/MeasuredSize.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/MeasuredSize.java index 60940f2e..b2f29b2d 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/MeasuredSize.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/MeasuredSize.java @@ -4,7 +4,6 @@ * 组件 measure 阶段的结果。 */ public record MeasuredSize(float width, float height) { - public static final MeasuredSize ZERO = new MeasuredSize(0, 0); public static MeasuredSize of(float width, float height) { diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Modifier.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Modifier.java index 3461e2b0..6cafc5b4 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Modifier.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Modifier.java @@ -12,7 +12,6 @@ * 调用方通过 {@link #foldIn} / {@link #foldOut} 遍历链。 */ public interface Modifier { - Modifier NONE = new Modifier() { @Override public Modifier then(Modifier other) { diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Ref.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Ref.java index 31e09a68..d07b84af 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Ref.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Ref.java @@ -15,7 +15,6 @@ * @param 持有值的类型 */ public class Ref { - final Set readers = new HashSet<>(); private T value; diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIComponent.java index e6909120..67cf0f96 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIComponent.java @@ -15,7 +15,6 @@ * */ public interface UIComponent { - /** * 应用于此组件的修饰符链。 */ diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIScope.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIScope.java index 746c3046..43eb3c8d 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIScope.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIScope.java @@ -34,7 +34,6 @@ * 组件构建器在此定义为具体方法,所有 scope 子类自动继承。 */ public abstract class UIScope { - final List children = new ArrayList<>(); /** diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/BoxComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/BoxComponent.java index a7a73e78..05140100 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/BoxComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/BoxComponent.java @@ -20,7 +20,6 @@ */ @Accessors(fluent = true) public class BoxComponent implements UIComponent { - @Getter @Setter private Modifier modifier; diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ButtonComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ButtonComponent.java index 1d6316fc..d15cb792 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ButtonComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ButtonComponent.java @@ -20,7 +20,6 @@ */ @Accessors(fluent = true) public class ButtonComponent implements UIComponent { - // 原版按钮配色 private static final int BG_COLOR = 0xFF404040; private static final int BG_HOVER_COLOR = 0xFF606060; diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/CheckboxComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/CheckboxComponent.java index 2bb9d57f..1119e238 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/CheckboxComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/CheckboxComponent.java @@ -19,7 +19,6 @@ */ @Accessors(fluent = true) public class CheckboxComponent implements UIComponent { - private static final int BOX_COLOR = 0xFF404040; private static final int CHECKED_COLOR = 0xFFFFFFFF; private static final float SIZE = 16; diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ColumnComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ColumnComponent.java index 729dcf40..e3d98941 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ColumnComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ColumnComponent.java @@ -20,7 +20,6 @@ */ @Accessors(fluent = true) public class ColumnComponent implements UIComponent { - @Getter @Setter private Modifier modifier; diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/DropdownComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/DropdownComponent.java index 89c19570..09fd5c91 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/DropdownComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/DropdownComponent.java @@ -23,7 +23,6 @@ */ @Accessors(fluent = true) public class DropdownComponent implements UIComponent { - private static final int BG_COLOR = 0xFF404040; private static final int HOVER_COLOR = 0xFF606060; private static final int TEXT_COLOR = 0xFFFFFFFF; diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/GridComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/GridComponent.java index b1244225..283d4073 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/GridComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/GridComponent.java @@ -18,7 +18,6 @@ */ @Accessors(fluent = true) public class GridComponent implements UIComponent { - private final int columns; @Getter @Setter diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ImageComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ImageComponent.java index 606a90ff..70c78579 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ImageComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ImageComponent.java @@ -16,11 +16,10 @@ /** * 渲染一个材质精灵(sprite)。 - * 通过 {@link GuiGraphicsExtractor#blitSprite} 使用原版纹理管线。 + * 通过 {@code GuiGraphicsExtractor#blitSprite} 使用原版纹理管线。 */ @Accessors(fluent = true) public class ImageComponent implements UIComponent { - @Getter @Setter private Modifier modifier; diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/RowComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/RowComponent.java index e6b936a7..770c176d 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/RowComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/RowComponent.java @@ -20,7 +20,6 @@ */ @Accessors(fluent = true) public class RowComponent implements UIComponent { - @Getter @Setter private Modifier modifier; diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ScrollableComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ScrollableComponent.java index 3f73ac6d..e4e0371f 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ScrollableComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ScrollableComponent.java @@ -21,7 +21,6 @@ */ @Accessors(fluent = true) public class ScrollableComponent implements UIComponent { - private static final int SCROLLBAR_COLOR = 0xFF888888; private static final int SCROLLBAR_BG = 0xFF333333; private static final int SCROLLBAR_W = 4; diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/SliderComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/SliderComponent.java index ec2ce4ae..e9200684 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/SliderComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/SliderComponent.java @@ -21,7 +21,6 @@ */ @Accessors(fluent = true) public class SliderComponent implements UIComponent { - private static final int TRACK_COLOR = 0xFF404040; private static final int THUMB_COLOR = 0xFFAAAAAA; private static final float TRACK_H = 4; diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/SpacerComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/SpacerComponent.java index ac366536..a8a5139e 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/SpacerComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/SpacerComponent.java @@ -17,7 +17,6 @@ */ @Accessors(fluent = true) public class SpacerComponent implements UIComponent { - private final float spacerWidth; private final float spacerHeight; @Getter diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextComponent.java index c2d24b37..227965f7 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextComponent.java @@ -19,7 +19,6 @@ */ @Accessors(fluent = true) public class TextComponent implements UIComponent { - private static final int VANILLA_TEXT_COLOR = 0xFFFFFFFF; @Getter @Setter diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextInputComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextInputComponent.java index 5c3a58c8..ea66e715 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextInputComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextInputComponent.java @@ -27,7 +27,6 @@ */ @Accessors(fluent = true) public class TextInputComponent implements UIComponent, KeyInputHandler { - private static final int BG_COLOR = 0xFF202020; private static final int TEXT_COLOR = 0xFFFFFFFF; private static final int PLACEHOLDER_COLOR = 0xFF555555; @@ -77,10 +76,7 @@ public List children() { @Override public MeasuredSize measure(Constraints constraints) { var font = Minecraft.getInstance().font; - return MeasuredSize.of( - constraints.constrainWidth(WIDTH), - constraints.constrainHeight(font.lineHeight + PADDING_V * 2) - ); + return MeasuredSize.of(constraints.constrainWidth(WIDTH), constraints.constrainHeight(font.lineHeight + PADDING_V * 2)); } @Override diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/input/KeyInputHandler.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/input/KeyInputHandler.java index 55d79eeb..9459482d 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/input/KeyInputHandler.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/input/KeyInputHandler.java @@ -8,7 +8,6 @@ * 由 {@link dev.anvilcraft.lib.v2.ui.DeclarativeScreen} 的焦点系统驱动。 */ public interface KeyInputHandler { - /** * 控制键按下时调用。返回 true 表示已处理。 */ diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/BackgroundElement.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/BackgroundElement.java index 25bcb66e..4340dc19 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/BackgroundElement.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/BackgroundElement.java @@ -8,7 +8,6 @@ * 渲染填充圆角矩形背景。 */ public record BackgroundElement(int color, float round) implements ModifierElement { - public BackgroundElement(int color) { this(color, 0); } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/BorderElement.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/BorderElement.java index 1d0b10c5..2a37eec7 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/BorderElement.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/BorderElement.java @@ -8,7 +8,6 @@ * 渲染描边圆角矩形边框。 */ public record BorderElement(float width, int color, float round) implements ModifierElement { - public BorderElement(float width, int color) { this(width, color, 0); } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/CombinedModifier.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/CombinedModifier.java index 63446fa2..8573a254 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/CombinedModifier.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/CombinedModifier.java @@ -5,7 +5,6 @@ import java.util.function.BiFunction; public record CombinedModifier(ModifierElement outer, Modifier inner) implements Modifier { - @Override public Modifier then(Modifier other) { return new CombinedModifier(outer, inner.then(other)); diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/ModifierElement.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/ModifierElement.java index 9abac25b..78cff306 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/ModifierElement.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/ModifierElement.java @@ -11,7 +11,6 @@ * 每个元素可拦截 measure、layout、render 阶段。 */ public interface ModifierElement { - static ModifierElement size(float width, float height) { return new SizeElement(width, width, height, height); } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/PaddingElement.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/PaddingElement.java index 2ac63005..8be69e9a 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/PaddingElement.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/PaddingElement.java @@ -6,7 +6,6 @@ import dev.anvilcraft.lib.v2.ui.UIComponent; public record PaddingElement(float left, float top, float right, float bottom) implements ModifierElement { - @Override public MeasuredSize modifyMeasuredSize(UIComponent component, Constraints constraints, MeasuredSize childSize) { return MeasuredSize.of( diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/SingleElementModifier.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/SingleElementModifier.java index 6f0f245f..a06587f4 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/SingleElementModifier.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/SingleElementModifier.java @@ -5,7 +5,6 @@ import java.util.function.BiFunction; public record SingleElementModifier(ModifierElement element) implements Modifier { - @Override public Modifier then(Modifier other) { return new CombinedModifier(element, other); diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/SizeElement.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/SizeElement.java index bf98681b..e1750347 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/SizeElement.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/SizeElement.java @@ -4,7 +4,6 @@ public record SizeElement(float minWidthHint, float maxWidthHint, float minHeightHint, float maxHeightHint) implements ModifierElement { - @Override public Constraints modifyConstraints(Constraints constraints) { float minW = minWidthHint > 0 ? Math.max(constraints.minWidth(), minWidthHint) : constraints.minWidth(); From fbafa9107eaf70b7fc3bb2466fe7cb490f5d16dd Mon Sep 17 00:00:00 2001 From: Gugle Date: Tue, 19 May 2026 03:10:39 +0800 Subject: [PATCH 45/67] feat(ui): enhance null safety by adding nullable annotations and adjusting component initializations --- .../dev/anvilcraft/lib/v2/ui/Composition.java | 26 ++++-- .../dev/anvilcraft/lib/v2/ui/Constraints.java | 4 +- .../lib/v2/ui/DeclarativeScreen.java | 87 ++++++++++--------- .../java/dev/anvilcraft/lib/v2/ui/Ref.java | 2 +- .../dev/anvilcraft/lib/v2/ui/UIScope.java | 4 +- .../v2/ui/component/CheckboxComponent.java | 1 + .../lib/v2/ui/component/ImageComponent.java | 2 + .../lib/v2/ui/component/SliderComponent.java | 4 +- .../lib/v2/ui/component/TextComponent.java | 2 +- .../v2/ui/component/TextInputComponent.java | 7 +- 10 files changed, 79 insertions(+), 60 deletions(-) diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Composition.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Composition.java index 50751c4f..d0a3c811 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Composition.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Composition.java @@ -3,6 +3,7 @@ import dev.anvilcraft.lib.v2.ui.component.DropdownComponent; import dev.anvilcraft.lib.v2.ui.component.ScrollableComponent; import dev.anvilcraft.lib.v2.ui.component.TextInputComponent; +import dev.anvilcraft.lib.v2.ui.modifier.ModifierElement; import lombok.Setter; import net.minecraft.client.gui.GuiGraphicsExtractor; import org.jspecify.annotations.Nullable; @@ -45,8 +46,9 @@ public class Composition { // ── 状态 ── @Setter private @Nullable Consumer content; - private @Nullable UIScope rootScope; - public Composition(UIScope rootScope) { + private final @Nullable UIScope rootScope; + + public Composition(@Nullable UIScope rootScope) { this.rootScope = rootScope; } @@ -171,8 +173,10 @@ public void renderFrame(GuiGraphicsExtractor extractor, float screenWidth, float dirty = false; } Constraints rootConstraints = new Constraints(0, screenWidth, 0, screenHeight); - for (UIComponent child : rootScope.getChildren()) { - renderTree(child, extractor, rootConstraints); + if (rootScope != null) { + for (UIComponent child : rootScope.getChildren()) { + renderTree(child, extractor, rootConstraints); + } } } finally { CURRENT.set(null); @@ -182,16 +186,20 @@ public void renderFrame(GuiGraphicsExtractor extractor, float screenWidth, float // ── recompose ── private void recompose() { - rootScope.clearChildren(); + if (rootScope != null) { + rootScope.clearChildren(); + } currentIndex = 0; currentRememberKey = 0; // recompose 前清除 slot 脏标记 for (Slot slot : slots) { slot.dirty = false; } - content.accept(rootScope); + if (content != null && rootScope != null) { + content.accept(rootScope); + } while (slots.size() > currentIndex) { - slots.remove(slots.size() - 1); + slots.removeLast(); } } @@ -229,7 +237,7 @@ private void renderTree(UIComponent component, GuiGraphicsExtractor extractor, C LayoutRect rect = LayoutRect.of(0, 0, size.width(), size.height()); rect = component.modifier().foldOut( rect, - (el, r) -> el.modifyLayout(r) + ModifierElement::modifyLayout ); component.layout(rect.x(), rect.y(), rect.width(), rect.height()); @@ -259,7 +267,7 @@ private void renderTree(UIComponent component, GuiGraphicsExtractor extractor, C */ public static class Slot { final Set> readStates = new HashSet<>(); - UIComponent component; + @Nullable UIComponent component; boolean dirty = true; void addReadState(Ref state) { diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Constraints.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Constraints.java index e716935d..1d19538e 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Constraints.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Constraints.java @@ -7,11 +7,11 @@ public record Constraints(float minWidth, float maxWidth, float minHeight, float public static final Constraints NONE = new Constraints(0, Float.MAX_VALUE, 0, Float.MAX_VALUE); public float constrainWidth(float w) { - return Math.max(minWidth, Math.min(w, maxWidth)); + return Math.clamp(w, minWidth, maxWidth); } public float constrainHeight(float h) { - return Math.max(minHeight, Math.min(h, maxHeight)); + return Math.clamp(h, minHeight, maxHeight); } public Constraints withWidth(float width) { diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java index 0118759f..542e969b 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java @@ -39,7 +39,7 @@ protected DeclarativeScreen(Component title) { /** * 声明 UI 内容。初始组合和每次 recompose 时调用。 */ - protected abstract void content(UIScope scope); + protected abstract void content(@Nullable UIScope scope); @Override public void extractRenderState(GuiGraphicsExtractor extractor, int mouseX, int mouseY, float partialTick) { @@ -102,7 +102,7 @@ private void refreshFocus() { focusOwner = null; } - private KeyInputHandler findFocused(UIComponent component) { + private @Nullable KeyInputHandler findFocused(UIComponent component) { if (component instanceof TextInputComponent tf && tf.focused()) return tf; for (UIComponent child : component.children()) { KeyInputHandler found = findFocused(child); @@ -197,35 +197,38 @@ private boolean hitTestClick(UIComponent component, float px, float py) { for (int i = children.size() - 1; i >= 0; i--) { if (hitTestClick(children.get(i), px, py)) return true; } - if (component instanceof ButtonComponent btn && btn.hitRect().contains(px, py)) { - btn.click(); - return true; - } - if (component instanceof CheckboxComponent cb && cb.hitRect().contains(px, py)) { - cb.toggle(); - return true; - } - if (component instanceof SliderComponent sl && sl.hitRect().contains(px, py)) { - sl.setValueFromMouse(px); - return true; - } - if (component instanceof TextInputComponent tf && tf.hitRect().contains(px, py)) { - tf.setFocused(true); - focusOwner = tf; - return true; - } - if (component instanceof DropdownComponent dd) { - if (dd.isOnPopupScrollbar(px, py)) { - dd.startPopupScrollbarDrag(py); + switch (component) { + case ButtonComponent btn when btn.hitRect().contains(px, py) -> { + btn.click(); return true; } - if (dd.clickPopup(px, py)) return true; - if (dd.clickTrigger(px, py)) return true; - return false; - } - if (component instanceof ScrollableComponent sc && sc.isOnScrollbar(px, py)) { - sc.startScrollbarDrag(py); - return true; + case CheckboxComponent cb when cb.hitRect().contains(px, py) -> { + cb.toggle(); + return true; + } + case SliderComponent sl when sl.hitRect().contains(px, py) -> { + sl.setValueFromMouse(px); + return true; + } + case TextInputComponent tf when tf.hitRect().contains(px, py) -> { + tf.setFocused(true); + focusOwner = tf; + return true; + } + case DropdownComponent dd -> { + if (dd.isOnPopupScrollbar(px, py)) { + dd.startPopupScrollbarDrag(py); + return true; + } + if (dd.clickPopup(px, py)) return true; + return dd.clickTrigger(px, py); + } + case ScrollableComponent sc when sc.isOnScrollbar(px, py) -> { + sc.startScrollbarDrag(py); + return true; + } + default -> { + } } return false; } @@ -238,17 +241,21 @@ private boolean hitTestDrag(UIComponent component, float px, float py) { for (int i = children.size() - 1; i >= 0; i--) { if (hitTestDrag(children.get(i), px, py)) return true; } - if (component instanceof SliderComponent sl && sl.hitRect().contains(px, py)) { - sl.setValueFromMouse(px); - return true; - } - if (component instanceof ScrollableComponent sc && sc.scrollbarDragging()) { - sc.onScrollbarDrag(py); - return true; - } - if (component instanceof DropdownComponent dd && dd.scrollbarDragging()) { - dd.onPopupScrollbarDrag(py); - return true; + switch (component) { + case SliderComponent sl when sl.hitRect().contains(px, py) -> { + sl.setValueFromMouse(px); + return true; + } + case ScrollableComponent sc when sc.scrollbarDragging() -> { + sc.onScrollbarDrag(py); + return true; + } + case DropdownComponent dd when dd.scrollbarDragging() -> { + dd.onPopupScrollbarDrag(py); + return true; + } + default -> { + } } return false; } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Ref.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Ref.java index d07b84af..1667be2e 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Ref.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Ref.java @@ -16,7 +16,7 @@ */ public class Ref { final Set readers = new HashSet<>(); - private T value; + private @Nullable T value; public Ref(@Nullable T initialValue) { this.value = initialValue; diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIScope.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIScope.java index 43eb3c8d..b30d4ade 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIScope.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIScope.java @@ -119,7 +119,7 @@ public CheckboxComponent Checkbox(String label, boolean checked) { * @param vModel {@code Ref},点击时自动同步值,无需手动 onToggle */ public CheckboxComponent Checkbox(String label, Ref vModel) { - CheckboxComponent c = new CheckboxComponent(Modifier.NONE, label, vModel.getValue()); + CheckboxComponent c = new CheckboxComponent(Modifier.NONE, label, vModel.getValue() != null && vModel.getValue()); c.onToggle(() -> vModel.setValue(!vModel.getValue())); addChild(c); Composition.current().emit(c); @@ -164,7 +164,7 @@ public SliderComponent Slider(float value, float min, float max, float width) { * @param vModel {@code Ref},拖拽时自动同步值 */ public SliderComponent Slider(float min, float max, float width, Ref vModel) { - SliderComponent c = new SliderComponent(Modifier.NONE, vModel.getValue(), min, max, width); + SliderComponent c = new SliderComponent(Modifier.NONE, vModel.getValue() == null ? 0 : vModel.getValue(), min, max, width); c.onChange(vModel::setValue); addChild(c); Composition.current().emit(c); diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/CheckboxComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/CheckboxComponent.java index 1119e238..06b3282d 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/CheckboxComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/CheckboxComponent.java @@ -27,6 +27,7 @@ public class CheckboxComponent implements UIComponent { @Getter @Setter private Modifier modifier; + @Setter private String label; private boolean checked; @Setter diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ImageComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ImageComponent.java index 70c78579..d5fb38b0 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ImageComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ImageComponent.java @@ -26,8 +26,10 @@ public class ImageComponent implements UIComponent { @Setter private Identifier sprite; @Getter + @Setter private float imageWidth; @Getter + @Setter private float imageHeight; private float x, y, width, height; diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/SliderComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/SliderComponent.java index e9200684..ef1f6608 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/SliderComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/SliderComponent.java @@ -10,6 +10,7 @@ import lombok.experimental.Accessors; import net.minecraft.client.gui.GuiGraphicsExtractor; import net.minecraft.util.Mth; +import org.jspecify.annotations.Nullable; import java.util.Collections; import java.util.List; @@ -34,7 +35,7 @@ public class SliderComponent implements UIComponent { @Getter private float value; @Setter - private Consumer onChange; + private @Nullable Consumer onChange; private float x, y, width, height; @@ -44,7 +45,6 @@ public SliderComponent(Modifier modifier, float value, float min, float max, flo this.min = min; this.max = max; this.trackWidth = trackWidth; - this.onChange = onChange; } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextComponent.java index 227965f7..d0980dd9 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextComponent.java @@ -65,7 +65,7 @@ public void extractRenderState(GuiGraphicsExtractor extractor) { Component component = Component.literal(text); float textW = font.width(component); - float renderX = (float) switch (align) { + float renderX = switch (align) { case LEFT -> x; case CENTER -> x + (width - textW) / 2f; case RIGHT -> x + width - textW; diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextInputComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextInputComponent.java index ea66e715..8cb771ac 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextInputComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextInputComponent.java @@ -14,6 +14,7 @@ import net.minecraft.client.input.CharacterEvent; import net.minecraft.client.input.KeyEvent; import net.minecraft.util.StringUtil; +import org.jspecify.annotations.Nullable; import java.util.Collections; import java.util.List; @@ -43,19 +44,19 @@ public class TextInputComponent implements UIComponent, KeyInputHandler { @Getter private boolean focused; @Setter - private Consumer onChange; + private @Nullable Consumer onChange; private int displayPos; @Getter private int cursorPos; private float x, y, width, height; - public TextInputComponent(Modifier modifier, String placeholder) { + public TextInputComponent(Modifier modifier, @Nullable String placeholder) { this.modifier = modifier; this.placeholder = placeholder != null ? placeholder : ""; } - public void setValue(String value) { + public void setValue(@Nullable String value) { this.value = value != null ? value : ""; this.cursorPos = this.value.length(); } From 1a26c80055c83f1682a5929fac66f7201f00192a Mon Sep 17 00:00:00 2001 From: Gugle Date: Tue, 19 May 2026 03:15:00 +0800 Subject: [PATCH 46/67] feat(ui): add @SuppressWarnings annotations to suppress unused variable warnings --- .../src/main/java/dev/anvilcraft/lib/v2/ui/Alignment.java | 6 ++++++ .../main/java/dev/anvilcraft/lib/v2/ui/Animatable.java | 6 ++++++ .../main/java/dev/anvilcraft/lib/v2/ui/AnvilLibUi.java | 6 ++++++ .../main/java/dev/anvilcraft/lib/v2/ui/Arrangement.java | 6 ++++++ .../main/java/dev/anvilcraft/lib/v2/ui/Composition.java | 6 ++++++ .../main/java/dev/anvilcraft/lib/v2/ui/Constraints.java | 6 ++++++ .../java/dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java | 6 ++++++ .../src/main/java/dev/anvilcraft/lib/v2/ui/ForEach.java | 6 ++++++ .../main/java/dev/anvilcraft/lib/v2/ui/LayoutRect.java | 6 ++++++ .../main/java/dev/anvilcraft/lib/v2/ui/MeasuredSize.java | 6 ++++++ .../src/main/java/dev/anvilcraft/lib/v2/ui/Modifier.java | 6 ++++++ module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Ref.java | 6 ++++++ .../main/java/dev/anvilcraft/lib/v2/ui/UIComponent.java | 6 ++++++ .../src/main/java/dev/anvilcraft/lib/v2/ui/UIScope.java | 6 ++++++ .../dev/anvilcraft/lib/v2/ui/component/BoxComponent.java | 7 +++++++ .../anvilcraft/lib/v2/ui/component/ButtonComponent.java | 7 +++++++ .../anvilcraft/lib/v2/ui/component/CheckboxComponent.java | 7 +++++++ .../anvilcraft/lib/v2/ui/component/ColumnComponent.java | 7 +++++++ .../anvilcraft/lib/v2/ui/component/DropdownComponent.java | 7 +++++++ .../dev/anvilcraft/lib/v2/ui/component/GridComponent.java | 8 ++++++++ .../anvilcraft/lib/v2/ui/component/ImageComponent.java | 7 +++++++ .../dev/anvilcraft/lib/v2/ui/component/RowComponent.java | 7 +++++++ .../lib/v2/ui/component/ScrollableComponent.java | 7 +++++++ .../anvilcraft/lib/v2/ui/component/SliderComponent.java | 7 +++++++ .../anvilcraft/lib/v2/ui/component/SpacerComponent.java | 6 ++++++ .../dev/anvilcraft/lib/v2/ui/component/TextComponent.java | 8 ++++++++ .../lib/v2/ui/component/TextInputComponent.java | 7 +++++++ .../anvilcraft/lib/v2/ui/component/scope/BoxScope.java | 6 ++++++ .../anvilcraft/lib/v2/ui/component/scope/ColumnScope.java | 6 ++++++ .../anvilcraft/lib/v2/ui/component/scope/GridScope.java | 6 ++++++ .../anvilcraft/lib/v2/ui/component/scope/RowScope.java | 6 ++++++ .../lib/v2/ui/component/scope/ScrollableScope.java | 6 ++++++ .../dev/anvilcraft/lib/v2/ui/input/KeyInputHandler.java | 6 ++++++ .../anvilcraft/lib/v2/ui/modifier/BackgroundElement.java | 6 ++++++ .../dev/anvilcraft/lib/v2/ui/modifier/BorderElement.java | 6 ++++++ .../anvilcraft/lib/v2/ui/modifier/CombinedModifier.java | 6 ++++++ .../anvilcraft/lib/v2/ui/modifier/ModifierElement.java | 6 ++++++ .../dev/anvilcraft/lib/v2/ui/modifier/PaddingElement.java | 6 ++++++ .../lib/v2/ui/modifier/SingleElementModifier.java | 6 ++++++ .../dev/anvilcraft/lib/v2/ui/modifier/SizeElement.java | 6 ++++++ 40 files changed, 254 insertions(+) diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Alignment.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Alignment.java index eeae3208..c33eff11 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Alignment.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Alignment.java @@ -3,6 +3,12 @@ /** * 子组件在交叉轴上的对齐方式。 */ +@SuppressWarnings( + { + "unused", + "UnusedReturnValue" + } +) public final class Alignment { private Alignment() { } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Animatable.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Animatable.java index 4cacad88..d94dea1b 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Animatable.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Animatable.java @@ -7,6 +7,12 @@ * 基于游戏 tick 的动画值。从当前值平滑过渡到目标值。 * 每 tick 调用 {@link #tick()} 推进动画。 */ +@SuppressWarnings( + { + "unused", + "UnusedReturnValue" + } +) public class Animatable { @Getter private float value; diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/AnvilLibUi.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/AnvilLibUi.java index 47a2d9e2..c40ae3a6 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/AnvilLibUi.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/AnvilLibUi.java @@ -4,6 +4,12 @@ import net.neoforged.fml.common.Mod; @Mod(AnvilLibUi.MOD_ID) +@SuppressWarnings( + { + "unused", + "UnusedReturnValue" + } +) public class AnvilLibUi { public static final String MOD_ID = "anvillib_ui"; diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Arrangement.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Arrangement.java index 17d60626..f506c5c0 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Arrangement.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Arrangement.java @@ -5,6 +5,12 @@ /** * 子组件在主轴上的分布方式。 */ +@SuppressWarnings( + { + "unused", + "UnusedReturnValue" + } +) public final class Arrangement { private Arrangement() { } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Composition.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Composition.java index d0a3c811..75f64377 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Composition.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Composition.java @@ -27,6 +27,12 @@ *

* 状态读取按 slot 追踪,写入只标记受影响 slot 为脏——不会波及整棵树。 */ +@SuppressWarnings( + { + "unused", + "UnusedReturnValue" + } +) public class Composition { private static final ThreadLocal<@Nullable Composition> CURRENT = new ThreadLocal<>(); private final List slots = new ArrayList<>(); diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Constraints.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Constraints.java index 1d19538e..8dbf2db6 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Constraints.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Constraints.java @@ -3,6 +3,12 @@ /** * 父容器传给子组件的 min/max 尺寸约束。 */ +@SuppressWarnings( + { + "unused", + "UnusedReturnValue" + } +) public record Constraints(float minWidth, float maxWidth, float minHeight, float maxHeight) { public static final Constraints NONE = new Constraints(0, Float.MAX_VALUE, 0, Float.MAX_VALUE); diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java index 542e969b..131a648c 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java @@ -25,6 +25,12 @@ * 每帧自动完成:dirty check → recompose → measure → layout → render states。 * 输入事件(点击、按键、滚轮)通过命中测试路由到对应组件。 */ +@SuppressWarnings( + { + "unused", + "UnusedReturnValue" + } +) public abstract class DeclarativeScreen extends Screen { private final UIScope rootScope = new RootScope(); @Nullable diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/ForEach.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/ForEach.java index 03d5eafc..4c7640f9 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/ForEach.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/ForEach.java @@ -13,6 +13,12 @@ * }); * } */ +@SuppressWarnings( + { + "unused", + "UnusedReturnValue" + } +) public final class ForEach { private ForEach() { } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/LayoutRect.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/LayoutRect.java index a361117c..b2bd2634 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/LayoutRect.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/LayoutRect.java @@ -3,6 +3,12 @@ /** * 布局阶段之后的定位矩形。 */ +@SuppressWarnings( + { + "unused", + "UnusedReturnValue" + } +) public record LayoutRect(float x, float y, float width, float height) { public static LayoutRect of(float x, float y, float width, float height) { return new LayoutRect(x, y, width, height); diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/MeasuredSize.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/MeasuredSize.java index b2f29b2d..88afe0aa 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/MeasuredSize.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/MeasuredSize.java @@ -3,6 +3,12 @@ /** * 组件 measure 阶段的结果。 */ +@SuppressWarnings( + { + "unused", + "UnusedReturnValue" + } +) public record MeasuredSize(float width, float height) { public static final MeasuredSize ZERO = new MeasuredSize(0, 0); diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Modifier.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Modifier.java index 6cafc5b4..b5fc3c75 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Modifier.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Modifier.java @@ -11,6 +11,12 @@ * 每个修饰符元素可参与 measure、layout、render 阶段。 * 调用方通过 {@link #foldIn} / {@link #foldOut} 遍历链。 */ +@SuppressWarnings( + { + "unused", + "UnusedReturnValue" + } +) public interface Modifier { Modifier NONE = new Modifier() { @Override diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Ref.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Ref.java index 1667be2e..b320ea19 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Ref.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Ref.java @@ -14,6 +14,12 @@ * * @param 持有值的类型 */ +@SuppressWarnings( + { + "unused", + "UnusedReturnValue" + } +) public class Ref { final Set readers = new HashSet<>(); private @Nullable T value; diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIComponent.java index 67cf0f96..d44ccb16 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIComponent.java @@ -14,6 +14,12 @@ *

  • {@link #extractRenderState(GuiGraphicsExtractor)} — 提交渲染状态给 GPU
  • * */ +@SuppressWarnings( + { + "unused", + "UnusedReturnValue" + } +) public interface UIComponent { /** * 应用于此组件的修饰符链。 diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIScope.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIScope.java index b30d4ade..b24234de 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIScope.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIScope.java @@ -33,6 +33,12 @@ *

    * 组件构建器在此定义为具体方法,所有 scope 子类自动继承。 */ +@SuppressWarnings( + { + "unused", + "UnusedReturnValue" + } +) public abstract class UIScope { final List children = new ArrayList<>(); diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/BoxComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/BoxComponent.java index 05140100..a2d426ee 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/BoxComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/BoxComponent.java @@ -19,6 +19,12 @@ * Box 本身的大小由最大的子组件决定。 */ @Accessors(fluent = true) +@SuppressWarnings( + { + "unused", + "UnusedReturnValue" + } +) public class BoxComponent implements UIComponent { @Getter @Setter @@ -30,6 +36,7 @@ public class BoxComponent implements UIComponent { private Alignment.Horizontal contentAlignmentH = Alignment.Horizontal.Start; private Alignment.Vertical contentAlignmentV = Alignment.Vertical.Top; + @Getter private float x, y, width, height; public BoxComponent(Modifier modifier) { diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ButtonComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ButtonComponent.java index d15cb792..627bd3f6 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ButtonComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ButtonComponent.java @@ -19,6 +19,12 @@ * 可点击按钮。原版 fill() 背景 + 手动居中 text() 文字。 */ @Accessors(fluent = true) +@SuppressWarnings( + { + "unused", + "UnusedReturnValue" + } +) public class ButtonComponent implements UIComponent { // 原版按钮配色 private static final int BG_COLOR = 0xFF404040; @@ -36,6 +42,7 @@ public class ButtonComponent implements UIComponent { private Runnable onClick; private boolean hovered; + @Getter private float x, y, width, height; public ButtonComponent(Modifier modifier, String label) { diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/CheckboxComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/CheckboxComponent.java index 06b3282d..3fbd88a2 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/CheckboxComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/CheckboxComponent.java @@ -18,6 +18,12 @@ * 16x16 方框,未选中=深色空心,选中=浅色填充。 */ @Accessors(fluent = true) +@SuppressWarnings( + { + "unused", + "UnusedReturnValue" + } +) public class CheckboxComponent implements UIComponent { private static final int BOX_COLOR = 0xFF404040; private static final int CHECKED_COLOR = 0xFFFFFFFF; @@ -33,6 +39,7 @@ public class CheckboxComponent implements UIComponent { @Setter private Runnable onToggle; + @Getter private float x, y, width, height; public CheckboxComponent(Modifier modifier, String label, boolean checked) { diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ColumnComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ColumnComponent.java index e3d98941..f81e883f 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ColumnComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ColumnComponent.java @@ -19,6 +19,12 @@ * 纵向线性布局。子组件自上而下排列。主轴=垂直,交叉轴=水平。 */ @Accessors(fluent = true) +@SuppressWarnings( + { + "unused", + "UnusedReturnValue" + } +) public class ColumnComponent implements UIComponent { @Getter @Setter @@ -35,6 +41,7 @@ public class ColumnComponent implements UIComponent { private float spacing; // 布局状态 + @Getter private float x, y, width, height; public ColumnComponent(Modifier modifier) { diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/DropdownComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/DropdownComponent.java index 09fd5c91..f11ae606 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/DropdownComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/DropdownComponent.java @@ -22,6 +22,12 @@ * 弹出层延迟渲染以确保 z-order 正确,支持最大高度 + 滚动。 */ @Accessors(fluent = true) +@SuppressWarnings( + { + "unused", + "UnusedReturnValue" + } +) public class DropdownComponent implements UIComponent { private static final int BG_COLOR = 0xFF404040; private static final int HOVER_COLOR = 0xFF606060; @@ -52,6 +58,7 @@ public class DropdownComponent implements UIComponent { private boolean scrollbarDragging; private float dragAnchorY; + @Getter private float x, y, width, height; public DropdownComponent(Modifier modifier, String[] options, int selectedIndex, float maxPopupHeight) { diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/GridComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/GridComponent.java index 283d4073..abe31776 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/GridComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/GridComponent.java @@ -17,6 +17,12 @@ * 网格布局。子组件按列数排列,每格大小由最大子组件决定。 */ @Accessors(fluent = true) +@SuppressWarnings( + { + "unused", + "UnusedReturnValue" + } +) public class GridComponent implements UIComponent { private final int columns; @Getter @@ -24,9 +30,11 @@ public class GridComponent implements UIComponent { private Modifier modifier; @Getter private List children = Collections.emptyList(); + @SuppressWarnings("FieldCanBeLocal") private List childSizes = Collections.emptyList(); private float hSpacing, vSpacing; + @Getter private float x, y, width, height; private float cellW, cellH; diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ImageComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ImageComponent.java index d5fb38b0..4591f699 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ImageComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ImageComponent.java @@ -19,6 +19,12 @@ * 通过 {@code GuiGraphicsExtractor#blitSprite} 使用原版纹理管线。 */ @Accessors(fluent = true) +@SuppressWarnings( + { + "unused", + "UnusedReturnValue" + } +) public class ImageComponent implements UIComponent { @Getter @Setter @@ -32,6 +38,7 @@ public class ImageComponent implements UIComponent { @Setter private float imageHeight; + @Getter private float x, y, width, height; public ImageComponent(Modifier modifier, Identifier sprite, float imageWidth, float imageHeight) { diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/RowComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/RowComponent.java index 770c176d..a02c2e18 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/RowComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/RowComponent.java @@ -19,6 +19,12 @@ * 横向线性布局。子组件自左而右排列。主轴=水平,交叉轴=垂直。 */ @Accessors(fluent = true) +@SuppressWarnings( + { + "unused", + "UnusedReturnValue" + } +) public class RowComponent implements UIComponent { @Getter @Setter @@ -34,6 +40,7 @@ public class RowComponent implements UIComponent { @Setter private float spacing; + @Getter private float x, y, width, height; public RowComponent(Modifier modifier) { diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ScrollableComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ScrollableComponent.java index e4e0371f..7476de50 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ScrollableComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ScrollableComponent.java @@ -20,6 +20,12 @@ * 渲染时自动裁剪,并绘制滚动条。 */ @Accessors(fluent = true) +@SuppressWarnings( + { + "unused", + "UnusedReturnValue" + } +) public class ScrollableComponent implements UIComponent { private static final int SCROLLBAR_COLOR = 0xFF888888; private static final int SCROLLBAR_BG = 0xFF333333; @@ -39,6 +45,7 @@ public class ScrollableComponent implements UIComponent { private boolean scrollbarDragging; private float dragAnchorY; + @Getter private float x, y, width, height; public ScrollableComponent(Modifier modifier, float maxHeight) { diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/SliderComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/SliderComponent.java index ef1f6608..985fd3b2 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/SliderComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/SliderComponent.java @@ -21,6 +21,12 @@ * 原版风格:深色轨道 + 浅色滑块按钮。 */ @Accessors(fluent = true) +@SuppressWarnings( + { + "unused", + "UnusedReturnValue" + } +) public class SliderComponent implements UIComponent { private static final int TRACK_COLOR = 0xFF404040; private static final int THUMB_COLOR = 0xFFAAAAAA; @@ -37,6 +43,7 @@ public class SliderComponent implements UIComponent { @Setter private @Nullable Consumer onChange; + @Getter private float x, y, width, height; public SliderComponent(Modifier modifier, float value, float min, float max, float trackWidth) { diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/SpacerComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/SpacerComponent.java index a8a5139e..340a255e 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/SpacerComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/SpacerComponent.java @@ -16,6 +16,12 @@ * 固定尺寸的空白占位组件,不渲染任何内容。 */ @Accessors(fluent = true) +@SuppressWarnings( + { + "unused", + "UnusedReturnValue" + } +) public class SpacerComponent implements UIComponent { private final float spacerWidth; private final float spacerHeight; diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextComponent.java index d0980dd9..63914af9 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextComponent.java @@ -18,6 +18,12 @@ * 单行文字渲染。默认样式与原版一致:白色带阴影、左对齐。 */ @Accessors(fluent = true) +@SuppressWarnings( + { + "unused", + "UnusedReturnValue" + } +) public class TextComponent implements UIComponent { private static final int VANILLA_TEXT_COLOR = 0xFFFFFFFF; @Getter @@ -31,6 +37,8 @@ public class TextComponent implements UIComponent { private boolean shadow; @Setter private Align align = Align.LEFT; + + @Getter private float x, y, width, height; public TextComponent(Modifier modifier, String text) { diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextInputComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextInputComponent.java index 8cb771ac..38c5a102 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextInputComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextInputComponent.java @@ -27,6 +27,12 @@ * 字符输入通过 {@link CharacterEvent} 处理,支持所有语言和输入法。 */ @Accessors(fluent = true) +@SuppressWarnings( + { + "unused", + "UnusedReturnValue" + } +) public class TextInputComponent implements UIComponent, KeyInputHandler { private static final int BG_COLOR = 0xFF202020; private static final int TEXT_COLOR = 0xFFFFFFFF; @@ -49,6 +55,7 @@ public class TextInputComponent implements UIComponent, KeyInputHandler { @Getter private int cursorPos; + @Getter private float x, y, width, height; public TextInputComponent(Modifier modifier, @Nullable String placeholder) { diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/scope/BoxScope.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/scope/BoxScope.java index c2e1bfa8..62b6ab24 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/scope/BoxScope.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/scope/BoxScope.java @@ -6,5 +6,11 @@ /** * {@link BoxComponent} 的子级作用域。 */ +@SuppressWarnings( + { + "unused", + "UnusedReturnValue" + } +) public class BoxScope extends UIScope { } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/scope/ColumnScope.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/scope/ColumnScope.java index e9a517cc..86e4ca86 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/scope/ColumnScope.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/scope/ColumnScope.java @@ -6,5 +6,11 @@ /** * {@link ColumnComponent} 的子级作用域。 */ +@SuppressWarnings( + { + "unused", + "UnusedReturnValue" + } +) public class ColumnScope extends UIScope { } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/scope/GridScope.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/scope/GridScope.java index 3b2d9b58..a744ff91 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/scope/GridScope.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/scope/GridScope.java @@ -6,5 +6,11 @@ /** * {@link GridComponent} 子级作用域。 */ +@SuppressWarnings( + { + "unused", + "UnusedReturnValue" + } +) public class GridScope extends UIScope { } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/scope/RowScope.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/scope/RowScope.java index a97136d9..b146a7ee 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/scope/RowScope.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/scope/RowScope.java @@ -6,5 +6,11 @@ /** * {@link RowComponent} 的子级作用域。 */ +@SuppressWarnings( + { + "unused", + "UnusedReturnValue" + } +) public class RowScope extends UIScope { } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/scope/ScrollableScope.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/scope/ScrollableScope.java index f25bf47e..fe8d6841 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/scope/ScrollableScope.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/scope/ScrollableScope.java @@ -6,5 +6,11 @@ /** * {@link ScrollableComponent} 子级作用域。 */ +@SuppressWarnings( + { + "unused", + "UnusedReturnValue" + } +) public class ScrollableScope extends UIScope { } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/input/KeyInputHandler.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/input/KeyInputHandler.java index 9459482d..ea79ba16 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/input/KeyInputHandler.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/input/KeyInputHandler.java @@ -7,6 +7,12 @@ * 可接收键盘输入的组件接口。 * 由 {@link dev.anvilcraft.lib.v2.ui.DeclarativeScreen} 的焦点系统驱动。 */ +@SuppressWarnings( + { + "unused", + "UnusedReturnValue" + } +) public interface KeyInputHandler { /** * 控制键按下时调用。返回 true 表示已处理。 diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/BackgroundElement.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/BackgroundElement.java index 4340dc19..5367ae05 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/BackgroundElement.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/BackgroundElement.java @@ -7,6 +7,12 @@ /** * 渲染填充圆角矩形背景。 */ +@SuppressWarnings( + { + "unused", + "UnusedReturnValue" + } +) public record BackgroundElement(int color, float round) implements ModifierElement { public BackgroundElement(int color) { this(color, 0); diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/BorderElement.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/BorderElement.java index 2a37eec7..d9f6f1c6 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/BorderElement.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/BorderElement.java @@ -7,6 +7,12 @@ /** * 渲染描边圆角矩形边框。 */ +@SuppressWarnings( + { + "unused", + "UnusedReturnValue" + } +) public record BorderElement(float width, int color, float round) implements ModifierElement { public BorderElement(float width, int color) { this(width, color, 0); diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/CombinedModifier.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/CombinedModifier.java index 8573a254..d7919099 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/CombinedModifier.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/CombinedModifier.java @@ -4,6 +4,12 @@ import java.util.function.BiFunction; +@SuppressWarnings( + { + "unused", + "UnusedReturnValue" + } +) public record CombinedModifier(ModifierElement outer, Modifier inner) implements Modifier { @Override public Modifier then(Modifier other) { diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/ModifierElement.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/ModifierElement.java index 78cff306..741332e9 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/ModifierElement.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/ModifierElement.java @@ -10,6 +10,12 @@ * {@link dev.anvilcraft.lib.v2.ui.Modifier} 链中的单个节点。 * 每个元素可拦截 measure、layout、render 阶段。 */ +@SuppressWarnings( + { + "unused", + "UnusedReturnValue" + } +) public interface ModifierElement { static ModifierElement size(float width, float height) { return new SizeElement(width, width, height, height); diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/PaddingElement.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/PaddingElement.java index 8be69e9a..e2fbd454 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/PaddingElement.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/PaddingElement.java @@ -5,6 +5,12 @@ import dev.anvilcraft.lib.v2.ui.MeasuredSize; import dev.anvilcraft.lib.v2.ui.UIComponent; +@SuppressWarnings( + { + "unused", + "UnusedReturnValue" + } +) public record PaddingElement(float left, float top, float right, float bottom) implements ModifierElement { @Override public MeasuredSize modifyMeasuredSize(UIComponent component, Constraints constraints, MeasuredSize childSize) { diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/SingleElementModifier.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/SingleElementModifier.java index a06587f4..8605dc9e 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/SingleElementModifier.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/SingleElementModifier.java @@ -4,6 +4,12 @@ import java.util.function.BiFunction; +@SuppressWarnings( + { + "unused", + "UnusedReturnValue" + } +) public record SingleElementModifier(ModifierElement element) implements Modifier { @Override public Modifier then(Modifier other) { diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/SizeElement.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/SizeElement.java index e1750347..61e54152 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/SizeElement.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/SizeElement.java @@ -2,6 +2,12 @@ import dev.anvilcraft.lib.v2.ui.Constraints; +@SuppressWarnings( + { + "unused", + "UnusedReturnValue" + } +) public record SizeElement(float minWidthHint, float maxWidthHint, float minHeightHint, float maxHeightHint) implements ModifierElement { @Override From 1f4fec153de73cf8d3c1dfd682bf34f0f5ed7aaa Mon Sep 17 00:00:00 2001 From: Gugle Date: Tue, 19 May 2026 03:44:26 +0800 Subject: [PATCH 47/67] feat(ui): enhance code consistency by using 'this' keyword for instance variables --- .../dev/anvilcraft/lib/v2/ui/Animatable.java | 12 +-- .../dev/anvilcraft/lib/v2/ui/Composition.java | 68 +++++++------- .../lib/v2/ui/DeclarativeScreen.java | 86 ++++++++--------- .../java/dev/anvilcraft/lib/v2/ui/Ref.java | 12 +-- .../dev/anvilcraft/lib/v2/ui/UIScope.java | 38 ++++---- .../lib/v2/ui/component/ButtonComponent.java | 69 ++++---------- .../v2/ui/component/CheckboxComponent.java | 10 +- .../lib/v2/ui/component/ColumnComponent.java | 46 ++++----- .../lib/v2/ui/component/GridComponent.java | 41 ++++---- .../lib/v2/ui/component/ImageComponent.java | 10 +- .../lib/v2/ui/component/RowComponent.java | 42 ++++----- .../lib/v2/ui/component/SliderComponent.java | 26 ++--- .../lib/v2/ui/component/SpacerComponent.java | 4 +- .../lib/v2/ui/component/TextComponent.java | 14 +-- .../v2/ui/component/TextInputComponent.java | 94 ++++++------------- 15 files changed, 235 insertions(+), 337 deletions(-) diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Animatable.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Animatable.java index d94dea1b..51bcbcf7 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Animatable.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Animatable.java @@ -55,19 +55,19 @@ public void animateTo(float target, int durationTicks) { * 每 tick 调用一次以推进动画。返回 true 表示动画进行中。 */ public boolean tick() { - if (elapsed >= durationTicks) return false; - elapsed++; - float t = durationTicks > 0 ? (float) elapsed / durationTicks : 1f; + if (this.elapsed >= this.durationTicks) return false; + this.elapsed++; + float t = this.durationTicks > 0 ? (float) this.elapsed / this.durationTicks : 1f; // 缓入缓出 float eased = t < 0.5f ? 2f * t * t : -1f + (4f - 2f * t) * t; - this.value = Mth.lerp(eased, startValue, targetValue); - return elapsed < durationTicks; + this.value = Mth.lerp(eased, this.startValue, this.targetValue); + return this.elapsed < this.durationTicks; } /** * 动画是否进行中。 */ public boolean isRunning() { - return elapsed < durationTicks; + return this.elapsed < this.durationTicks; } } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Composition.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Composition.java index 75f64377..a24b3cd6 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Composition.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Composition.java @@ -81,15 +81,15 @@ public static Composition current() { * 标记组合需要在下一帧 recompose。 */ public void invalidate() { - dirty = true; + this.dirty = true; } /** * 注册动画值,每帧自动 tick。动画进行中时自动触发 recompose。 */ public void watch(Animatable anim) { - if (!animatables.contains(anim)) { - animatables.add(anim); + if (!this.animatables.contains(anim)) { + this.animatables.add(anim); } } @@ -100,13 +100,13 @@ public void watch(Animatable anim) { */ @SuppressWarnings("unchecked") public T remember(Supplier init) { - int key = currentRememberKey++; - Object existing = rememberedValues.get(key); + int key = this.currentRememberKey++; + Object existing = this.rememberedValues.get(key); if (existing != null) { return (T) existing; } T value = init.get(); - rememberedValues.put(key, value); + this.rememberedValues.put(key, value); return value; } @@ -114,7 +114,7 @@ public T remember(Supplier init) { * {@code comp.ref(0)} 等价于 {@code comp.remember(() -> new Ref<>(0))}。 */ public Ref ref(T initialValue) { - return remember(() -> new Ref<>(initialValue)); + return this.remember(() -> new Ref<>(initialValue)); } // ── emit ── @@ -124,21 +124,20 @@ public Ref ref(T initialValue) { */ public void emit(UIComponent component) { Slot slot; - if (currentIndex < slots.size()) { - slot = slots.get(currentIndex); - // 同类型组件保留运行时状态(如滚动位置) + if (this.currentIndex < this.slots.size()) { + slot = this.slots.get(this.currentIndex); UIComponent old = slot.component; if (old != null && old.getClass() == component.getClass()) { - copyRuntimeState(old, component); + this.copyRuntimeState(old, component); } slot.component = component; } else { slot = new Slot(); slot.component = component; - slots.add(slot); + this.slots.add(slot); } - currentSlot = slot; - currentIndex++; + this.currentSlot = slot; + this.currentIndex++; } /** @@ -171,17 +170,17 @@ private void copyRuntimeState(UIComponent old, UIComponent replacement) { public void renderFrame(GuiGraphicsExtractor extractor, float screenWidth, float screenHeight) { CURRENT.set(this); try { - for (Animatable anim : animatables) { - if (anim.tick()) dirty = true; + for (Animatable anim : this.animatables) { + if (anim.tick()) this.dirty = true; } - if (dirty || hasDirtySlots()) { - recompose(); - dirty = false; + if (this.dirty || this.hasDirtySlots()) { + this.recompose(); + this.dirty = false; } Constraints rootConstraints = new Constraints(0, screenWidth, 0, screenHeight); - if (rootScope != null) { - for (UIComponent child : rootScope.getChildren()) { - renderTree(child, extractor, rootConstraints); + if (this.rootScope != null) { + for (UIComponent child : this.rootScope.getChildren()) { + this.renderTree(child, extractor, rootConstraints); } } } finally { @@ -192,25 +191,24 @@ public void renderFrame(GuiGraphicsExtractor extractor, float screenWidth, float // ── recompose ── private void recompose() { - if (rootScope != null) { - rootScope.clearChildren(); + if (this.rootScope != null) { + this.rootScope.clearChildren(); } - currentIndex = 0; - currentRememberKey = 0; - // recompose 前清除 slot 脏标记 - for (Slot slot : slots) { + this.currentIndex = 0; + this.currentRememberKey = 0; + for (Slot slot : this.slots) { slot.dirty = false; } - if (content != null && rootScope != null) { - content.accept(rootScope); + if (this.content != null && this.rootScope != null) { + this.content.accept(this.rootScope); } - while (slots.size() > currentIndex) { - slots.removeLast(); + while (this.slots.size() > this.currentIndex) { + this.slots.removeLast(); } } private boolean hasDirtySlots() { - for (Slot slot : slots) { + for (Slot slot : this.slots) { if (slot.dirty) return true; } return false; @@ -277,11 +275,11 @@ public static class Slot { boolean dirty = true; void addReadState(Ref state) { - readStates.add(state); + this.readStates.add(state); } void markDirty() { - dirty = true; + this.dirty = true; } } } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java index 131a648c..7131a124 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java @@ -50,13 +50,12 @@ protected DeclarativeScreen(Component title) { @Override public void extractRenderState(GuiGraphicsExtractor extractor, int mouseX, int mouseY, float partialTick) { super.extractRenderState(extractor, mouseX, mouseY, partialTick); - if (composition != null) { - composition.renderFrame(extractor, this.width, this.height); - updateHover(mouseX, mouseY); - refreshFocus(); - // 延迟渲染下拉弹出层(确保 z-order 正确) - for (UIComponent child : rootScope.getChildren()) { - renderPopups(child, extractor); + if (this.composition != null) { + this.composition.renderFrame(extractor, this.width, this.height); + this.updateHover(mouseX, mouseY); + this.refreshFocus(); + for (UIComponent child : this.rootScope.getChildren()) { + this.renderPopups(child, extractor); } } } @@ -66,10 +65,10 @@ public void extractRenderState(GuiGraphicsExtractor extractor, int mouseX, int m @Override public boolean keyPressed(KeyEvent event) { if (event.key() == 256) { - onClose(); + this.onClose(); return true; } - if (focusOwner != null && focusOwner.onKeyPressed(event)) { + if (this.focusOwner != null && this.focusOwner.onKeyPressed(event)) { return true; } return super.keyPressed(event); @@ -82,13 +81,13 @@ public void onClose() { @Override protected void init() { - composition = new Composition(rootScope); - composition.setContent(this::content); + this.composition = new Composition(this.rootScope); + this.composition.setContent(this::content); } private void renderPopups(UIComponent component, GuiGraphicsExtractor extractor) { if (component instanceof DropdownComponent dd) dd.renderPopup(extractor); - for (UIComponent child : component.children()) renderPopups(child, extractor); + for (UIComponent child : component.children()) this.renderPopups(child, extractor); } // ── 鼠标输入 ── @@ -97,21 +96,21 @@ private void renderPopups(UIComponent component, GuiGraphicsExtractor extractor) * recompose 后重新绑定 focusOwner(旧实例可能已被替换)。 */ private void refreshFocus() { - if (focusOwner == null) return; - for (UIComponent child : rootScope.getChildren()) { - KeyInputHandler found = findFocused(child); + if (this.focusOwner == null) return; + for (UIComponent child : this.rootScope.getChildren()) { + KeyInputHandler found = this.findFocused(child); if (found != null) { - focusOwner = found; + this.focusOwner = found; return; } } - focusOwner = null; + this.focusOwner = null; } private @Nullable KeyInputHandler findFocused(UIComponent component) { if (component instanceof TextInputComponent tf && tf.focused()) return tf; for (UIComponent child : component.children()) { - KeyInputHandler found = findFocused(child); + KeyInputHandler found = this.findFocused(child); if (found != null) return found; } return null; @@ -124,25 +123,22 @@ public boolean mouseClicked(MouseButtonEvent event, boolean isDoubleClick) { int mx = (int) mc.mouseHandler.getScaledXPos(mc.getWindow()); int my = (int) mc.mouseHandler.getScaledYPos(mc.getWindow()); - // 清除焦点 - focusOwner = null; - for (UIComponent child : rootScope.getChildren()) { - clearFocusRecursive(child); + this.focusOwner = null; + for (UIComponent child : this.rootScope.getChildren()) { + this.clearFocusRecursive(child); } - // 命中测试 boolean hit = false; - for (UIComponent child : rootScope.getChildren()) { - if (hitTestClick(child, mx, my)) { + for (UIComponent child : this.rootScope.getChildren()) { + if (this.hitTestClick(child, mx, my)) { hit = true; break; } } - // 未命中任何 dropdown 时关闭所有 if (!hit) { - for (UIComponent child : rootScope.getChildren()) { - closeDropdownsRecursive(child); + for (UIComponent child : this.rootScope.getChildren()) { + this.closeDropdownsRecursive(child); } } @@ -156,8 +152,8 @@ public boolean mouseClicked(MouseButtonEvent event, boolean isDoubleClick) { @Override public boolean mouseReleased(MouseButtonEvent event) { - for (UIComponent child : rootScope.getChildren()) { - stopDragRecursive(child); + for (UIComponent child : this.rootScope.getChildren()) { + this.stopDragRecursive(child); } return super.mouseReleased(event); } @@ -169,16 +165,16 @@ public boolean mouseDragged(MouseButtonEvent event, double deltaX, double deltaY var mc = Minecraft.getInstance(); int mx = (int) mc.mouseHandler.getScaledXPos(mc.getWindow()); int my = (int) mc.mouseHandler.getScaledYPos(mc.getWindow()); - for (UIComponent child : rootScope.getChildren()) { - if (hitTestDrag(child, mx, my)) return true; + for (UIComponent child : this.rootScope.getChildren()) { + if (this.hitTestDrag(child, mx, my)) return true; } return super.mouseDragged(event, deltaX, deltaY); } @Override public boolean mouseScrolled(double mouseX, double mouseY, double scrollX, double scrollY) { - for (UIComponent child : rootScope.getChildren()) { - if (hitTestScroll(child, (float) mouseX, (float) mouseY, (float) scrollY)) { + for (UIComponent child : this.rootScope.getChildren()) { + if (this.hitTestScroll(child, (float) mouseX, (float) mouseY, (float) scrollY)) { return true; } } @@ -187,7 +183,7 @@ public boolean mouseScrolled(double mouseX, double mouseY, double scrollX, doubl @Override public boolean charTyped(CharacterEvent event) { - if (focusOwner != null && focusOwner.onCharTyped(event)) { + if (this.focusOwner != null && this.focusOwner.onCharTyped(event)) { return true; } return super.charTyped(event); @@ -201,7 +197,7 @@ public boolean charTyped(CharacterEvent event) { private boolean hitTestClick(UIComponent component, float px, float py) { var children = component.children(); for (int i = children.size() - 1; i >= 0; i--) { - if (hitTestClick(children.get(i), px, py)) return true; + if (this.hitTestClick(children.get(i), px, py)) return true; } switch (component) { case ButtonComponent btn when btn.hitRect().contains(px, py) -> { @@ -218,7 +214,7 @@ private boolean hitTestClick(UIComponent component, float px, float py) { } case TextInputComponent tf when tf.hitRect().contains(px, py) -> { tf.setFocused(true); - focusOwner = tf; + this.focusOwner = tf; return true; } case DropdownComponent dd -> { @@ -245,7 +241,7 @@ private boolean hitTestClick(UIComponent component, float px, float py) { private boolean hitTestDrag(UIComponent component, float px, float py) { var children = component.children(); for (int i = children.size() - 1; i >= 0; i--) { - if (hitTestDrag(children.get(i), px, py)) return true; + if (this.hitTestDrag(children.get(i), px, py)) return true; } switch (component) { case SliderComponent sl when sl.hitRect().contains(px, py) -> { @@ -271,7 +267,7 @@ private boolean hitTestDrag(UIComponent component, float px, float py) { */ private void closeDropdownsRecursive(UIComponent component) { if (component instanceof DropdownComponent dd) dd.setOpen(false); - for (UIComponent child : component.children()) closeDropdownsRecursive(child); + for (UIComponent child : component.children()) this.closeDropdownsRecursive(child); } /** @@ -280,7 +276,7 @@ private void closeDropdownsRecursive(UIComponent component) { private void stopDragRecursive(UIComponent component) { if (component instanceof ScrollableComponent sc) sc.stopScrollbarDrag(); if (component instanceof DropdownComponent dd) dd.stopPopupScrollbarDrag(); - for (UIComponent child : component.children()) stopDragRecursive(child); + for (UIComponent child : component.children()) this.stopDragRecursive(child); } /** @@ -289,7 +285,7 @@ private void stopDragRecursive(UIComponent component) { private boolean hitTestScroll(UIComponent component, float px, float py, float amount) { var children = component.children(); for (int i = children.size() - 1; i >= 0; i--) { - if (hitTestScroll(children.get(i), px, py, amount)) return true; + if (this.hitTestScroll(children.get(i), px, py, amount)) return true; } if (component instanceof ScrollableComponent sc && sc.hitRect().contains(px, py)) { return sc.onScroll(amount); @@ -306,15 +302,15 @@ private boolean hitTestScroll(UIComponent component, float px, float py, float a */ private void clearFocusRecursive(UIComponent component) { if (component instanceof TextInputComponent tf) tf.setFocused(false); - for (UIComponent child : component.children()) clearFocusRecursive(child); + for (UIComponent child : component.children()) this.clearFocusRecursive(child); } /** * 遍历组件树,更新 ButtonComponent 的 hover 状态。 */ private void updateHover(float mouseX, float mouseY) { - for (UIComponent child : rootScope.getChildren()) { - updateHoverRecursive(child, mouseX, mouseY); + for (UIComponent child : this.rootScope.getChildren()) { + this.updateHoverRecursive(child, mouseX, mouseY); } } @@ -323,7 +319,7 @@ private void updateHoverRecursive(UIComponent component, float mx, float my) { btn.setHovered(btn.hitRect().contains(mx, my)); } for (UIComponent child : component.children()) { - updateHoverRecursive(child, mx, my); + this.updateHoverRecursive(child, mx, my); } } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Ref.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Ref.java index b320ea19..5cece7ca 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Ref.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Ref.java @@ -36,18 +36,18 @@ public T getValue() { Composition comp = Composition.currentOrNull(); if (comp != null && comp.currentSlot != null) { comp.currentSlot.addReadState(this); - readers.add(comp.currentSlot); + this.readers.add(comp.currentSlot); } - return value; + return this.value; } /** * 设置新值。若值发生变化,标记所有 reader slot 为脏。 */ public void setValue(@Nullable T newValue) { - if (!Objects.equals(value, newValue)) { - value = newValue; - for (Composition.Slot slot : readers) { + if (!Objects.equals(this.value, newValue)) { + this.value = newValue; + for (Composition.Slot slot : this.readers) { slot.markDirty(); } } @@ -55,6 +55,6 @@ public void setValue(@Nullable T newValue) { @Override public String toString() { - return "State(" + value + ")"; + return "State(" + this.value + ")"; } } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIScope.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIScope.java index b24234de..d121394c 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIScope.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIScope.java @@ -46,21 +46,21 @@ public abstract class UIScope { * 向当前 scope 添加一个子组件。 */ public void addChild(UIComponent child) { - children.add(child); + this.children.add(child); } /** * 返回当前 scope 中已收集的子组件列表(只读)。 */ public List getChildren() { - return Collections.unmodifiableList(children); + return Collections.unmodifiableList(this.children); } /** * 内部方法:recompose 前清空子组件。 */ void clearChildren() { - children.clear(); + this.children.clear(); } // ── 组件构建器 ── @@ -73,7 +73,7 @@ void clearChildren() { */ public TextComponent Text(String text) { TextComponent c = new TextComponent(Modifier.NONE, text); - addChild(c); + this.addChild(c); Composition.current().emit(c); return c; } @@ -86,7 +86,7 @@ public TextComponent Text(String text) { */ public SpacerComponent Spacer(float width, float height) { SpacerComponent c = new SpacerComponent(Modifier.NONE, width, height); - addChild(c); + this.addChild(c); Composition.current().emit(c); return c; } @@ -100,7 +100,7 @@ public SpacerComponent Spacer(float width, float height) { */ public ImageComponent Image(Identifier sprite, float width, float height) { ImageComponent c = new ImageComponent(Modifier.NONE, sprite, width, height); - addChild(c); + this.addChild(c); Composition.current().emit(c); return c; } @@ -113,7 +113,7 @@ public ImageComponent Image(Identifier sprite, float width, float height) { */ public CheckboxComponent Checkbox(String label, boolean checked) { CheckboxComponent c = new CheckboxComponent(Modifier.NONE, label, checked); - addChild(c); + this.addChild(c); Composition.current().emit(c); return c; } @@ -127,7 +127,7 @@ public CheckboxComponent Checkbox(String label, boolean checked) { public CheckboxComponent Checkbox(String label, Ref vModel) { CheckboxComponent c = new CheckboxComponent(Modifier.NONE, label, vModel.getValue() != null && vModel.getValue()); c.onToggle(() -> vModel.setValue(!vModel.getValue())); - addChild(c); + this.addChild(c); Composition.current().emit(c); return c; } @@ -141,7 +141,7 @@ public CheckboxComponent Checkbox(String label, Ref vModel) { */ public DropdownComponent Dropdown(String[] options, int selectedIndex, float maxPopupHeight) { DropdownComponent c = new DropdownComponent(Modifier.NONE, options, selectedIndex, maxPopupHeight); - addChild(c); + this.addChild(c); Composition.current().emit(c); return c; } @@ -156,7 +156,7 @@ public DropdownComponent Dropdown(String[] options, int selectedIndex, float max */ public SliderComponent Slider(float value, float min, float max, float width) { SliderComponent c = new SliderComponent(Modifier.NONE, value, min, max, width); - addChild(c); + this.addChild(c); Composition.current().emit(c); return c; } @@ -172,7 +172,7 @@ public SliderComponent Slider(float value, float min, float max, float width) { public SliderComponent Slider(float min, float max, float width, Ref vModel) { SliderComponent c = new SliderComponent(Modifier.NONE, vModel.getValue() == null ? 0 : vModel.getValue(), min, max, width); c.onChange(vModel::setValue); - addChild(c); + this.addChild(c); Composition.current().emit(c); return c; } @@ -184,7 +184,7 @@ public SliderComponent Slider(float min, float max, float width, Ref vMod */ public TextInputComponent TextInput(String placeholder) { TextInputComponent c = new TextInputComponent(Modifier.NONE, placeholder); - addChild(c); + this.addChild(c); Composition.current().emit(c); return c; } @@ -198,7 +198,7 @@ public TextInputComponent TextInput(String placeholder) { public TextInputComponent TextInput(String placeholder, Ref vModel) { TextInputComponent c = new TextInputComponent(Modifier.NONE, placeholder); c.onChange(vModel::setValue); - addChild(c); + this.addChild(c); Composition.current().emit(c); return c; } @@ -210,7 +210,7 @@ public TextInputComponent TextInput(String placeholder, Ref vModel) { */ public ButtonComponent Button(String label) { ButtonComponent c = new ButtonComponent(Modifier.NONE, label); - addChild(c); + this.addChild(c); Composition.current().emit(c); return c; } @@ -226,7 +226,7 @@ public ColumnComponent Column(Consumer content) { ColumnScope inner = new ColumnScope(); content.accept(inner); c.setChildren(inner.getChildren()); - addChild(c); + this.addChild(c); Composition.current().emit(c); return c; } @@ -242,7 +242,7 @@ public RowComponent Row(Consumer content) { RowScope inner = new RowScope(); content.accept(inner); c.setChildren(inner.getChildren()); - addChild(c); + this.addChild(c); Composition.current().emit(c); return c; } @@ -257,7 +257,7 @@ public BoxComponent Box(Consumer content) { BoxScope inner = new BoxScope(); content.accept(inner); c.setChildren(inner.getChildren()); - addChild(c); + this.addChild(c); Composition.current().emit(c); return c; } @@ -274,7 +274,7 @@ public GridComponent Grid(int columns, Consumer content) { GridScope inner = new GridScope(); content.accept(inner); c.setChildren(inner.getChildren()); - addChild(c); + this.addChild(c); Composition.current().emit(c); return c; } @@ -290,7 +290,7 @@ public ScrollableComponent Scrollable(float maxHeight, Consumer ScrollableScope inner = new ScrollableScope(); content.accept(inner); c.setChildren(inner.getChildren()); - addChild(c); + this.addChild(c); Composition.current().emit(c); return c; } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ButtonComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ButtonComponent.java index 627bd3f6..a8fd91bb 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ButtonComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ButtonComponent.java @@ -15,26 +15,17 @@ import java.util.Collections; import java.util.List; -/** - * 可点击按钮。原版 fill() 背景 + 手动居中 text() 文字。 - */ @Accessors(fluent = true) -@SuppressWarnings( - { - "unused", - "UnusedReturnValue" - } -) +@SuppressWarnings({"unused", "UnusedReturnValue"}) public class ButtonComponent implements UIComponent { - // 原版按钮配色 - private static final int BG_COLOR = 0xFF404040; + + private static final int BG_COLOR = 0xFF404040; private static final int BG_HOVER_COLOR = 0xFF606060; - private static final int TEXT_COLOR = 0xFFFFFFFF; - private static final float PADDING_H = 12; - private static final float PADDING_V = 6; + private static final int TEXT_COLOR = 0xFFFFFFFF; + private static final float PADDING_H = 12; + private static final float PADDING_V = 6; - @Getter - @Setter + @Getter @Setter private Modifier modifier; @Setter private String label; @@ -42,7 +33,6 @@ public class ButtonComponent implements UIComponent { private Runnable onClick; private boolean hovered; - @Getter private float x, y, width, height; public ButtonComponent(Modifier modifier, String label) { @@ -50,24 +40,15 @@ public ButtonComponent(Modifier modifier, String label) { this.label = label; } - public ButtonComponent onClick(@Nullable Runnable onClick) { - this.onClick = onClick; - return this; - } + public ButtonComponent onClick(@Nullable Runnable onClick) { this.onClick = onClick; return this; } + public void setHovered(boolean hovered) { this.hovered = hovered; } - public void setHovered(boolean hovered) { - this.hovered = hovered; - } - - @Override - public List children() { - return Collections.emptyList(); - } + @Override public List children() { return Collections.emptyList(); } @Override public MeasuredSize measure(Constraints constraints) { var font = Minecraft.getInstance().font; - float textW = font.width(label); + float textW = font.width(this.label); float textH = font.lineHeight; return MeasuredSize.of( constraints.constrainWidth(textW + PADDING_H * 2), @@ -85,34 +66,22 @@ public void layout(float x, float y, float width, float height) { @Override public void extractRenderState(GuiGraphicsExtractor extractor) { - int bg = hovered ? BG_HOVER_COLOR : BG_COLOR; - int ix = (int) x, iy = (int) y, iw = (int) width, ih = (int) height; - - // 背景 — 用原版 fill,精确定位 + int bg = this.hovered ? BG_HOVER_COLOR : BG_COLOR; + int ix = (int) this.x, iy = (int) this.y, iw = (int) this.width, ih = (int) this.height; extractor.fill(ix, iy, ix + iw, iy + ih, bg); - // 文字 — text() 手动居中 var font = Minecraft.getInstance().font; - String txt = label; - float textW = font.width(txt); - int textX = (int) (x + (width - textW) / 2f); - int textY = (int) (y + (height - font.lineHeight) / 2f); - - extractor.text(font, txt, textX, textY, TEXT_COLOR, true); + float textW = font.width(this.label); + int textX = (int) (this.x + (this.width - textW) / 2f); + int textY = (int) (this.y + (this.height - font.lineHeight) / 2f); + extractor.text(font, this.label, textX, textY, TEXT_COLOR, true); } - /** - * 按钮包围盒,用于命中测试。 - */ public LayoutRect hitRect() { - return LayoutRect.of(x, y, width, height); + return LayoutRect.of(this.x, this.y, this.width, this.height); } - /** - * 触发点击回调。 - */ public void click() { - if (onClick != null) onClick.run(); + if (this.onClick != null) this.onClick.run(); } } - diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/CheckboxComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/CheckboxComponent.java index 3fbd88a2..06344839 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/CheckboxComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/CheckboxComponent.java @@ -57,7 +57,7 @@ public List children() { @Override public MeasuredSize measure(Constraints constraints) { // 方框 + 间距 + 标签文字宽度(简化:估算每字符 7px 宽) - float labelW = label.length() * 7f; + float labelW = this.label.length() * 7f; return MeasuredSize.of( constraints.constrainWidth(SIZE + 4 + labelW), constraints.constrainHeight(SIZE) @@ -74,13 +74,13 @@ public void layout(float x, float y, float width, float height) { @Override public void extractRenderState(GuiGraphicsExtractor extractor) { - int ix = (int) x, iy = (int) y; + int ix = (int) this.x, iy = (int) this.y; // 外层深灰方块(始终显示) extractor.fill(ix, iy, ix + (int) SIZE, iy + (int) SIZE, BOX_COLOR); // 选中时中间白色小方块 - if (checked) { + if (this.checked) { int iix = ix + (int) INSET; int iiy = iy + (int) INSET; int iiw = (int) SIZE - (int) INSET * 2; @@ -96,8 +96,8 @@ public void extractRenderState(GuiGraphicsExtractor extractor) { * 切换状态。 */ public void toggle() { - checked = !checked; - if (onToggle != null) onToggle.run(); + this.checked = !this.checked; + if (this.onToggle != null) this.onToggle.run(); } /** diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ColumnComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ColumnComponent.java index f81e883f..02027e35 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ColumnComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ColumnComponent.java @@ -15,19 +15,11 @@ import java.util.Collections; import java.util.List; -/** - * 纵向线性布局。子组件自上而下排列。主轴=垂直,交叉轴=水平。 - */ @Accessors(fluent = true) -@SuppressWarnings( - { - "unused", - "UnusedReturnValue" - } -) +@SuppressWarnings({"unused", "UnusedReturnValue"}) public class ColumnComponent implements UIComponent { - @Getter - @Setter + + @Getter @Setter private Modifier modifier; @Getter private List children = Collections.emptyList(); @@ -40,8 +32,6 @@ public class ColumnComponent implements UIComponent { @Setter private float spacing; - // 布局状态 - @Getter private float x, y, width, height; public ColumnComponent(Modifier modifier) { @@ -52,28 +42,25 @@ public void setChildren(List children) { this.children = List.copyOf(children); } - // ── 链式 setter ── - - public MeasuredSize measure(Constraints constraints) { - if (children.isEmpty()) return MeasuredSize.ZERO; + if (this.children.isEmpty()) return MeasuredSize.ZERO; float totalHeight = 0; float maxWidth = 0; - List sizes = new ArrayList<>(children.size()); + List sizes = new ArrayList<>(this.children.size()); Constraints childConstraints = new Constraints( constraints.minWidth(), constraints.maxWidth(), 0, Float.MAX_VALUE ); - for (UIComponent child : children) { + for (UIComponent child : this.children) { MeasuredSize size = child.measure(childConstraints); sizes.add(size); totalHeight += size.height(); maxWidth = Math.max(maxWidth, size.width()); } - totalHeight += spacing * (children.size() - 1); + totalHeight += this.spacing * (this.children.size() - 1); this.childSizes = sizes; return MeasuredSize.of( @@ -88,22 +75,21 @@ public void layout(float x, float y, float width, float height) { this.width = width; this.height = height; - List heights = new ArrayList<>(childSizes.size()); - for (MeasuredSize s : childSizes) heights.add(s.height()); + List heights = new ArrayList<>(this.childSizes.size()); + for (MeasuredSize s : this.childSizes) heights.add(s.height()); - float[] yOffsets = verticalArrangement.arrange(height, heights, spacing); - for (int i = 0; i < children.size(); i++) { - UIComponent child = children.get(i); - MeasuredSize size = childSizes.get(i); - float childX = x + horizontalAlignment.align(width, size.width()); - child.layout(childX, y + yOffsets[i], size.width(), size.height()); + float[] yOffsets = this.verticalArrangement.arrange(this.height, heights, this.spacing); + for (int i = 0; i < this.children.size(); i++) { + UIComponent child = this.children.get(i); + MeasuredSize size = this.childSizes.get(i); + float childX = this.x + this.horizontalAlignment.align(this.width, size.width()); + child.layout(childX, this.y + yOffsets[i], size.width(), size.height()); } } public void extractRenderState(GuiGraphicsExtractor extractor) { - for (UIComponent child : children) { + for (UIComponent child : this.children) { child.extractRenderState(extractor); } } } - diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/GridComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/GridComponent.java index abe31776..fbdba53a 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/GridComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/GridComponent.java @@ -13,20 +13,12 @@ import java.util.Collections; import java.util.List; -/** - * 网格布局。子组件按列数排列,每格大小由最大子组件决定。 - */ @Accessors(fluent = true) -@SuppressWarnings( - { - "unused", - "UnusedReturnValue" - } -) +@SuppressWarnings({"unused", "UnusedReturnValue"}) public class GridComponent implements UIComponent { + private final int columns; - @Getter - @Setter + @Getter @Setter private Modifier modifier; @Getter private List children = Collections.emptyList(); @@ -34,7 +26,6 @@ public class GridComponent implements UIComponent { private List childSizes = Collections.emptyList(); private float hSpacing, vSpacing; - @Getter private float x, y, width, height; private float cellW, cellH; @@ -54,13 +45,13 @@ public GridComponent spacing(float h, float v) { } public MeasuredSize measure(Constraints constraints) { - if (children.isEmpty()) return MeasuredSize.ZERO; + if (this.children.isEmpty()) return MeasuredSize.ZERO; float maxW = 0, maxH = 0; - List sizes = new ArrayList<>(children.size()); + List sizes = new ArrayList<>(this.children.size()); Constraints childC = new Constraints(0, Float.MAX_VALUE, 0, Float.MAX_VALUE); - for (UIComponent child : children) { + for (UIComponent child : this.children) { MeasuredSize s = child.measure(childC); sizes.add(s); maxW = Math.max(maxW, s.width()); @@ -71,9 +62,9 @@ public MeasuredSize measure(Constraints constraints) { this.cellW = maxW; this.cellH = maxH; - int rows = (children.size() + columns - 1) / columns; - float totalW = maxW * columns + hSpacing * (columns - 1); - float totalH = maxH * rows + vSpacing * (rows - 1); + int rows = (this.children.size() + this.columns - 1) / this.columns; + float totalW = maxW * this.columns + this.hSpacing * (this.columns - 1); + float totalH = maxH * rows + this.vSpacing * (rows - 1); return MeasuredSize.of(constraints.constrainWidth(totalW), constraints.constrainHeight(totalH)); } @@ -84,16 +75,16 @@ public void layout(float x, float y, float width, float height) { this.width = width; this.height = height; - for (int i = 0; i < children.size(); i++) { - int col = i % columns; - int row = i / columns; - float cx = x + col * (cellW + hSpacing); - float cy = y + row * (cellH + vSpacing); - children.get(i).layout(cx, cy, cellW, cellH); + for (int i = 0; i < this.children.size(); i++) { + int col = i % this.columns; + int row = i / this.columns; + float cx = this.x + col * (this.cellW + this.hSpacing); + float cy = this.y + row * (this.cellH + this.vSpacing); + this.children.get(i).layout(cx, cy, this.cellW, this.cellH); } } public void extractRenderState(GuiGraphicsExtractor extractor) { - for (UIComponent child : children) child.extractRenderState(extractor); + for (UIComponent child : this.children) child.extractRenderState(extractor); } } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ImageComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ImageComponent.java index 4591f699..627399dc 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ImageComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ImageComponent.java @@ -57,8 +57,8 @@ public List children() { @Override public MeasuredSize measure(Constraints constraints) { return MeasuredSize.of( - constraints.constrainWidth(imageWidth), - constraints.constrainHeight(imageHeight) + constraints.constrainWidth(this.imageWidth), + constraints.constrainHeight(this.imageHeight) ); } @@ -74,9 +74,9 @@ public void layout(float x, float y, float width, float height) { public void extractRenderState(GuiGraphicsExtractor extractor) { extractor.blitSprite( RenderPipelines.GUI_TEXTURED, - sprite, - (int) x, (int) y, - (int) width, (int) height + this.sprite, + (int) this.x, (int) this.y, + (int) this.width, (int) this.height ); } } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/RowComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/RowComponent.java index a02c2e18..09abf4ea 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/RowComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/RowComponent.java @@ -15,19 +15,11 @@ import java.util.Collections; import java.util.List; -/** - * 横向线性布局。子组件自左而右排列。主轴=水平,交叉轴=垂直。 - */ @Accessors(fluent = true) -@SuppressWarnings( - { - "unused", - "UnusedReturnValue" - } -) +@SuppressWarnings({"unused", "UnusedReturnValue"}) public class RowComponent implements UIComponent { - @Getter - @Setter + + @Getter @Setter private Modifier modifier; @Getter private List children = Collections.emptyList(); @@ -40,7 +32,6 @@ public class RowComponent implements UIComponent { @Setter private float spacing; - @Getter private float x, y, width, height; public RowComponent(Modifier modifier) { @@ -51,26 +42,25 @@ public void setChildren(List children) { this.children = List.copyOf(children); } - public MeasuredSize measure(Constraints constraints) { - if (children.isEmpty()) return MeasuredSize.ZERO; + if (this.children.isEmpty()) return MeasuredSize.ZERO; float totalWidth = 0; float maxHeight = 0; - List sizes = new ArrayList<>(children.size()); + List sizes = new ArrayList<>(this.children.size()); Constraints childConstraints = new Constraints( 0, Float.MAX_VALUE, constraints.minHeight(), constraints.maxHeight() ); - for (UIComponent child : children) { + for (UIComponent child : this.children) { MeasuredSize size = child.measure(childConstraints); sizes.add(size); totalWidth += size.width(); maxHeight = Math.max(maxHeight, size.height()); } - totalWidth += spacing * (children.size() - 1); + totalWidth += this.spacing * (this.children.size() - 1); this.childSizes = sizes; return MeasuredSize.of( @@ -85,20 +75,20 @@ public void layout(float x, float y, float width, float height) { this.width = width; this.height = height; - List widths = new ArrayList<>(childSizes.size()); - for (MeasuredSize s : childSizes) widths.add(s.width()); + List widths = new ArrayList<>(this.childSizes.size()); + for (MeasuredSize s : this.childSizes) widths.add(s.width()); - float[] xOffsets = horizontalArrangement.arrange(width, widths, spacing); - for (int i = 0; i < children.size(); i++) { - UIComponent child = children.get(i); - MeasuredSize size = childSizes.get(i); - float childY = y + verticalAlignment.align(height, size.height()); - child.layout(x + xOffsets[i], childY, size.width(), size.height()); + float[] xOffsets = this.horizontalArrangement.arrange(this.width, widths, this.spacing); + for (int i = 0; i < this.children.size(); i++) { + UIComponent child = this.children.get(i); + MeasuredSize size = this.childSizes.get(i); + float childY = this.y + this.verticalAlignment.align(this.height, size.height()); + child.layout(this.x + xOffsets[i], childY, size.width(), size.height()); } } public void extractRenderState(GuiGraphicsExtractor extractor) { - for (UIComponent child : children) { + for (UIComponent child : this.children) { child.extractRenderState(extractor); } } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/SliderComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/SliderComponent.java index 985fd3b2..9e8d8974 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/SliderComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/SliderComponent.java @@ -63,7 +63,7 @@ public List children() { @Override public MeasuredSize measure(Constraints constraints) { return MeasuredSize.of( - constraints.constrainWidth(trackWidth), + constraints.constrainWidth(this.trackWidth), constraints.constrainHeight(THUMB_H) ); } @@ -78,17 +78,17 @@ public void layout(float x, float y, float width, float height) { @Override public void extractRenderState(GuiGraphicsExtractor extractor) { - int ix = (int) x, iy = (int) y; - int iw = (int) width; + int ix = (int) this.x, iy = (int) this.y; + int iw = (int) this.width; // 轨道 — 居中画在 THUMB_H 中间 - int trackY = (int) (y + (THUMB_H - TRACK_H) / 2f); + int trackY = (int) (this.y + (THUMB_H - TRACK_H) / 2f); extractor.fill(ix, trackY, ix + iw, (int) (trackY + TRACK_H), TRACK_COLOR); // 滑块 — 按比例定位 - float ratio = (value - min) / (max - min); - float thumbX = x + ratio * (width - THUMB_W); - int tix = (int) thumbX, tiy = (int) y; + float ratio = (this.value - this.min) / (this.max - this.min); + float thumbX = this.x + ratio * (this.width - THUMB_W); + int tix = (int) thumbX, tiy = (int) this.y; extractor.fill(tix, tiy, tix + (int) THUMB_W, tiy + (int) THUMB_H, THUMB_COLOR); } @@ -96,11 +96,11 @@ public void extractRenderState(GuiGraphicsExtractor extractor) { * 根据鼠标 X 坐标更新值。 */ public void setValueFromMouse(float mouseX) { - float ratio = Mth.clamp((mouseX - x) / Math.max(width - 1, 1), 0f, 1f); - float newValue = min + ratio * (max - min); - if (newValue != value) { - value = newValue; - if (onChange != null) onChange.accept(value); + float ratio = Mth.clamp((mouseX - this.x) / Math.max(this.width - 1, 1), 0f, 1f); + float newValue = this.min + ratio * (this.max - this.min); + if (newValue != this.value) { + this.value = newValue; + if (this.onChange != null) this.onChange.accept(this.value); } } @@ -108,6 +108,6 @@ public void setValueFromMouse(float mouseX) { * 命中测试包围盒(整个轨道+滑块区域)。 */ public LayoutRect hitRect() { - return LayoutRect.of(x, y, width, height); + return LayoutRect.of(this.x, this.y, this.width, this.height); } } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/SpacerComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/SpacerComponent.java index 340a255e..87d4c044 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/SpacerComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/SpacerComponent.java @@ -44,8 +44,8 @@ public List children() { @Override public MeasuredSize measure(Constraints constraints) { return MeasuredSize.of( - constraints.constrainWidth(spacerWidth), - constraints.constrainHeight(spacerHeight) + constraints.constrainWidth(this.spacerWidth), + constraints.constrainHeight(this.spacerHeight) ); } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextComponent.java index 63914af9..c467f444 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextComponent.java @@ -54,7 +54,7 @@ public List children() { @Override public MeasuredSize measure(Constraints constraints) { var font = Minecraft.getInstance().font; - float w = font.width(Component.literal(text)); + float w = font.width(Component.literal(this.text)); float h = font.lineHeight; return MeasuredSize.of(constraints.constrainWidth(w), constraints.constrainHeight(h)); } @@ -70,16 +70,16 @@ public void layout(float x, float y, float width, float height) { @Override public void extractRenderState(GuiGraphicsExtractor extractor) { var font = Minecraft.getInstance().font; - Component component = Component.literal(text); + Component component = Component.literal(this.text); float textW = font.width(component); - float renderX = switch (align) { - case LEFT -> x; - case CENTER -> x + (width - textW) / 2f; - case RIGHT -> x + width - textW; + float renderX = switch (this.align) { + case LEFT -> this.x; + case CENTER -> this.x + (this.width - textW) / 2f; + case RIGHT -> this.x + this.width - textW; }; - extractor.text(font, text, (int) renderX, (int) y, color, this.shadow); + extractor.text(font, this.text, (int) renderX, (int) this.y, this.color, this.shadow); } /** diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextInputComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextInputComponent.java index 38c5a102..b09d19f5 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextInputComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextInputComponent.java @@ -20,20 +20,10 @@ import java.util.List; import java.util.function.Consumer; -/** - * 单行文本输入框。参照原版 {@code EditBox} 实现。 - *

    - * placeholder 作为占位提示(灰色),开始输入后直接替换为输入内容。 - * 字符输入通过 {@link CharacterEvent} 处理,支持所有语言和输入法。 - */ @Accessors(fluent = true) -@SuppressWarnings( - { - "unused", - "UnusedReturnValue" - } -) +@SuppressWarnings({"unused", "UnusedReturnValue"}) public class TextInputComponent implements UIComponent, KeyInputHandler { + private static final int BG_COLOR = 0xFF202020; private static final int TEXT_COLOR = 0xFFFFFFFF; private static final int PLACEHOLDER_COLOR = 0xFF555555; @@ -42,8 +32,7 @@ public class TextInputComponent implements UIComponent, KeyInputHandler { private static final float PADDING_V = 4; private static final float WIDTH = 160; private final String placeholder; - @Getter - @Setter + @Getter @Setter private Modifier modifier; @Getter private String value = ""; @@ -55,7 +44,6 @@ public class TextInputComponent implements UIComponent, KeyInputHandler { @Getter private int cursorPos; - @Getter private float x, y, width, height; public TextInputComponent(Modifier modifier, @Nullable String placeholder) { @@ -69,17 +57,14 @@ public void setValue(@Nullable String value) { } public void setCursorPos(int pos) { - this.cursorPos = Math.clamp(pos, 0, value.length()); + this.cursorPos = Math.clamp(pos, 0, this.value.length()); } public void setFocused(boolean focused) { this.focused = focused; } - @Override - public List children() { - return Collections.emptyList(); - } + @Override public List children() { return Collections.emptyList(); } @Override public MeasuredSize measure(Constraints constraints) { @@ -97,87 +82,70 @@ public void layout(float x, float y, float width, float height) { @Override public void extractRenderState(GuiGraphicsExtractor extractor) { - int ix = (int) x, iy = (int) y, iw = (int) width, ih = (int) height; + int ix = (int) this.x, iy = (int) this.y, iw = (int) this.width, ih = (int) this.height; extractor.fill(ix, iy, ix + iw, iy + ih, BG_COLOR); var font = Minecraft.getInstance().font; int textX = ix + (int) PADDING_H; - int textY = (int) (y + (height + font.lineHeight) / 2f - font.lineHeight); - boolean hasText = !value.isEmpty(); + int textY = (int) (this.y + (this.height + font.lineHeight) / 2f - font.lineHeight); + boolean hasText = !this.value.isEmpty(); - String display = hasText ? value : placeholder; + String display = hasText ? this.value : this.placeholder; int color = hasText ? TEXT_COLOR : PLACEHOLDER_COLOR; extractor.text(font, display, textX, textY, color); - if (focused && hasText) { - String before = value.substring(0, Math.min(cursorPos, value.length())); - int cursorX = (int) (x + PADDING_H + font.width(before)); + if (this.focused && hasText) { + String before = this.value.substring(0, Math.min(this.cursorPos, this.value.length())); + int cursorX = (int) (this.x + PADDING_H + font.width(before)); extractor.fill(cursorX, iy + 2, cursorX + 1, iy + ih - 2, CURSOR_COLOR); } } - // ── 键盘输入 ── - @Override public boolean onKeyPressed(KeyEvent event) { int key = event.key(); - if (key == 259) { // 退格 - if (cursorPos > 0) { - value = new StringBuilder(value).deleteCharAt(cursorPos - 1).toString(); - cursorPos--; - fireChange(); + if (key == 259) { + if (this.cursorPos > 0) { + this.value = new StringBuilder(this.value).deleteCharAt(this.cursorPos - 1).toString(); + this.cursorPos--; + this.fireChange(); } return true; } - if (key == 261) { // Delete - if (cursorPos < value.length()) { - value = new StringBuilder(value).deleteCharAt(cursorPos).toString(); - fireChange(); + if (key == 261) { + if (this.cursorPos < this.value.length()) { + this.value = new StringBuilder(this.value).deleteCharAt(this.cursorPos).toString(); + this.fireChange(); } return true; } - if (key == 263) { - if (cursorPos > 0) cursorPos--; - return true; - } // ← - if (key == 262) { - if (cursorPos < value.length()) cursorPos++; - return true; - } // → - if (key == 268) { - cursorPos = 0; - return true; - } // Home - if (key == 269) { - cursorPos = value.length(); - return true; - } // End + if (key == 263) { if (this.cursorPos > 0) this.cursorPos--; return true; } + if (key == 262) { if (this.cursorPos < this.value.length()) this.cursorPos++; return true; } + if (key == 268) { this.cursorPos = 0; return true; } + if (key == 269) { this.cursorPos = this.value.length(); return true; } return false; } - /** - * 字符输入——支持所有语言、输入法、小键盘。参照原版 {@code EditBox.charTyped}。 - */ @Override public boolean onCharTyped(CharacterEvent event) { if (!event.isAllowedChatCharacter()) return false; String text = StringUtil.filterText(event.codepointAsString()); if (text.isEmpty()) return false; - insertText(text); + this.insertText(text); return true; } private void insertText(String text) { - value = new StringBuilder(value).insert(cursorPos, text).toString(); - cursorPos += text.length(); - fireChange(); + this.value = new StringBuilder(this.value).insert(this.cursorPos, text).toString(); + this.cursorPos += text.length(); + this.fireChange(); } private void fireChange() { - if (onChange != null) onChange.accept(value); + if (this.onChange != null) this.onChange.accept(this.value); } public LayoutRect hitRect() { - return LayoutRect.of(x, y, width, height); + return LayoutRect.of(this.x, this.y, this.width, this.height); } } From def17f9b7b912fb79307e6efccd77d3cd8c3b901 Mon Sep 17 00:00:00 2001 From: Gugle Date: Tue, 19 May 2026 04:02:59 +0800 Subject: [PATCH 48/67] feat(ui): improve code consistency by using 'this' keyword for instance variables --- .../dev/anvilcraft/lib/v2/ui/Alignment.java | 12 +- .../dev/anvilcraft/lib/v2/ui/Animatable.java | 2 +- .../dev/anvilcraft/lib/v2/ui/Arrangement.java | 24 +-- .../dev/anvilcraft/lib/v2/ui/Constraints.java | 8 +- .../dev/anvilcraft/lib/v2/ui/LayoutRect.java | 6 +- .../lib/v2/ui/component/BoxComponent.java | 18 +-- .../lib/v2/ui/component/ButtonComponent.java | 8 +- .../v2/ui/component/CheckboxComponent.java | 19 +-- .../lib/v2/ui/component/ColumnComponent.java | 1 + .../v2/ui/component/DropdownComponent.java | 153 +++++++++--------- .../lib/v2/ui/component/GridComponent.java | 1 + .../lib/v2/ui/component/RowComponent.java | 1 + .../v2/ui/component/ScrollableComponent.java | 82 +++++----- .../lib/v2/ui/component/SliderComponent.java | 10 +- .../lib/v2/ui/component/TextComponent.java | 8 +- .../v2/ui/component/TextInputComponent.java | 51 ++++-- .../lib/v2/ui/modifier/BackgroundElement.java | 4 +- .../lib/v2/ui/modifier/BorderElement.java | 6 +- .../lib/v2/ui/modifier/CombinedModifier.java | 6 +- .../lib/v2/ui/modifier/PaddingElement.java | 12 +- .../v2/ui/modifier/SingleElementModifier.java | 6 +- .../lib/v2/ui/modifier/SizeElement.java | 8 +- 22 files changed, 234 insertions(+), 212 deletions(-) diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Alignment.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Alignment.java index c33eff11..df19ece9 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Alignment.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Alignment.java @@ -26,9 +26,9 @@ public enum Horizontal { */ public float align(float totalWidth, float childWidth) { return switch (this) { - case Start -> 0; - case Center -> (totalWidth - childWidth) / 2; - case End -> totalWidth - childWidth; + case Horizontal.Start -> 0; + case Horizontal.Center -> (totalWidth - childWidth) / 2; + case Horizontal.End -> totalWidth - childWidth; }; } } @@ -46,9 +46,9 @@ public enum Vertical { */ public float align(float totalHeight, float childHeight) { return switch (this) { - case Top -> 0; - case Center -> (totalHeight - childHeight) / 2; - case Bottom -> totalHeight - childHeight; + case Vertical.Top -> 0; + case Vertical.Center -> (totalHeight - childHeight) / 2; + case Vertical.Bottom -> totalHeight - childHeight; }; } } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Animatable.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Animatable.java index 51bcbcf7..d639c6ea 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Animatable.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Animatable.java @@ -45,7 +45,7 @@ public void animateTo(float target, int durationTicks) { this.elapsed = 0; return; } - this.startValue = this.value; + this.startValue = this.getValue(); this.targetValue = target; this.durationTicks = durationTicks; this.elapsed = 0; diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Arrangement.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Arrangement.java index f506c5c0..6756e05a 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Arrangement.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Arrangement.java @@ -38,28 +38,28 @@ public float[] arrange(float totalHeight, List childHeights, float spacin float[] offsets = new float[n]; switch (this) { - case Top -> { + case Vertical.Top -> { float y = 0; for (int i = 0; i < n; i++) { offsets[i] = y; y += childHeights.get(i) + spacing; } } - case Center -> { + case Vertical.Center -> { float y = Math.max(0, extra / 2); for (int i = 0; i < n; i++) { offsets[i] = y; y += childHeights.get(i) + spacing; } } - case Bottom -> { + case Vertical.Bottom -> { float y = Math.max(0, extra); for (int i = 0; i < n; i++) { offsets[i] = y; y += childHeights.get(i) + spacing; } } - case SpaceBetween -> { + case Vertical.SpaceBetween -> { float gap = n > 1 ? (extra + gapTotal) / (n - 1) : 0; float y = 0; for (int i = 0; i < n; i++) { @@ -67,7 +67,7 @@ public float[] arrange(float totalHeight, List childHeights, float spacin y += childHeights.get(i) + gap; } } - case SpaceAround -> { + case Vertical.SpaceAround -> { float halfGap = n > 0 ? (extra + gapTotal) / (n * 2f) : 0; float y = halfGap; for (int i = 0; i < n; i++) { @@ -75,7 +75,7 @@ public float[] arrange(float totalHeight, List childHeights, float spacin y += childHeights.get(i) + spacing + halfGap * 2 - spacing; } } - case SpaceEvenly -> { + case Vertical.SpaceEvenly -> { float gap = n > 0 ? (extra + gapTotal) / (n + 1) : 0; float y = gap; for (int i = 0; i < n; i++) { @@ -111,28 +111,28 @@ public float[] arrange(float totalWidth, List childWidths, float spacing) float[] offsets = new float[n]; switch (this) { - case Start -> { + case Horizontal.Start -> { float x = 0; for (int i = 0; i < n; i++) { offsets[i] = x; x += childWidths.get(i) + spacing; } } - case Center -> { + case Horizontal.Center -> { float x = Math.max(0, extra / 2); for (int i = 0; i < n; i++) { offsets[i] = x; x += childWidths.get(i) + spacing; } } - case End -> { + case Horizontal.End -> { float x = Math.max(0, extra); for (int i = 0; i < n; i++) { offsets[i] = x; x += childWidths.get(i) + spacing; } } - case SpaceBetween -> { + case Horizontal.SpaceBetween -> { float gap = n > 1 ? (extra + gapTotal) / (n - 1) : 0; float x = 0; for (int i = 0; i < n; i++) { @@ -140,7 +140,7 @@ public float[] arrange(float totalWidth, List childWidths, float spacing) x += childWidths.get(i) + gap; } } - case SpaceAround -> { + case Horizontal.SpaceAround -> { float halfGap = n > 0 ? (extra + gapTotal) / (n * 2f) : 0; float x = halfGap; for (int i = 0; i < n; i++) { @@ -148,7 +148,7 @@ public float[] arrange(float totalWidth, List childWidths, float spacing) x += childWidths.get(i) + spacing + halfGap * 2 - spacing; } } - case SpaceEvenly -> { + case Horizontal.SpaceEvenly -> { float gap = n > 0 ? (extra + gapTotal) / (n + 1) : 0; float x = gap; for (int i = 0; i < n; i++) { diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Constraints.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Constraints.java index 8dbf2db6..64bff17f 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Constraints.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Constraints.java @@ -13,19 +13,19 @@ public record Constraints(float minWidth, float maxWidth, float minHeight, float public static final Constraints NONE = new Constraints(0, Float.MAX_VALUE, 0, Float.MAX_VALUE); public float constrainWidth(float w) { - return Math.clamp(w, minWidth, maxWidth); + return Math.clamp(w, this.minWidth(), this.maxWidth()); } public float constrainHeight(float h) { - return Math.clamp(h, minHeight, maxHeight); + return Math.clamp(h, this.minHeight(), this.maxHeight()); } public Constraints withWidth(float width) { - return new Constraints(width, width, minHeight, maxHeight); + return new Constraints(width, width, this.minHeight(), this.maxHeight()); } public Constraints withHeight(float height) { - return new Constraints(minWidth, maxWidth, height, height); + return new Constraints(this.minWidth(), this.maxWidth(), height, height); } public Constraints copy(float minW, float maxW, float minH, float maxH) { diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/LayoutRect.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/LayoutRect.java index b2bd2634..80477956 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/LayoutRect.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/LayoutRect.java @@ -15,14 +15,14 @@ public static LayoutRect of(float x, float y, float width, float height) { } public float right() { - return x + width; + return this.x() + this.width(); } public float bottom() { - return y + height; + return this.y() + this.height(); } public boolean contains(float px, float py) { - return px >= x && px < x + width && py >= y && py < y + height; + return px >= this.x() && px < this.x() + this.width() && py >= this.y() && py < this.y() + this.height(); } } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/BoxComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/BoxComponent.java index a2d426ee..31446c1c 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/BoxComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/BoxComponent.java @@ -56,13 +56,13 @@ public BoxComponent contentAlignment(Alignment.Horizontal h, Alignment.Vertical @Override public MeasuredSize measure(Constraints constraints) { - if (children.isEmpty()) return MeasuredSize.ZERO; + if (this.children.isEmpty()) return MeasuredSize.ZERO; float maxWidth = 0; float maxHeight = 0; - List sizes = new ArrayList<>(children.size()); + List sizes = new ArrayList<>(this.children.size()); - for (UIComponent child : children) { + for (UIComponent child : this.children) { MeasuredSize size = child.measure(constraints); sizes.add(size); maxWidth = Math.max(maxWidth, size.width()); @@ -83,18 +83,18 @@ public void layout(float x, float y, float width, float height) { this.width = width; this.height = height; - for (int i = 0; i < children.size(); i++) { - UIComponent child = children.get(i); - MeasuredSize size = childSizes.get(i); - float childX = x + contentAlignmentH.align(width, size.width()); - float childY = y + contentAlignmentV.align(height, size.height()); + for (int i = 0; i < this.children.size(); i++) { + UIComponent child = this.children.get(i); + MeasuredSize size = this.childSizes.get(i); + float childX = x + this.contentAlignmentH.align(width, size.width()); + float childY = y + this.contentAlignmentV.align(height, size.height()); child.layout(childX, childY, size.width(), size.height()); } } @Override public void extractRenderState(GuiGraphicsExtractor extractor) { - for (UIComponent child : children) { + for (UIComponent child : this.children) { child.extractRenderState(extractor); } } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ButtonComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ButtonComponent.java index a8fd91bb..3c41a659 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ButtonComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ButtonComponent.java @@ -51,8 +51,8 @@ public MeasuredSize measure(Constraints constraints) { float textW = font.width(this.label); float textH = font.lineHeight; return MeasuredSize.of( - constraints.constrainWidth(textW + PADDING_H * 2), - constraints.constrainHeight(textH + PADDING_V * 2) + constraints.constrainWidth(textW + ButtonComponent.PADDING_H * 2), + constraints.constrainHeight(textH + ButtonComponent.PADDING_V * 2) ); } @@ -66,7 +66,7 @@ public void layout(float x, float y, float width, float height) { @Override public void extractRenderState(GuiGraphicsExtractor extractor) { - int bg = this.hovered ? BG_HOVER_COLOR : BG_COLOR; + int bg = this.hovered ? ButtonComponent.BG_HOVER_COLOR : ButtonComponent.BG_COLOR; int ix = (int) this.x, iy = (int) this.y, iw = (int) this.width, ih = (int) this.height; extractor.fill(ix, iy, ix + iw, iy + ih, bg); @@ -74,7 +74,7 @@ public void extractRenderState(GuiGraphicsExtractor extractor) { float textW = font.width(this.label); int textX = (int) (this.x + (this.width - textW) / 2f); int textY = (int) (this.y + (this.height - font.lineHeight) / 2f); - extractor.text(font, this.label, textX, textY, TEXT_COLOR, true); + extractor.text(font, this.label, textX, textY, ButtonComponent.TEXT_COLOR, true); } public LayoutRect hitRect() { diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/CheckboxComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/CheckboxComponent.java index 06344839..503124a5 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/CheckboxComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/CheckboxComponent.java @@ -9,6 +9,7 @@ import lombok.Setter; import lombok.experimental.Accessors; import net.minecraft.client.gui.GuiGraphicsExtractor; +import org.jspecify.annotations.Nullable; import java.util.Collections; import java.util.List; @@ -37,7 +38,7 @@ public class CheckboxComponent implements UIComponent { private String label; private boolean checked; @Setter - private Runnable onToggle; + private @Nullable Runnable onToggle; @Getter private float x, y, width, height; @@ -59,8 +60,8 @@ public MeasuredSize measure(Constraints constraints) { // 方框 + 间距 + 标签文字宽度(简化:估算每字符 7px 宽) float labelW = this.label.length() * 7f; return MeasuredSize.of( - constraints.constrainWidth(SIZE + 4 + labelW), - constraints.constrainHeight(SIZE) + constraints.constrainWidth(CheckboxComponent.SIZE + 4 + labelW), + constraints.constrainHeight(CheckboxComponent.SIZE) ); } @@ -77,15 +78,15 @@ public void extractRenderState(GuiGraphicsExtractor extractor) { int ix = (int) this.x, iy = (int) this.y; // 外层深灰方块(始终显示) - extractor.fill(ix, iy, ix + (int) SIZE, iy + (int) SIZE, BOX_COLOR); + extractor.fill(ix, iy, ix + (int) CheckboxComponent.SIZE, iy + (int) CheckboxComponent.SIZE, CheckboxComponent.BOX_COLOR); // 选中时中间白色小方块 if (this.checked) { - int iix = ix + (int) INSET; - int iiy = iy + (int) INSET; - int iiw = (int) SIZE - (int) INSET * 2; - int iih = (int) SIZE - (int) INSET * 2; - extractor.fill(iix, iiy, iix + iiw, iiy + iih, CHECKED_COLOR); + int iix = ix + (int) CheckboxComponent.INSET; + int iiy = iy + (int) CheckboxComponent.INSET; + int iiw = (int) CheckboxComponent.SIZE - (int) CheckboxComponent.INSET * 2; + int iih = (int) CheckboxComponent.SIZE - (int) CheckboxComponent.INSET * 2; + extractor.fill(iix, iiy, iix + iiw, iiy + iih, CheckboxComponent.CHECKED_COLOR); } // 标签文字 diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ColumnComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ColumnComponent.java index 02027e35..d223b049 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ColumnComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ColumnComponent.java @@ -32,6 +32,7 @@ public class ColumnComponent implements UIComponent { @Setter private float spacing; + @Getter private float x, y, width, height; public ColumnComponent(Modifier modifier) { diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/DropdownComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/DropdownComponent.java index f11ae606..244dcd6e 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/DropdownComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/DropdownComponent.java @@ -12,6 +12,7 @@ import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiGraphicsExtractor; import net.minecraft.util.Mth; +import org.jspecify.annotations.Nullable; import java.util.Collections; import java.util.List; @@ -51,7 +52,7 @@ public class DropdownComponent implements UIComponent { @Getter private float popupScrollY; @Setter - private Consumer onChange; + private @Nullable Consumer onChange; // popup 拖拽 @Getter @@ -70,12 +71,12 @@ public DropdownComponent(Modifier modifier, String[] options, int selectedIndex, public String selectedOption() { - return options.length > 0 ? options[selectedIndex] : ""; + return this.options.length > 0 ? this.options[this.selectedIndex] : ""; } public void setOpen(boolean open) { this.open = open; - if (!open) popupScrollY = 0; + if (!open) this.popupScrollY = 0; } public void setPopupScrollY(float y) { @@ -91,9 +92,9 @@ public List children() { public MeasuredSize measure(Constraints constraints) { var font = Minecraft.getInstance().font; float maxTextW = 0; - for (String opt : options) maxTextW = Math.max(maxTextW, font.width(opt)); - float w = maxTextW + PADDING_H * 2 + ARROW_SIZE + 8; - float h = font.lineHeight + PADDING_V * 2; + for (String opt : this.options) maxTextW = Math.max(maxTextW, font.width(opt)); + float w = maxTextW + DropdownComponent.PADDING_H * 2 + DropdownComponent.ARROW_SIZE + 8; + float h = font.lineHeight + DropdownComponent.PADDING_V * 2; return MeasuredSize.of(constraints.constrainWidth(w), constraints.constrainHeight(h)); } @@ -109,32 +110,32 @@ public void layout(float x, float y, float width, float height) { @Override public void extractRenderState(GuiGraphicsExtractor extractor) { - renderTrigger(extractor); + this.renderTrigger(extractor); // 弹出层由 DeclarativeScreen 在最后统一渲染 } private void renderTrigger(GuiGraphicsExtractor extractor) { var font = Minecraft.getInstance().font; - int ix = (int) x, iy = (int) y, iw = (int) width, ih = (int) height; - int bg = open ? HOVER_COLOR : BG_COLOR; + int ix = (int) this.x, iy = (int) this.y, iw = (int) this.width, ih = (int) this.height; + int bg = this.open ? DropdownComponent.HOVER_COLOR : DropdownComponent.BG_COLOR; extractor.fill(ix, iy, ix + iw, iy + ih, bg); - String label = options.length > 0 ? options[selectedIndex] : ""; - int textY = (int) (y + (height + font.lineHeight) / 2f - font.lineHeight); - extractor.text(font, label, ix + (int) PADDING_H, textY, TEXT_COLOR); + String label = this.options.length > 0 ? this.options[this.selectedIndex] : ""; + int textY = (int) (this.y + (this.height + font.lineHeight) / 2f - font.lineHeight); + extractor.text(font, label, ix + (int) DropdownComponent.PADDING_H, textY, DropdownComponent.TEXT_COLOR); // SDF 三角箭头 - float arrowCx = x + width - PADDING_H - ARROW_SIZE / 2f; - float arrowCy = y + height / 2f; - float hs = ARROW_SIZE / 2f; - if (open) { + float arrowCx = this.x + this.width - DropdownComponent.PADDING_H - DropdownComponent.ARROW_SIZE / 2f; + float arrowCy = this.y + this.height / 2f; + float hs = DropdownComponent.ARROW_SIZE / 2f; + if (this.open) { SdfGraphics.instance .triangle( arrowCx - hs, arrowCy + hs * 0.6f, arrowCx + hs, arrowCy + hs * 0.6f, arrowCx, arrowCy - hs * 0.8f ) - .color(TEXT_COLOR) + .color(DropdownComponent.TEXT_COLOR) .fill() .draw(extractor); } else { @@ -144,7 +145,7 @@ private void renderTrigger(GuiGraphicsExtractor extractor) { arrowCx + hs, arrowCy - hs * 0.6f, arrowCx, arrowCy + hs * 0.8f ) - .color(TEXT_COLOR) + .color(DropdownComponent.TEXT_COLOR) .fill() .draw(extractor); } @@ -163,58 +164,58 @@ public float itemHeight() { * 实际弹出层高度(min 内容高度, maxPopupHeight)。 */ public float popupHeight() { - if (options.length == 0) return 0; - return Math.min(options.length * itemHeight(), maxPopupHeight); + if (this.options.length == 0) return 0; + return Math.min(this.options.length * this.itemHeight(), this.maxPopupHeight); } /** * 弹出层包围盒。 */ public LayoutRect popupRect() { - float ph = popupHeight(); - return LayoutRect.of(x, y + height, width, ph); + float ph = this.popupHeight(); + return LayoutRect.of(this.x, this.y + this.height, this.width, ph); } /** * 延迟渲染弹出层(在所有组件之后调用)。 */ public void renderPopup(GuiGraphicsExtractor extractor) { - if (!open || options.length == 0) return; + if (!this.open || this.options.length == 0) return; var font = Minecraft.getInstance().font; - float itemH = itemHeight(); - float ph = popupHeight(); - float totalH = options.length * itemH; + float itemH = this.itemHeight(); + float ph = this.popupHeight(); + float totalH = this.options.length * itemH; float maxScroll = Math.max(0, totalH - ph); - popupScrollY = Mth.clamp(popupScrollY, -maxScroll, 0); + this.popupScrollY = Mth.clamp(this.popupScrollY, -maxScroll, 0); - int px = (int) x, py = (int) (y + height), pw = (int) width; + int px = (int) this.x, py = (int) (this.y + this.height), pw = (int) this.width; extractor.enableScissor(px, py, px + pw, py + (int) ph); - extractor.fill(px, py, px + pw, py + (int) ph, POPUP_BG); + extractor.fill(px, py, px + pw, py + (int) ph, DropdownComponent.POPUP_BG); - float startY = y + height + popupScrollY; - for (int i = 0; i < options.length; i++) { + float startY = this.y + this.height + this.popupScrollY; + for (int i = 0; i < this.options.length; i++) { float iy = startY + i * itemH; float bottom = iy + itemH; // 跳过完全不可见的项 - if (bottom <= y + height || iy >= y + height + ph) continue; + if (bottom <= this.y + this.height || iy >= this.y + this.height + ph) continue; - int bg = (i == selectedIndex) ? POPUP_HOVER : POPUP_BG; + int bg = (i == this.selectedIndex) ? DropdownComponent.POPUP_HOVER : DropdownComponent.POPUP_BG; int fillTop = Math.max((int) iy, py); int fillBot = Math.min((int) bottom, py + (int) ph); extractor.fill(px, fillTop, px + pw, fillBot, bg); - extractor.text(font, options[i], px + (int) PADDING_H, (int) iy + 2, TEXT_COLOR); + extractor.text(font, this.options[i], px + (int) DropdownComponent.PADDING_H, (int) iy + 2, DropdownComponent.TEXT_COLOR); } extractor.disableScissor(); // 滚动条 if (totalH > ph) { float bh = Math.max(12, ph * ph / totalH); - float by = py + (-popupScrollY / maxScroll) * (ph - bh); - int bx = px + pw - SCROLLBAR_W - 1; - extractor.fill(bx, py, bx + SCROLLBAR_W, py + (int) ph, SCROLLBAR_BG); - extractor.fill(bx, (int) by, bx + SCROLLBAR_W, (int) (by + bh), SCROLLBAR_COLOR); + float by = py + (-this.popupScrollY / maxScroll) * (ph - bh); + int bx = px + pw - DropdownComponent.SCROLLBAR_W - 1; + extractor.fill(bx, py, bx + DropdownComponent.SCROLLBAR_W, py + (int) ph, DropdownComponent.SCROLLBAR_BG); + extractor.fill(bx, (int) by, bx + DropdownComponent.SCROLLBAR_W, (int) (by + bh), DropdownComponent.SCROLLBAR_COLOR); } } @@ -224,9 +225,9 @@ public void renderPopup(GuiGraphicsExtractor extractor) { * 点击触发器区域 → 切换展开。 */ public boolean clickTrigger(float px, float py) { - if (triggerRect().contains(px, py)) { - open = !open; - if (!open) popupScrollY = 0; + if (this.triggerRect().contains(px, py)) { + this.open = !this.open; + if (!this.open) this.popupScrollY = 0; return true; } return false; @@ -236,15 +237,15 @@ public boolean clickTrigger(float px, float py) { * 点击弹出层选项 → 选中并收起。返回 true 表示命中。 */ public boolean clickPopup(float px, float py) { - if (!open || options.length == 0) return false; - float ph = popupHeight(); - if (px < x || px > x + width || py < y + height || py > y + height + ph) return false; + if (!this.open || this.options.length == 0) return false; + float ph = this.popupHeight(); + if (px < this.x || px > this.x + this.width || py < this.y + this.height || py > this.y + this.height + ph) return false; - float itemH = itemHeight(); - float idxF = (py - y - height - popupScrollY) / itemH; + float itemH = this.itemHeight(); + float idxF = (py - this.y - this.height - this.popupScrollY) / itemH; int idx = (int) idxF; - if (idx >= 0 && idx < options.length) { - select(idx); + if (idx >= 0 && idx < this.options.length) { + this.select(idx); return true; } return false; @@ -254,12 +255,12 @@ public boolean clickPopup(float px, float py) { * 滚轮滚动弹出层。 */ public boolean onPopupScroll(float amount) { - if (!open) return false; - float ph = popupHeight(); - float totalH = options.length * itemHeight(); + if (!this.open) return false; + float ph = this.popupHeight(); + float totalH = this.options.length * this.itemHeight(); if (totalH <= ph) return false; float maxScroll = totalH - ph; - popupScrollY = Mth.clamp(popupScrollY + amount * 20, -maxScroll, 0); + this.popupScrollY = Mth.clamp(this.popupScrollY + amount * 20, -maxScroll, 0); return true; } @@ -267,56 +268,56 @@ public boolean onPopupScroll(float amount) { * 弹出层滚动条命中测试。 */ public boolean isOnPopupScrollbar(float mx, float my) { - if (!open) return false; - float ph = popupHeight(); - float totalH = options.length * itemHeight(); + if (!this.open) return false; + float ph = this.popupHeight(); + float totalH = this.options.length * this.itemHeight(); if (totalH <= ph) return false; float bh = Math.max(12, ph * ph / totalH); float maxScroll = totalH - ph; - float by = y + height + (-popupScrollY / maxScroll) * (ph - bh); - int bx = (int) (x + width - SCROLLBAR_W - 1); - return mx >= bx && mx < bx + SCROLLBAR_W && my >= by && my < by + bh; + float by = this.y + this.height + (-this.popupScrollY / maxScroll) * (ph - bh); + int bx = (int) (this.x + this.width - DropdownComponent.SCROLLBAR_W - 1); + return mx >= bx && mx < bx + DropdownComponent.SCROLLBAR_W && my >= by && my < by + bh; } public void startPopupScrollbarDrag(float my) { - scrollbarDragging = true; - float ph = popupHeight(); - float totalH = options.length * itemHeight(); + this.scrollbarDragging = true; + float ph = this.popupHeight(); + float totalH = this.options.length * this.itemHeight(); float bh = Math.max(12, ph * ph / totalH); float maxScroll = totalH - ph; - float by = y + height + (-popupScrollY / maxScroll) * (ph - bh); - dragAnchorY = my - by; + float by = this.y + this.height + (-this.popupScrollY / maxScroll) * (ph - bh); + this.dragAnchorY = my - by; } public void onPopupScrollbarDrag(float my) { - if (!scrollbarDragging) return; - float ph = popupHeight(); - float totalH = options.length * itemHeight(); + if (!this.scrollbarDragging) return; + float ph = this.popupHeight(); + float totalH = this.options.length * this.itemHeight(); float bh = Math.max(12, ph * ph / totalH); float maxScroll = totalH - ph; - float newBarY = my - dragAnchorY; + float newBarY = my - this.dragAnchorY; float ratio = Mth.clamp(newBarY / (ph - bh), 0f, 1f); - popupScrollY = -(ratio * maxScroll); + this.popupScrollY = -(ratio * maxScroll); } public void stopPopupScrollbarDrag() { - scrollbarDragging = false; + this.scrollbarDragging = false; } private void select(int idx) { - if (idx != selectedIndex) { - selectedIndex = idx; - if (onChange != null) onChange.accept(options[idx]); + if (idx != this.selectedIndex) { + this.selectedIndex = idx; + if (this.onChange != null) this.onChange.accept(this.options[idx]); } - open = false; - popupScrollY = 0; + this.open = false; + this.popupScrollY = 0; } private LayoutRect triggerRect() { - return LayoutRect.of(x, y, width, height); + return LayoutRect.of(this.x, this.y, this.width, this.height); } public LayoutRect hitRect() { - return triggerRect(); + return this.triggerRect(); } } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/GridComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/GridComponent.java index fbdba53a..74102272 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/GridComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/GridComponent.java @@ -26,6 +26,7 @@ public class GridComponent implements UIComponent { private List childSizes = Collections.emptyList(); private float hSpacing, vSpacing; + @Getter private float x, y, width, height; private float cellW, cellH; diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/RowComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/RowComponent.java index 09abf4ea..219032f5 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/RowComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/RowComponent.java @@ -32,6 +32,7 @@ public class RowComponent implements UIComponent { @Setter private float spacing; + @Getter private float x, y, width, height; public RowComponent(Modifier modifier) { diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ScrollableComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ScrollableComponent.java index 7476de50..d89951f5 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ScrollableComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ScrollableComponent.java @@ -59,20 +59,17 @@ public void setChildren(List children) { public MeasuredSize measure(Constraints constraints) { - if (children.isEmpty()) return MeasuredSize.ZERO; + if (this.children.isEmpty()) return MeasuredSize.ZERO; float maxW = 0; float totalH = 0; - List sizes = new ArrayList<>(children.size()); - Constraints childC = new Constraints(0, constraints.maxWidth() - SCROLLBAR_W - 1, 0, Float.MAX_VALUE); + List sizes = new ArrayList<>(this.children.size()); + Constraints childC = new Constraints(0, constraints.maxWidth() - ScrollableComponent.SCROLLBAR_W - 1, 0, Float.MAX_VALUE); - for (UIComponent child : children) { + for (UIComponent child : this.children) { MeasuredSize s = child.measure(childC); // 修饰符会扩展尺寸(如 padding),需要计入内容高度 - s = child.modifier().foldOut( - s, - (el, sz) -> el.modifyMeasuredSize(child, childC, sz) - ); + s = child.modifier().foldOut(s, (el, sz) -> el.modifyMeasuredSize(child, childC, sz)); sizes.add(s); totalH += s.height(); maxW = Math.max(maxW, s.width()); @@ -80,10 +77,7 @@ public MeasuredSize measure(Constraints constraints) { this.childSizes = sizes; this.contentHeight = totalH; - return MeasuredSize.of( - constraints.constrainWidth(maxW), - constraints.constrainHeight(Math.min(totalH, maxHeight)) - ); + return MeasuredSize.of(constraints.constrainWidth(maxW), constraints.constrainHeight(Math.min(totalH, this.maxHeight))); } public void layout(float x, float y, float width, float height) { @@ -92,44 +86,44 @@ public void layout(float x, float y, float width, float height) { this.width = width; this.height = height; - float currentY = y + scrollY; - float childW = width - SCROLLBAR_W - 1; - for (int i = 0; i < children.size(); i++) { - UIComponent child = children.get(i); - MeasuredSize size = childSizes.get(i); + float currentY = y + this.scrollY; + float childW = width - ScrollableComponent.SCROLLBAR_W - 1; + for (int i = 0; i < this.children.size(); i++) { + UIComponent child = this.children.get(i); + MeasuredSize size = this.childSizes.get(i); child.layout(x, currentY, childW, size.height()); currentY += size.height(); } } public void extractRenderState(GuiGraphicsExtractor extractor) { - int ix = (int) x, iy = (int) y, iw = (int) width, ih = (int) height; + int ix = (int) this.x, iy = (int) this.y, iw = (int) this.width, ih = (int) this.height; // 裁剪到容器范围 extractor.enableScissor(ix, iy, ix + iw, iy + ih); - for (UIComponent child : children) { + for (UIComponent child : this.children) { child.extractRenderState(extractor); } extractor.disableScissor(); // 滚动条 - if (contentHeight > height) { + if (this.contentHeight > this.height) { float bh = barH(); float by = barY(); - int bx = (int) (x + width - SCROLLBAR_W - 1); - extractor.fill(bx, iy, bx + SCROLLBAR_W, iy + ih, SCROLLBAR_BG); - extractor.fill(bx, (int) by, bx + SCROLLBAR_W, (int) (by + bh), SCROLLBAR_COLOR); + int bx = (int) (this.x + this.width - ScrollableComponent.SCROLLBAR_W - 1); + extractor.fill(bx, iy, bx + ScrollableComponent.SCROLLBAR_W, iy + ih, ScrollableComponent.SCROLLBAR_BG); + extractor.fill(bx, (int) by, bx + ScrollableComponent.SCROLLBAR_W, (int) (by + bh), ScrollableComponent.SCROLLBAR_COLOR); } } // ── 滚动 ── public boolean onScroll(float amount) { - if (contentHeight <= height) return false; - float maxScroll = contentHeight - height; - scrollY = Mth.clamp(scrollY + amount * 20, -maxScroll, 0); + if (this.contentHeight <= this.height) return false; + float maxScroll = this.contentHeight - this.height; + this.scrollY = Mth.clamp(this.scrollY + amount * 20, -maxScroll, 0); return true; } @@ -137,48 +131,48 @@ public boolean onScroll(float amount) { * 鼠标是否在滚动条滑块上。 */ public boolean isOnScrollbar(float mx, float my) { - if (contentHeight <= height) return false; - float bh = barH(); - float by = barY(); - int bx = (int) (x + width - SCROLLBAR_W - 1); - return mx >= bx && mx < bx + SCROLLBAR_W && my >= by && my < by + bh; + if (this.contentHeight <= this.height) return false; + float bh = this.barH(); + float by = this.barY(); + int bx = (int) (this.x + this.width - ScrollableComponent.SCROLLBAR_W - 1); + return mx >= bx && mx < bx + ScrollableComponent.SCROLLBAR_W && my >= by && my < by + bh; } /** * 开始拖拽滚动条。 */ public void startScrollbarDrag(float my) { - scrollbarDragging = true; - dragAnchorY = my - barY(); + this.scrollbarDragging = true; + this.dragAnchorY = my - this.barY(); } /** * 拖拽滚动条时更新位置。 */ public void onScrollbarDrag(float my) { - if (!scrollbarDragging) return; - float bh = barH(); - float maxScroll = contentHeight - height; - float newBarY = my - dragAnchorY; - float ratio = Mth.clamp(newBarY / (height - bh), 0f, 1f); - scrollY = -(ratio * maxScroll); + if (!this.scrollbarDragging) return; + float bh = this.barH(); + float maxScroll = this.contentHeight - this.height; + float newBarY = my - this.dragAnchorY; + float ratio = Mth.clamp(newBarY / (this.height - bh), 0f, 1f); + this.scrollY = -(ratio * maxScroll); } /** * 停止拖拽。 */ public void stopScrollbarDrag() { - scrollbarDragging = false; + this.scrollbarDragging = false; } private float barH() { - return Math.max(16, height * height / contentHeight); + return Math.max(16, this.height * this.height / this.contentHeight); } private float barY() { - float maxScroll = contentHeight - height; - return y + (-scrollY / maxScroll) * (height - barH()); + float maxScroll = this.contentHeight - this.height; + return this.y + (-this.scrollY / maxScroll) * (this.height - this.barH()); } public void setScrollY(float scrollY) { @@ -189,6 +183,6 @@ public void setScrollY(float scrollY) { * 命中测试包围盒。 */ public LayoutRect hitRect() { - return LayoutRect.of(x, y, width, height); + return LayoutRect.of(this.x, this.y, this.width, this.height); } } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/SliderComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/SliderComponent.java index 9e8d8974..bbad881f 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/SliderComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/SliderComponent.java @@ -64,7 +64,7 @@ public List children() { public MeasuredSize measure(Constraints constraints) { return MeasuredSize.of( constraints.constrainWidth(this.trackWidth), - constraints.constrainHeight(THUMB_H) + constraints.constrainHeight(SliderComponent.THUMB_H) ); } @@ -82,14 +82,14 @@ public void extractRenderState(GuiGraphicsExtractor extractor) { int iw = (int) this.width; // 轨道 — 居中画在 THUMB_H 中间 - int trackY = (int) (this.y + (THUMB_H - TRACK_H) / 2f); - extractor.fill(ix, trackY, ix + iw, (int) (trackY + TRACK_H), TRACK_COLOR); + int trackY = (int) (this.y + (SliderComponent.THUMB_H - SliderComponent.TRACK_H) / 2f); + extractor.fill(ix, trackY, ix + iw, (int) (trackY + SliderComponent.TRACK_H), SliderComponent.TRACK_COLOR); // 滑块 — 按比例定位 float ratio = (this.value - this.min) / (this.max - this.min); - float thumbX = this.x + ratio * (this.width - THUMB_W); + float thumbX = this.x + ratio * (this.width - SliderComponent.THUMB_W); int tix = (int) thumbX, tiy = (int) this.y; - extractor.fill(tix, tiy, tix + (int) THUMB_W, tiy + (int) THUMB_H, THUMB_COLOR); + extractor.fill(tix, tiy, tix + (int) SliderComponent.THUMB_W, tiy + (int) THUMB_H, THUMB_COLOR); } /** diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextComponent.java index c467f444..b3a396a9 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextComponent.java @@ -32,7 +32,7 @@ public class TextComponent implements UIComponent { @Setter private String text; @Setter - private int color = VANILLA_TEXT_COLOR; + private int color = TextComponent.VANILLA_TEXT_COLOR; @Setter private boolean shadow; @Setter @@ -74,9 +74,9 @@ public void extractRenderState(GuiGraphicsExtractor extractor) { float textW = font.width(component); float renderX = switch (this.align) { - case LEFT -> this.x; - case CENTER -> this.x + (this.width - textW) / 2f; - case RIGHT -> this.x + this.width - textW; + case Align.LEFT -> this.x; + case Align.CENTER -> this.x + (this.width - textW) / 2f; + case Align.RIGHT -> this.x + this.width - textW; }; extractor.text(font, this.text, (int) renderX, (int) this.y, this.color, this.shadow); diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextInputComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextInputComponent.java index b09d19f5..c5876bad 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextInputComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextInputComponent.java @@ -21,9 +21,13 @@ import java.util.function.Consumer; @Accessors(fluent = true) -@SuppressWarnings({"unused", "UnusedReturnValue"}) +@SuppressWarnings( + { + "unused", + "UnusedReturnValue" + } +) public class TextInputComponent implements UIComponent, KeyInputHandler { - private static final int BG_COLOR = 0xFF202020; private static final int TEXT_COLOR = 0xFFFFFFFF; private static final int PLACEHOLDER_COLOR = 0xFF555555; @@ -32,7 +36,8 @@ public class TextInputComponent implements UIComponent, KeyInputHandler { private static final float PADDING_V = 4; private static final float WIDTH = 160; private final String placeholder; - @Getter @Setter + @Getter + @Setter private Modifier modifier; @Getter private String value = ""; @@ -64,12 +69,18 @@ public void setFocused(boolean focused) { this.focused = focused; } - @Override public List children() { return Collections.emptyList(); } + @Override + public List children() { + return Collections.emptyList(); + } @Override public MeasuredSize measure(Constraints constraints) { var font = Minecraft.getInstance().font; - return MeasuredSize.of(constraints.constrainWidth(WIDTH), constraints.constrainHeight(font.lineHeight + PADDING_V * 2)); + return MeasuredSize.of( + constraints.constrainWidth(TextInputComponent.WIDTH), + constraints.constrainHeight(font.lineHeight + PADDING_V * 2) + ); } @Override @@ -83,21 +94,21 @@ public void layout(float x, float y, float width, float height) { @Override public void extractRenderState(GuiGraphicsExtractor extractor) { int ix = (int) this.x, iy = (int) this.y, iw = (int) this.width, ih = (int) this.height; - extractor.fill(ix, iy, ix + iw, iy + ih, BG_COLOR); + extractor.fill(ix, iy, ix + iw, iy + ih, TextInputComponent.BG_COLOR); var font = Minecraft.getInstance().font; - int textX = ix + (int) PADDING_H; + int textX = ix + (int) TextInputComponent.PADDING_H; int textY = (int) (this.y + (this.height + font.lineHeight) / 2f - font.lineHeight); boolean hasText = !this.value.isEmpty(); String display = hasText ? this.value : this.placeholder; - int color = hasText ? TEXT_COLOR : PLACEHOLDER_COLOR; + int color = hasText ? TextInputComponent.TEXT_COLOR : TextInputComponent.PLACEHOLDER_COLOR; extractor.text(font, display, textX, textY, color); if (this.focused && hasText) { String before = this.value.substring(0, Math.min(this.cursorPos, this.value.length())); - int cursorX = (int) (this.x + PADDING_H + font.width(before)); - extractor.fill(cursorX, iy + 2, cursorX + 1, iy + ih - 2, CURSOR_COLOR); + int cursorX = (int) (this.x + TextInputComponent.PADDING_H + font.width(before)); + extractor.fill(cursorX, iy + 2, cursorX + 1, iy + ih - 2, TextInputComponent.CURSOR_COLOR); } } @@ -119,10 +130,22 @@ public boolean onKeyPressed(KeyEvent event) { } return true; } - if (key == 263) { if (this.cursorPos > 0) this.cursorPos--; return true; } - if (key == 262) { if (this.cursorPos < this.value.length()) this.cursorPos++; return true; } - if (key == 268) { this.cursorPos = 0; return true; } - if (key == 269) { this.cursorPos = this.value.length(); return true; } + if (key == 263) { + if (this.cursorPos > 0) this.cursorPos--; + return true; + } + if (key == 262) { + if (this.cursorPos < this.value.length()) this.cursorPos++; + return true; + } + if (key == 268) { + this.cursorPos = 0; + return true; + } + if (key == 269) { + this.cursorPos = this.value.length(); + return true; + } return false; } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/BackgroundElement.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/BackgroundElement.java index 5367ae05..93125efe 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/BackgroundElement.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/BackgroundElement.java @@ -22,8 +22,8 @@ public BackgroundElement(int color) { public void emitRenderState(GuiGraphicsExtractor extractor, LayoutRect bounds) { SdfGraphics.instance .box(bounds.x(), bounds.y(), bounds.width(), bounds.height()) - .color(color) - .round(round) + .color(this.color()) + .round(this.round()) .fill() .draw(extractor); } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/BorderElement.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/BorderElement.java index d9f6f1c6..1c69c185 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/BorderElement.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/BorderElement.java @@ -22,9 +22,9 @@ public BorderElement(float width, int color) { public void emitRenderState(GuiGraphicsExtractor extractor, LayoutRect bounds) { SdfGraphics.instance .box(bounds.x(), bounds.y(), bounds.width(), bounds.height()) - .color(color) - .round(round) - .stroke(width) + .color(this.color()) + .round(this.round()) + .stroke(this.width()) .draw(extractor); } } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/CombinedModifier.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/CombinedModifier.java index d7919099..a97218ac 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/CombinedModifier.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/CombinedModifier.java @@ -13,16 +13,16 @@ public record CombinedModifier(ModifierElement outer, Modifier inner) implements Modifier { @Override public Modifier then(Modifier other) { - return new CombinedModifier(outer, inner.then(other)); + return new CombinedModifier(this.outer(), this.inner().then(other)); } @Override public R foldIn(R initial, BiFunction operation) { - return inner.foldIn(operation.apply(initial, outer), operation); + return this.inner().foldIn(operation.apply(initial, this.outer()), operation); } @Override public R foldOut(R initial, BiFunction operation) { - return operation.apply(outer, inner.foldOut(initial, operation)); + return operation.apply(this.outer(), this.inner().foldOut(initial, operation)); } } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/PaddingElement.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/PaddingElement.java index e2fbd454..301a41e8 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/PaddingElement.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/PaddingElement.java @@ -15,18 +15,18 @@ public record PaddingElement(float left, float top, float right, float bottom) i @Override public MeasuredSize modifyMeasuredSize(UIComponent component, Constraints constraints, MeasuredSize childSize) { return MeasuredSize.of( - childSize.width() + left + right, - childSize.height() + top + bottom + childSize.width() + this.left() + this.right(), + childSize.height() + this.top() + this.bottom() ); } @Override public LayoutRect modifyLayout(LayoutRect rect) { return LayoutRect.of( - rect.x() + left, - rect.y() + top, - rect.width() - left - right, - rect.height() - top - bottom + rect.x() + this.left(), + rect.y() + this.top(), + rect.width() - this.left() - this.right(), + rect.height() - this.top() - this.bottom() ); } } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/SingleElementModifier.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/SingleElementModifier.java index 8605dc9e..5201dc94 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/SingleElementModifier.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/SingleElementModifier.java @@ -13,16 +13,16 @@ public record SingleElementModifier(ModifierElement element) implements Modifier { @Override public Modifier then(Modifier other) { - return new CombinedModifier(element, other); + return new CombinedModifier(this.element(), other); } @Override public R foldIn(R initial, BiFunction operation) { - return operation.apply(initial, element); + return operation.apply(initial, this.element()); } @Override public R foldOut(R initial, BiFunction operation) { - return operation.apply(element, initial); + return operation.apply(this.element(), initial); } } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/SizeElement.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/SizeElement.java index 61e54152..f8c80af9 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/SizeElement.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/SizeElement.java @@ -12,10 +12,10 @@ public record SizeElement(float minWidthHint, float maxWidthHint, float minHeigh implements ModifierElement { @Override public Constraints modifyConstraints(Constraints constraints) { - float minW = minWidthHint > 0 ? Math.max(constraints.minWidth(), minWidthHint) : constraints.minWidth(); - float maxW = maxWidthHint < Float.MAX_VALUE ? Math.min(constraints.maxWidth(), maxWidthHint) : constraints.maxWidth(); - float minH = minHeightHint > 0 ? Math.max(constraints.minHeight(), minHeightHint) : constraints.minHeight(); - float maxH = maxHeightHint < Float.MAX_VALUE ? Math.min(constraints.maxHeight(), maxHeightHint) : constraints.maxHeight(); + float minW = this.minWidthHint() > 0 ? Math.max(constraints.minWidth(), this.minWidthHint()) : constraints.minWidth(); + float maxW = this.maxWidthHint() < Float.MAX_VALUE ? Math.min(constraints.maxWidth(), this.maxWidthHint()) : constraints.maxWidth(); + float minH = this.minHeightHint() > 0 ? Math.max(constraints.minHeight(), this.minHeightHint()) : constraints.minHeight(); + float maxH = this.maxHeightHint() < Float.MAX_VALUE ? Math.min(constraints.maxHeight(), this.maxHeightHint()) : constraints.maxHeight(); return constraints.copy(minW, maxW, minH, maxH); } } From 2b74d03f674b88d28447ff49d6e45b30e1024cbe Mon Sep 17 00:00:00 2001 From: Gugle Date: Tue, 19 May 2026 04:12:28 +0800 Subject: [PATCH 49/67] feat(ui): enhance null safety by adding NonNull annotations and refining Ref class --- .../client/screen/DeclarativeTestScreen.java | 209 +++++++++--------- .../java/dev/anvilcraft/lib/v2/ui/Ref.java | 9 +- 2 files changed, 110 insertions(+), 108 deletions(-) diff --git a/module.test/src/main/java/dev/anvilcraft/lib/v2/test/client/screen/DeclarativeTestScreen.java b/module.test/src/main/java/dev/anvilcraft/lib/v2/test/client/screen/DeclarativeTestScreen.java index b9a1deb7..c671fef5 100644 --- a/module.test/src/main/java/dev/anvilcraft/lib/v2/test/client/screen/DeclarativeTestScreen.java +++ b/module.test/src/main/java/dev/anvilcraft/lib/v2/test/client/screen/DeclarativeTestScreen.java @@ -9,6 +9,7 @@ import dev.anvilcraft.lib.v2.ui.UIScope; import dev.anvilcraft.lib.v2.ui.component.TextComponent; import net.minecraft.network.chat.Component; +import org.jspecify.annotations.NonNull; import java.util.List; @@ -23,113 +24,115 @@ public DeclarativeTestScreen() { @Override protected void content(UIScope scope) { + if (scope == null) return; Composition comp = Composition.current(); - Ref counter = comp.ref(0); - Ref checked = comp.ref(false); - Ref text = comp.ref(""); + Ref<@NonNull Integer> counter = comp.ref(0); + Ref<@NonNull Boolean> checked = comp.ref(false); + Ref<@NonNull String> text = comp.ref(""); scope.Scrollable( - this.height, scroll -> { - scroll.Column(col -> { - // ── 标题 ── - col.Text("=== AnvilLib Declarative UI ===").color(0xFFFFFF00); - - // ── 1. Text 样式 ── - col.Text("1) Text styles:"); - col.Row(row -> { - row.Text("Shadow").shadow(true).color(0xFFFFAA00); - row.Text(" | Left").align(TextComponent.Align.LEFT); - row.Text("Center").align(TextComponent.Align.CENTER).color(0xFF00AAFF); - row.Text("Right |").align(TextComponent.Align.RIGHT); - }).spacing(8); - - col.Spacer(0, 4); - - // ── 2. Button + 状态 ── - col.Text("2) Button + state (Counter):"); - col.Row(row -> { - row.Button("-").onClick(() -> counter.setValue(counter.getValue() - 1)); - row.Text(" " + counter.getValue() + " ").align(TextComponent.Align.CENTER); - row.Button("+").onClick(() -> counter.setValue(counter.getValue() + 1)); - row.Button("Reset").onClick(() -> counter.setValue(0)); - }).spacing(4); - - col.Spacer(0, 4); - - // ── 3. Box 层叠 ── - col.Text("3) Box overlay:"); - col.Box(box -> { - box.Text(" "); - box.Text("<< Overlay Text >>").color(0xFF00FF00); - }).contentAlignment(Alignment.Horizontal.Center, Alignment.Vertical.Center); - - col.Spacer(0, 4); - - // ── 4. Grid 网格 ── - col.Text("4) Grid (3 columns):"); - col.Grid( - 3, grid -> { - for (int i = 0; i < 6; i++) { - grid.Button("G" + i); - } + this.height, scroll -> scroll.Column(col -> { + // ── 标题 ── + col.Text("=== AnvilLib Declarative UI ===").color(0xFFFFFF00); + + // ── 1. Text 样式 ── + col.Text("1) Text styles:"); + col.Row(row -> { + row.Text("Shadow").shadow(true).color(0xFFFFAA00); + row.Text(" | Left").align(TextComponent.Align.LEFT); + row.Text("Center").align(TextComponent.Align.CENTER).color(0xFF00AAFF); + row.Text("Right |").align(TextComponent.Align.RIGHT); + }).spacing(8); + + col.Spacer(0, 4); + + // ── 2. Button + 状态 ── + col.Text("2) Button + state (Counter):"); + col.Row(row -> { + row.Button("-").onClick(() -> counter.setValue(counter.getValue() - 1)); + row.Text(" " + counter.getValue() + " ").align(TextComponent.Align.CENTER); + row.Button("+").onClick(() -> counter.setValue(counter.getValue() + 1)); + row.Button("Reset").onClick(() -> counter.setValue(0)); + }).spacing(4); + + col.Spacer(0, 4); + + // ── 3. Box 层叠 ── + col.Text("3) Box overlay:"); + col.Box(box -> { + box.Text(" "); + box.Text("<< Overlay Text >>").color(0xFF00FF00); + }).contentAlignment(Alignment.Horizontal.Center, Alignment.Vertical.Center); + + col.Spacer(0, 4); + + // ── 4. Grid 网格 ── + col.Text("4) Grid (3 columns):"); + col.Grid( + 3, grid -> { + for (int i = 0; i < 6; i++) { + grid.Button("G" + i); } - ).spacing(2, 2); - - col.Spacer(0, 4); - - // ── 5. Checkbox ── - col.Text("5) Checkbox:"); - col.Row(row -> { - row.Checkbox("Enable feature", checked); - row.Text(" Enabled: " + checked.getValue()); - }).spacing(4); - - col.Spacer(0, 4); - - // ── 6. Slider ── - Ref sliderVal = comp.ref(50f); - col.Text("6) Slider:"); - col.Row(row -> { - row.Slider(0, 100, 100, sliderVal); - row.Text(" " + sliderVal.getValue().intValue() + "%"); - }).spacing(4); - - col.Spacer(0, 4); - - // ── 7. TextInput ── - col.Text("7) TextInput:"); - col.Row(row -> { - row.TextInput("Enter text...", text); - row.Text(" Value: '" + text.getValue() + "'"); - }).spacing(4); - - col.Spacer(0, 4); - - // ── 8. Dropdown ── - col.Text("8) Dropdown:"); - col.Dropdown(new String[]{"Option A", "Option B", "Option C"}, 0, 80) - .onChange(v -> {}); - col.Text(" (click to open)"); - - col.Spacer(0, 4); - - // ── 9. ForEach ── - col.Text("9) ForEach (list of 4 items):"); - ForEach.of(col, List.of("Apple", "Banana", "Cherry", "Date"), (s, item) -> s.Text(" - " + item)); - - col.Spacer(0, 4); - - // ── 10. Modifier 样式 ── - col.Text("10) Modifiers:"); - col.Row(row -> { - row.Button("Styled") - .modifier(Modifier.NONE.background(0xFF884444).border(1, 0xFFFF8888)); - row.Text(" "); - row.Text("Padded").modifier(Modifier.NONE.padding(8).background(0xFF444488)); - }).spacing(4); - - }).spacing(6).modifier(Modifier.NONE.padding(10)); - } + } + ).spacing(2, 2); + + col.Spacer(0, 4); + + // ── 5. Checkbox ── + col.Text("5) Checkbox:"); + col.Row(row -> { + row.Checkbox("Enable feature", checked); + row.Text(" Enabled: " + checked.getValue()); + }).spacing(4); + + col.Spacer(0, 4); + + // ── 6. Slider ── + Ref sliderVal = comp.ref(50f); + col.Text("6) Slider:"); + col.Row(row -> { + row.Slider(0, 100, 100, sliderVal); + row.Text(" " + sliderVal.getValue().intValue() + "%"); + }).spacing(4); + + col.Spacer(0, 4); + + // ── 7. TextInput ── + col.Text("7) TextInput:"); + col.Row(row -> { + row.TextInput("Enter text...", text); + row.Text(" Value: '" + text.getValue() + "'"); + }).spacing(4); + + col.Spacer(0, 4); + + // ── 8. Dropdown ── + col.Text("8) Dropdown:"); + String[] options = new String[26]; + for (int i = 0; i < 26; i++) { + options[i] = "Option " + (char) ('A' + i); + } + col.Dropdown(options, 0, 80).onChange(_ -> { + }); + col.Text(" (click to open)"); + + col.Spacer(0, 4); + + // ── 9. ForEach ── + col.Text("9) ForEach (list of 4 items):"); + ForEach.of(col, List.of("Apple", "Banana", "Cherry", "Date"), (s, item) -> s.Text(" - " + item)); + + col.Spacer(0, 4); + + // ── 10. Modifier 样式 ── + col.Text("10) Modifiers:"); + col.Row(row -> { + row.Button("Styled").modifier(Modifier.NONE.background(0xFF884444).border(1, 0xFFFF8888)); + row.Text(" "); + row.Text("Padded").modifier(Modifier.NONE.padding(8).background(0xFF444488)); + }).spacing(4); + + }).spacing(6).modifier(Modifier.NONE.padding(10)) ); } } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Ref.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Ref.java index 5cece7ca..a1391ffd 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Ref.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Ref.java @@ -20,18 +20,17 @@ "UnusedReturnValue" } ) -public class Ref { +public class Ref { final Set readers = new HashSet<>(); - private @Nullable T value; + private T value; - public Ref(@Nullable T initialValue) { + public Ref(T initialValue) { this.value = initialValue; } /** * 读取当前值。若在 composition emission 期间调用,记录此 slot 为 reader。 */ - @Nullable public T getValue() { Composition comp = Composition.currentOrNull(); if (comp != null && comp.currentSlot != null) { @@ -44,7 +43,7 @@ public T getValue() { /** * 设置新值。若值发生变化,标记所有 reader slot 为脏。 */ - public void setValue(@Nullable T newValue) { + public void setValue(T newValue) { if (!Objects.equals(this.value, newValue)) { this.value = newValue; for (Composition.Slot slot : this.readers) { From bfa1ef0841a2ef934dae2a4af0efeb6d4d1fd5bb Mon Sep 17 00:00:00 2001 From: Gugle Date: Tue, 19 May 2026 16:54:01 +0800 Subject: [PATCH 50/67] feat(ui): implement mouse interaction for UI components with click and scroll handling --- .../lib/v2/ui/DeclarativeScreen.java | 294 ++++++------------ .../dev/anvilcraft/lib/v2/ui/Focusable.java | 14 + .../dev/anvilcraft/lib/v2/ui/UIComponent.java | 50 +-- .../lib/v2/ui/component/ButtonComponent.java | 17 + .../v2/ui/component/CheckboxComponent.java | 14 +- .../v2/ui/component/DropdownComponent.java | 35 +++ .../v2/ui/component/ScrollableComponent.java | 35 +++ .../lib/v2/ui/component/SliderComponent.java | 21 ++ .../v2/ui/component/TextInputComponent.java | 19 +- .../lib/v2/ui/input/KeyInputHandler.java | 27 -- .../lib/v2/ui/input/package-info.java | 4 - 11 files changed, 273 insertions(+), 257 deletions(-) create mode 100644 module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Focusable.java delete mode 100644 module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/input/KeyInputHandler.java delete mode 100644 module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/input/package-info.java diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java index 7131a124..25ccf8f2 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java @@ -1,29 +1,20 @@ package dev.anvilcraft.lib.v2.ui; import dev.anvilcraft.lib.v2.ui.component.ButtonComponent; -import dev.anvilcraft.lib.v2.ui.component.CheckboxComponent; import dev.anvilcraft.lib.v2.ui.component.DropdownComponent; -import dev.anvilcraft.lib.v2.ui.component.ScrollableComponent; -import dev.anvilcraft.lib.v2.ui.component.SliderComponent; -import dev.anvilcraft.lib.v2.ui.component.TextInputComponent; -import dev.anvilcraft.lib.v2.ui.input.KeyInputHandler; -import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiGraphicsExtractor; 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.client.resources.sounds.SimpleSoundInstance; import net.minecraft.network.chat.Component; -import net.minecraft.sounds.SoundEvents; - import org.jspecify.annotations.Nullable; /** * 声明式 UI 的 {@link Screen} 宿主。 *

    * 每帧自动完成:dirty check → recompose → measure → layout → render states。 - * 输入事件(点击、按键、滚轮)通过命中测试路由到对应组件。 + * 输入事件通过递归遍历组件树分发,各组件覆写 {@link UIComponent} 的事件方法处理自身逻辑。 */ @SuppressWarnings( { @@ -32,21 +23,21 @@ } ) public abstract class DeclarativeScreen extends Screen { + private final UIScope rootScope = new RootScope(); @Nullable private Composition composition; @Nullable - private KeyInputHandler focusOwner; + private UIComponent focusOwner; protected DeclarativeScreen(Component title) { super(title); } - /** - * 声明 UI 内容。初始组合和每次 recompose 时调用。 - */ protected abstract void content(@Nullable UIScope scope); + // ── 每帧渲染 ── + @Override public void extractRenderState(GuiGraphicsExtractor extractor, int mouseX, int mouseY, float partialTick) { super.extractRenderState(extractor, mouseX, mouseY, partialTick); @@ -60,25 +51,6 @@ public void extractRenderState(GuiGraphicsExtractor extractor, int mouseX, int m } } - // ── 每帧渲染 ── - - @Override - public boolean keyPressed(KeyEvent event) { - if (event.key() == 256) { - this.onClose(); - return true; - } - if (this.focusOwner != null && this.focusOwner.onKeyPressed(event)) { - return true; - } - return super.keyPressed(event); - } - - @Override - public void onClose() { - super.onClose(); - } - @Override protected void init() { this.composition = new Composition(this.rootScope); @@ -87,18 +59,15 @@ protected void init() { private void renderPopups(UIComponent component, GuiGraphicsExtractor extractor) { if (component instanceof DropdownComponent dd) dd.renderPopup(extractor); - for (UIComponent child : component.children()) this.renderPopups(child, extractor); + for (UIComponent child : component.children()) renderPopups(child, extractor); } - // ── 鼠标输入 ── + // ── 焦点 ── - /** - * recompose 后重新绑定 focusOwner(旧实例可能已被替换)。 - */ private void refreshFocus() { if (this.focusOwner == null) return; for (UIComponent child : this.rootScope.getChildren()) { - KeyInputHandler found = this.findFocused(child); + UIComponent found = findFocused(child); if (found != null) { this.focusOwner = found; return; @@ -107,220 +76,148 @@ private void refreshFocus() { this.focusOwner = null; } - private @Nullable KeyInputHandler findFocused(UIComponent component) { - if (component instanceof TextInputComponent tf && tf.focused()) return tf; + private @Nullable UIComponent findFocused(UIComponent component) { + if (component instanceof Focusable f && f.focused()) return component; for (UIComponent child : component.children()) { - KeyInputHandler found = this.findFocused(child); + UIComponent found = findFocused(child); if (found != null) return found; } return null; } + // ── 鼠标点击 ── + @Override public boolean mouseClicked(MouseButtonEvent event, boolean isDoubleClick) { - if (event.button() == 0) { - var mc = Minecraft.getInstance(); - int mx = (int) mc.mouseHandler.getScaledXPos(mc.getWindow()); - int my = (int) mc.mouseHandler.getScaledYPos(mc.getWindow()); - - this.focusOwner = null; - for (UIComponent child : this.rootScope.getChildren()) { - this.clearFocusRecursive(child); - } + if (event.button() != 0) return super.mouseClicked(event, isDoubleClick); - boolean hit = false; - for (UIComponent child : this.rootScope.getChildren()) { - if (this.hitTestClick(child, mx, my)) { - hit = true; - break; - } - } + this.clearAllFocus(); + this.closeAllDropdowns(); - if (!hit) { - for (UIComponent child : this.rootScope.getChildren()) { - this.closeDropdownsRecursive(child); - } - } + if (dispatchMouseClicked(event, isDoubleClick)) return true; - if (hit) { - mc.getSoundManager().play(SimpleSoundInstance.forUI(SoundEvents.UI_BUTTON_CLICK, 1.0f)); - return true; - } - } return super.mouseClicked(event, isDoubleClick); } - @Override - public boolean mouseReleased(MouseButtonEvent event) { - for (UIComponent child : this.rootScope.getChildren()) { - this.stopDragRecursive(child); - } - return super.mouseReleased(event); + private void clearAllFocus() { + this.focusOwner = null; + for (UIComponent child : this.rootScope.getChildren()) clearFocusRecursive(child); } - // ── 键盘输入 ── + private void clearFocusRecursive(UIComponent component) { + if (component instanceof Focusable f) f.setFocused(false); + for (UIComponent child : component.children()) clearFocusRecursive(child); + } - @Override - public boolean mouseDragged(MouseButtonEvent event, double deltaX, double deltaY) { - var mc = Minecraft.getInstance(); - int mx = (int) mc.mouseHandler.getScaledXPos(mc.getWindow()); - int my = (int) mc.mouseHandler.getScaledYPos(mc.getWindow()); - for (UIComponent child : this.rootScope.getChildren()) { - if (this.hitTestDrag(child, mx, my)) return true; - } - return super.mouseDragged(event, deltaX, deltaY); + private void closeAllDropdowns() { + for (UIComponent child : this.rootScope.getChildren()) closeDropdownsRecursive(child); } - @Override - public boolean mouseScrolled(double mouseX, double mouseY, double scrollX, double scrollY) { - for (UIComponent child : this.rootScope.getChildren()) { - if (this.hitTestScroll(child, (float) mouseX, (float) mouseY, (float) scrollY)) { - return true; - } - } - return super.mouseScrolled(mouseX, mouseY, scrollX, scrollY); + private void closeDropdownsRecursive(UIComponent component) { + if (component instanceof DropdownComponent dd) dd.setOpen(false); + for (UIComponent child : component.children()) closeDropdownsRecursive(child); } - @Override - public boolean charTyped(CharacterEvent event) { - if (this.focusOwner != null && this.focusOwner.onCharTyped(event)) { - return true; + private boolean dispatchMouseClicked(MouseButtonEvent event, boolean isDouble) { + for (UIComponent child : this.rootScope.getChildren()) { + if (dispatchMouseClickedRecursive(child, event, isDouble)) return true; } - return super.charTyped(event); + return false; } - // ── 命中测试 ── - - /** - * 命中测试 + 点击触发。子组件优先。 - */ - private boolean hitTestClick(UIComponent component, float px, float py) { + private boolean dispatchMouseClickedRecursive(UIComponent component, MouseButtonEvent event, boolean isDouble) { var children = component.children(); for (int i = children.size() - 1; i >= 0; i--) { - if (this.hitTestClick(children.get(i), px, py)) return true; + if (dispatchMouseClickedRecursive(children.get(i), event, isDouble)) return true; } - switch (component) { - case ButtonComponent btn when btn.hitRect().contains(px, py) -> { - btn.click(); - return true; - } - case CheckboxComponent cb when cb.hitRect().contains(px, py) -> { - cb.toggle(); - return true; - } - case SliderComponent sl when sl.hitRect().contains(px, py) -> { - sl.setValueFromMouse(px); - return true; - } - case TextInputComponent tf when tf.hitRect().contains(px, py) -> { - tf.setFocused(true); - this.focusOwner = tf; - return true; - } - case DropdownComponent dd -> { - if (dd.isOnPopupScrollbar(px, py)) { - dd.startPopupScrollbarDrag(py); - return true; - } - if (dd.clickPopup(px, py)) return true; - return dd.clickTrigger(px, py); - } - case ScrollableComponent sc when sc.isOnScrollbar(px, py) -> { - sc.startScrollbarDrag(py); - return true; - } - default -> { - } + if (component.mouseClicked(event, isDouble)) { + if (component instanceof Focusable) this.focusOwner = component; + return true; } return false; } - /** - * 拖拽命中测试(Slider + Scrollable 滚动条)。 - */ - private boolean hitTestDrag(UIComponent component, float px, float py) { + // ── 鼠标拖拽 ── + + @Override + public boolean mouseDragged(MouseButtonEvent event, double deltaX, double deltaY) { + for (UIComponent child : this.rootScope.getChildren()) { + if (dispatchMouseDragged(child, event, deltaX, deltaY)) return true; + } + return super.mouseDragged(event, deltaX, deltaY); + } + + private boolean dispatchMouseDragged(UIComponent component, MouseButtonEvent event, double dx, double dy) { var children = component.children(); for (int i = children.size() - 1; i >= 0; i--) { - if (this.hitTestDrag(children.get(i), px, py)) return true; + if (dispatchMouseDragged(children.get(i), event, dx, dy)) return true; } - switch (component) { - case SliderComponent sl when sl.hitRect().contains(px, py) -> { - sl.setValueFromMouse(px); - return true; - } - case ScrollableComponent sc when sc.scrollbarDragging() -> { - sc.onScrollbarDrag(py); - return true; - } - case DropdownComponent dd when dd.scrollbarDragging() -> { - dd.onPopupScrollbarDrag(py); - return true; - } - default -> { - } + return component.mouseDragged(event, dx, dy); + } + + // ── 鼠标释放 ── + + @Override + public boolean mouseReleased(MouseButtonEvent event) { + for (UIComponent child : this.rootScope.getChildren()) { + dispatchMouseReleased(child, event); } - return false; + return super.mouseReleased(event); } - /** - * 递归关闭所有 Dropdown。 - */ - private void closeDropdownsRecursive(UIComponent component) { - if (component instanceof DropdownComponent dd) dd.setOpen(false); - for (UIComponent child : component.children()) this.closeDropdownsRecursive(child); + private void dispatchMouseReleased(UIComponent component, MouseButtonEvent event) { + var children = component.children(); + for (int i = children.size() - 1; i >= 0; i--) dispatchMouseReleased(children.get(i), event); + component.mouseReleased(event); } - /** - * 递归停止拖拽状态。 - */ - private void stopDragRecursive(UIComponent component) { - if (component instanceof ScrollableComponent sc) sc.stopScrollbarDrag(); - if (component instanceof DropdownComponent dd) dd.stopPopupScrollbarDrag(); - for (UIComponent child : component.children()) this.stopDragRecursive(child); + // ── 滚轮 ── + + @Override + public boolean mouseScrolled(double mouseX, double mouseY, double scrollX, double scrollY) { + for (UIComponent child : this.rootScope.getChildren()) { + if (dispatchMouseScrolled(child, mouseX, mouseY, scrollX, scrollY)) return true; + } + return super.mouseScrolled(mouseX, mouseY, scrollX, scrollY); } - /** - * 滚轮命中测试(ScrollableComponent + Dropdown 弹出层)。 - */ - private boolean hitTestScroll(UIComponent component, float px, float py, float amount) { + private boolean dispatchMouseScrolled(UIComponent component, double mx, double my, double sx, double sy) { var children = component.children(); for (int i = children.size() - 1; i >= 0; i--) { - if (this.hitTestScroll(children.get(i), px, py, amount)) return true; - } - if (component instanceof ScrollableComponent sc && sc.hitRect().contains(px, py)) { - return sc.onScroll(amount); + if (dispatchMouseScrolled(children.get(i), mx, my, sx, sy)) return true; } - if (component instanceof DropdownComponent dd && dd.open() - && dd.popupRect().contains(px, py)) { - return dd.onPopupScroll(amount); + return component.mouseScrolled(mx, my, sx, sy); + } + + // ── 键盘 ── + + @Override + public boolean keyPressed(KeyEvent event) { + if (event.key() == 256) { + this.onClose(); + return true; } - return false; + if (this.focusOwner != null && this.focusOwner.keyPressed(event)) return true; + return super.keyPressed(event); } - /** - * 清除组件树中所有 TextField 的焦点。 - */ - private void clearFocusRecursive(UIComponent component) { - if (component instanceof TextInputComponent tf) tf.setFocused(false); - for (UIComponent child : component.children()) this.clearFocusRecursive(child); + @Override + public boolean charTyped(CharacterEvent event) { + if (this.focusOwner != null && this.focusOwner.charTyped(event)) return true; + return super.charTyped(event); } - /** - * 遍历组件树,更新 ButtonComponent 的 hover 状态。 - */ + // ── hover ── + private void updateHover(float mouseX, float mouseY) { for (UIComponent child : this.rootScope.getChildren()) { - this.updateHoverRecursive(child, mouseX, mouseY); + updateHoverRecursive(child, mouseX, mouseY); } } private void updateHoverRecursive(UIComponent component, float mx, float my) { - if (component instanceof ButtonComponent btn) { - btn.setHovered(btn.hitRect().contains(mx, my)); - } - for (UIComponent child : component.children()) { - this.updateHoverRecursive(child, mx, my); - } + if (component instanceof ButtonComponent btn) btn.setHovered(btn.hitRect().contains(mx, my)); + for (UIComponent child : component.children()) updateHoverRecursive(child, mx, my); } // ── 内部类 ── @@ -328,4 +225,3 @@ private void updateHoverRecursive(UIComponent component, float mx, float my) { private static class RootScope extends UIScope { } } - diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Focusable.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Focusable.java new file mode 100644 index 00000000..45f837b4 --- /dev/null +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Focusable.java @@ -0,0 +1,14 @@ +package dev.anvilcraft.lib.v2.ui; + +/** + * 可获取键盘焦点的组件接口。 + * 由 {@link DeclarativeScreen} 的焦点系统驱动。 + */ +public interface Focusable { + + /** 是否已获取焦点。 */ + boolean focused(); + + /** 设置焦点状态。 */ + void setFocused(boolean focused); +} diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIComponent.java index d44ccb16..47be3b0f 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIComponent.java @@ -1,6 +1,9 @@ package dev.anvilcraft.lib.v2.ui; import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.input.CharacterEvent; +import net.minecraft.client.input.KeyEvent; +import net.minecraft.client.input.MouseButtonEvent; import java.util.List; @@ -13,37 +16,40 @@ *

  • {@link #layout(float, float, float, float)} — 接收父容器分配的最终位置
  • *
  • {@link #extractRenderState(GuiGraphicsExtractor)} — 提交渲染状态给 GPU
  • * + *

    + * 事件处理方法均有默认空实现,子类只覆写需要的。 + * {@link DeclarativeScreen} 负责递归遍历组件树并分发事件。 */ -@SuppressWarnings( - { - "unused", - "UnusedReturnValue" - } -) +@SuppressWarnings({"unused", "UnusedReturnValue"}) public interface UIComponent { - /** - * 应用于此组件的修饰符链。 - */ + Modifier modifier(); - /** - * 子组件列表,叶子组件返回空列表。 - */ List children(); - /** - * 根据父容器约束测量此组件。容器组件递归测量子组件。 - */ MeasuredSize measure(Constraints constraints); - /** - * 布局阶段后设置最终位置。容器组件在此方法内定位子组件。 - */ void layout(float x, float y, float width, float height); - /** - * 提交渲染状态到 Minecraft GUI 渲染管线。 - * 在 measure+layout 之后调用,每帧一次。 - */ void extractRenderState(GuiGraphicsExtractor extractor); + + // ── 事件处理(默认空实现,子类覆写)── + + /** 鼠标点击。返回 true 表示已消费。 */ + default boolean mouseClicked(MouseButtonEvent event, boolean isDoubleClick) { return false; } + + /** 鼠标拖拽。返回 true 表示已消费。 */ + default boolean mouseDragged(MouseButtonEvent event, double deltaX, double deltaY) { return false; } + + /** 鼠标释放。 */ + default boolean mouseReleased(MouseButtonEvent event) { return false; } + + /** 滚轮滚动。返回 true 表示已消费。 */ + default boolean mouseScrolled(double mouseX, double mouseY, double scrollX, double scrollY) { return false; } + + /** 按键按下。返回 true 表示已消费。 */ + default boolean keyPressed(KeyEvent event) { return false; } + + /** 字符输入。返回 true 表示已消费。 */ + default boolean charTyped(CharacterEvent event) { return false; } } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ButtonComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ButtonComponent.java index 3c41a659..aaa6bdd6 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ButtonComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ButtonComponent.java @@ -10,6 +10,9 @@ import lombok.experimental.Accessors; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.input.MouseButtonEvent; +import net.minecraft.client.resources.sounds.SimpleSoundInstance; +import net.minecraft.sounds.SoundEvents; import org.jspecify.annotations.Nullable; import java.util.Collections; @@ -84,4 +87,18 @@ public LayoutRect hitRect() { public void click() { if (this.onClick != null) this.onClick.run(); } + + @Override + public boolean mouseClicked(MouseButtonEvent event, boolean isDoubleClick) { + if (event.button() != 0) return false; + var mc = Minecraft.getInstance(); + int mx = (int) mc.mouseHandler.getScaledXPos(mc.getWindow()); + int my = (int) mc.mouseHandler.getScaledYPos(mc.getWindow()); + if (this.hitRect().contains(mx, my)) { + this.click(); + mc.getSoundManager().play(SimpleSoundInstance.forUI(SoundEvents.UI_BUTTON_CLICK, 1.0f)); + return true; + } + return false; + } } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/CheckboxComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/CheckboxComponent.java index 503124a5..10c2b6e1 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/CheckboxComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/CheckboxComponent.java @@ -8,7 +8,9 @@ import lombok.Getter; import lombok.Setter; import lombok.experimental.Accessors; +import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.input.MouseButtonEvent; import org.jspecify.annotations.Nullable; import java.util.Collections; @@ -105,6 +107,16 @@ public void toggle() { * 命中测试包围盒。 */ public LayoutRect hitRect() { - return LayoutRect.of(x, y, SIZE, SIZE); + return LayoutRect.of(this.x, this.y, SIZE, SIZE); + } + + @Override + public boolean mouseClicked(MouseButtonEvent event, boolean isDoubleClick) { + if (event.button() != 0) return false; + var mc = Minecraft.getInstance(); + int mx = (int) mc.mouseHandler.getScaledXPos(mc.getWindow()); + int my = (int) mc.mouseHandler.getScaledYPos(mc.getWindow()); + if (this.hitRect().contains(mx, my)) { this.toggle(); return true; } + return false; } } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/DropdownComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/DropdownComponent.java index 244dcd6e..fda80931 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/DropdownComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/DropdownComponent.java @@ -11,6 +11,7 @@ import lombok.experimental.Accessors; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.input.MouseButtonEvent; import net.minecraft.util.Mth; import org.jspecify.annotations.Nullable; @@ -320,4 +321,38 @@ private LayoutRect triggerRect() { public LayoutRect hitRect() { return this.triggerRect(); } + + @Override + public boolean mouseClicked(MouseButtonEvent event, boolean isDoubleClick) { + if (event.button() != 0) return false; + var mc = Minecraft.getInstance(); + int mx = (int) mc.mouseHandler.getScaledXPos(mc.getWindow()); + int my = (int) mc.mouseHandler.getScaledYPos(mc.getWindow()); + if (this.isOnPopupScrollbar(mx, my)) { this.startPopupScrollbarDrag(my); return true; } + if (this.clickPopup(mx, my)) return true; + return this.clickTrigger(mx, my); + } + + @Override + public boolean mouseScrolled(double mouseX, double mouseY, double scrollX, double scrollY) { + if (this.open() && this.popupRect().contains((float) mouseX, (float) mouseY)) { + return this.onPopupScroll((float) scrollY); + } + return false; + } + + @Override + public boolean mouseDragged(MouseButtonEvent event, double deltaX, double deltaY) { + if (!this.scrollbarDragging()) return false; + var mc = Minecraft.getInstance(); + int my = (int) mc.mouseHandler.getScaledYPos(mc.getWindow()); + this.onPopupScrollbarDrag(my); + return true; + } + + @Override + public boolean mouseReleased(MouseButtonEvent event) { + this.stopPopupScrollbarDrag(); + return false; + } } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ScrollableComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ScrollableComponent.java index d89951f5..ef1f612c 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ScrollableComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ScrollableComponent.java @@ -8,7 +8,9 @@ import lombok.Getter; import lombok.Setter; import lombok.experimental.Accessors; +import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.input.MouseButtonEvent; import net.minecraft.util.Mth; import java.util.ArrayList; @@ -185,4 +187,37 @@ public void setScrollY(float scrollY) { public LayoutRect hitRect() { return LayoutRect.of(this.x, this.y, this.width, this.height); } + + @Override + public boolean mouseClicked(MouseButtonEvent event, boolean isDoubleClick) { + if (event.button() != 0) return false; + var mc = Minecraft.getInstance(); + int mx = (int) mc.mouseHandler.getScaledXPos(mc.getWindow()); + int my = (int) mc.mouseHandler.getScaledYPos(mc.getWindow()); + if (this.isOnScrollbar(mx, my)) { this.startScrollbarDrag(my); return true; } + return false; + } + + @Override + public boolean mouseScrolled(double mouseX, double mouseY, double scrollX, double scrollY) { + if (this.hitRect().contains((float) mouseX, (float) mouseY)) { + return this.onScroll((float) scrollY); + } + return false; + } + + @Override + public boolean mouseDragged(MouseButtonEvent event, double deltaX, double deltaY) { + if (!this.scrollbarDragging()) return false; + var mc = Minecraft.getInstance(); + int my = (int) mc.mouseHandler.getScaledYPos(mc.getWindow()); + this.onScrollbarDrag(my); + return true; + } + + @Override + public boolean mouseReleased(MouseButtonEvent event) { + this.stopScrollbarDrag(); + return false; + } } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/SliderComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/SliderComponent.java index bbad881f..19b8c54d 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/SliderComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/SliderComponent.java @@ -8,7 +8,9 @@ import lombok.Getter; import lombok.Setter; import lombok.experimental.Accessors; +import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.input.MouseButtonEvent; import net.minecraft.util.Mth; import org.jspecify.annotations.Nullable; @@ -110,4 +112,23 @@ public void setValueFromMouse(float mouseX) { public LayoutRect hitRect() { return LayoutRect.of(this.x, this.y, this.width, this.height); } + + @Override + public boolean mouseClicked(MouseButtonEvent event, boolean isDoubleClick) { + if (event.button() != 0) return false; + var mc = Minecraft.getInstance(); + int mx = (int) mc.mouseHandler.getScaledXPos(mc.getWindow()); + int my = (int) mc.mouseHandler.getScaledYPos(mc.getWindow()); + if (this.hitRect().contains(mx, my)) { this.setValueFromMouse(mx); return true; } + return false; + } + + @Override + public boolean mouseDragged(MouseButtonEvent event, double deltaX, double deltaY) { + var mc = Minecraft.getInstance(); + int mx = (int) mc.mouseHandler.getScaledXPos(mc.getWindow()); + int my = (int) mc.mouseHandler.getScaledYPos(mc.getWindow()); + if (this.hitRect().contains(mx, my)) { this.setValueFromMouse(mx); return true; } + return false; + } } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextInputComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextInputComponent.java index c5876bad..ed756295 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextInputComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextInputComponent.java @@ -4,8 +4,8 @@ import dev.anvilcraft.lib.v2.ui.LayoutRect; import dev.anvilcraft.lib.v2.ui.MeasuredSize; import dev.anvilcraft.lib.v2.ui.Modifier; +import dev.anvilcraft.lib.v2.ui.Focusable; import dev.anvilcraft.lib.v2.ui.UIComponent; -import dev.anvilcraft.lib.v2.ui.input.KeyInputHandler; import lombok.Getter; import lombok.Setter; import lombok.experimental.Accessors; @@ -13,6 +13,7 @@ import net.minecraft.client.gui.GuiGraphicsExtractor; import net.minecraft.client.input.CharacterEvent; import net.minecraft.client.input.KeyEvent; +import net.minecraft.client.input.MouseButtonEvent; import net.minecraft.util.StringUtil; import org.jspecify.annotations.Nullable; @@ -27,7 +28,7 @@ "UnusedReturnValue" } ) -public class TextInputComponent implements UIComponent, KeyInputHandler { +public class TextInputComponent implements UIComponent, Focusable { private static final int BG_COLOR = 0xFF202020; private static final int TEXT_COLOR = 0xFFFFFFFF; private static final int PLACEHOLDER_COLOR = 0xFF555555; @@ -113,7 +114,7 @@ public void extractRenderState(GuiGraphicsExtractor extractor) { } @Override - public boolean onKeyPressed(KeyEvent event) { + public boolean keyPressed(KeyEvent event) { int key = event.key(); if (key == 259) { if (this.cursorPos > 0) { @@ -150,7 +151,7 @@ public boolean onKeyPressed(KeyEvent event) { } @Override - public boolean onCharTyped(CharacterEvent event) { + public boolean charTyped(CharacterEvent event) { if (!event.isAllowedChatCharacter()) return false; String text = StringUtil.filterText(event.codepointAsString()); if (text.isEmpty()) return false; @@ -171,4 +172,14 @@ private void fireChange() { public LayoutRect hitRect() { return LayoutRect.of(this.x, this.y, this.width, this.height); } + + @Override + public boolean mouseClicked(MouseButtonEvent event, boolean isDoubleClick) { + if (event.button() != 0) return false; + var mc = Minecraft.getInstance(); + int mx = (int) mc.mouseHandler.getScaledXPos(mc.getWindow()); + int my = (int) mc.mouseHandler.getScaledYPos(mc.getWindow()); + if (this.hitRect().contains(mx, my)) { this.setFocused(true); return true; } + return false; + } } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/input/KeyInputHandler.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/input/KeyInputHandler.java deleted file mode 100644 index ea79ba16..00000000 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/input/KeyInputHandler.java +++ /dev/null @@ -1,27 +0,0 @@ -package dev.anvilcraft.lib.v2.ui.input; - -import net.minecraft.client.input.CharacterEvent; -import net.minecraft.client.input.KeyEvent; - -/** - * 可接收键盘输入的组件接口。 - * 由 {@link dev.anvilcraft.lib.v2.ui.DeclarativeScreen} 的焦点系统驱动。 - */ -@SuppressWarnings( - { - "unused", - "UnusedReturnValue" - } -) -public interface KeyInputHandler { - /** - * 控制键按下时调用。返回 true 表示已处理。 - */ - boolean onKeyPressed(KeyEvent event); - - /** - * 字符输入时调用(支持所有语言、输入法、小键盘)。 - * 返回 true 表示已处理。 - */ - boolean onCharTyped(CharacterEvent event); -} diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/input/package-info.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/input/package-info.java deleted file mode 100644 index a59de3b1..00000000 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/input/package-info.java +++ /dev/null @@ -1,4 +0,0 @@ -@NullMarked -package dev.anvilcraft.lib.v2.ui.input; - -import org.jspecify.annotations.NullMarked; From 30704e1ee4ee3ba6356b6b1425ce4b3d39b26684 Mon Sep 17 00:00:00 2001 From: Gugle Date: Tue, 19 May 2026 16:59:20 +0800 Subject: [PATCH 51/67] feat(ui): enhance mouse event handling by introducing event priority for UI components --- .../lib/v2/ui/DeclarativeScreen.java | 34 ++++++++++++------- .../dev/anvilcraft/lib/v2/ui/UIComponent.java | 6 ++++ 2 files changed, 27 insertions(+), 13 deletions(-) diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java index 25ccf8f2..5772e68e 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java @@ -92,10 +92,10 @@ public boolean mouseClicked(MouseButtonEvent event, boolean isDoubleClick) { if (event.button() != 0) return super.mouseClicked(event, isDoubleClick); this.clearAllFocus(); - this.closeAllDropdowns(); - if (dispatchMouseClicked(event, isDoubleClick)) return true; + if (this.dispatchMouseClicked(event, isDoubleClick)) return true; + this.closeAllDropdowns(); return super.mouseClicked(event, isDoubleClick); } @@ -126,9 +126,11 @@ private boolean dispatchMouseClicked(MouseButtonEvent event, boolean isDouble) { } private boolean dispatchMouseClickedRecursive(UIComponent component, MouseButtonEvent event, boolean isDouble) { - var children = component.children(); - for (int i = children.size() - 1; i >= 0; i--) { - if (dispatchMouseClickedRecursive(children.get(i), event, isDouble)) return true; + var sorted = component.children().stream() + .sorted(java.util.Comparator.comparingInt(UIComponent::eventPriority).reversed()) + .toList(); + for (UIComponent child : sorted) { + if (dispatchMouseClickedRecursive(child, event, isDouble)) return true; } if (component.mouseClicked(event, isDouble)) { if (component instanceof Focusable) this.focusOwner = component; @@ -148,9 +150,11 @@ public boolean mouseDragged(MouseButtonEvent event, double deltaX, double deltaY } private boolean dispatchMouseDragged(UIComponent component, MouseButtonEvent event, double dx, double dy) { - var children = component.children(); - for (int i = children.size() - 1; i >= 0; i--) { - if (dispatchMouseDragged(children.get(i), event, dx, dy)) return true; + var sorted = component.children().stream() + .sorted(java.util.Comparator.comparingInt(UIComponent::eventPriority).reversed()) + .toList(); + for (UIComponent child : sorted) { + if (dispatchMouseDragged(child, event, dx, dy)) return true; } return component.mouseDragged(event, dx, dy); } @@ -166,8 +170,10 @@ public boolean mouseReleased(MouseButtonEvent event) { } private void dispatchMouseReleased(UIComponent component, MouseButtonEvent event) { - var children = component.children(); - for (int i = children.size() - 1; i >= 0; i--) dispatchMouseReleased(children.get(i), event); + var sorted = component.children().stream() + .sorted(java.util.Comparator.comparingInt(UIComponent::eventPriority).reversed()) + .toList(); + for (UIComponent child : sorted) dispatchMouseReleased(child, event); component.mouseReleased(event); } @@ -182,9 +188,11 @@ public boolean mouseScrolled(double mouseX, double mouseY, double scrollX, doubl } private boolean dispatchMouseScrolled(UIComponent component, double mx, double my, double sx, double sy) { - var children = component.children(); - for (int i = children.size() - 1; i >= 0; i--) { - if (dispatchMouseScrolled(children.get(i), mx, my, sx, sy)) return true; + var sorted = component.children().stream() + .sorted(java.util.Comparator.comparingInt(UIComponent::eventPriority).reversed()) + .toList(); + for (UIComponent child : sorted) { + if (dispatchMouseScrolled(child, mx, my, sx, sy)) return true; } return component.mouseScrolled(mx, my, sx, sy); } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIComponent.java index 47be3b0f..19ea8de7 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIComponent.java @@ -52,4 +52,10 @@ public interface UIComponent { /** 字符输入。返回 true 表示已消费。 */ default boolean charTyped(CharacterEvent event) { return false; } + + /** + * 事件优先级。值越大越先处理。 + * 默认 0。弹出层等需要优先拦截事件的组件可覆写为更高值。 + */ + default int eventPriority() { return 0; } } From 13f9e060adbfe2ae5f4947d2f116292afbab19f9 Mon Sep 17 00:00:00 2001 From: Gugle Date: Tue, 19 May 2026 17:08:12 +0800 Subject: [PATCH 52/67] feat(ui): implement rendering priority for UI components and sort children accordingly --- .../lib/v2/ui/DeclarativeScreen.java | 8 -------- .../dev/anvilcraft/lib/v2/ui/UIComponent.java | 18 ++++++++++++++++-- .../lib/v2/ui/component/BoxComponent.java | 4 ++-- .../lib/v2/ui/component/ColumnComponent.java | 4 ++-- .../lib/v2/ui/component/DropdownComponent.java | 7 ++++++- .../lib/v2/ui/component/GridComponent.java | 4 ++-- .../lib/v2/ui/component/RowComponent.java | 4 ++-- .../v2/ui/component/ScrollableComponent.java | 4 ++-- 8 files changed, 32 insertions(+), 21 deletions(-) diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java index 5772e68e..63411579 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java @@ -45,9 +45,6 @@ public void extractRenderState(GuiGraphicsExtractor extractor, int mouseX, int m this.composition.renderFrame(extractor, this.width, this.height); this.updateHover(mouseX, mouseY); this.refreshFocus(); - for (UIComponent child : this.rootScope.getChildren()) { - this.renderPopups(child, extractor); - } } } @@ -57,11 +54,6 @@ protected void init() { this.composition.setContent(this::content); } - private void renderPopups(UIComponent component, GuiGraphicsExtractor extractor) { - if (component instanceof DropdownComponent dd) dd.renderPopup(extractor); - for (UIComponent child : component.children()) renderPopups(child, extractor); - } - // ── 焦点 ── private void refreshFocus() { diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIComponent.java index 19ea8de7..75715177 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIComponent.java @@ -54,8 +54,22 @@ public interface UIComponent { default boolean charTyped(CharacterEvent event) { return false; } /** - * 事件优先级。值越大越先处理。 - * 默认 0。弹出层等需要优先拦截事件的组件可覆写为更高值。 + * 事件优先级。值越大越先处理。默认 0。 */ default int eventPriority() { return 0; } + + /** + * 渲染优先级。值越大越后提交渲染状态(上层)。 + * 默认 0。弹出层等需置于顶层的组件可覆写为更高值。 + */ + default int renderingPriority() { return 0; } + + /** + * 按渲染优先级排序后的子组件列表(低→高,先渲染的在前)。 + */ + default List sortedChildren() { + return this.children().stream() + .sorted(java.util.Comparator.comparingInt(UIComponent::renderingPriority)) + .toList(); + } } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/BoxComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/BoxComponent.java index 31446c1c..af08b3a9 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/BoxComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/BoxComponent.java @@ -62,7 +62,7 @@ public MeasuredSize measure(Constraints constraints) { float maxHeight = 0; List sizes = new ArrayList<>(this.children.size()); - for (UIComponent child : this.children) { + for (UIComponent child : this.sortedChildren()) { MeasuredSize size = child.measure(constraints); sizes.add(size); maxWidth = Math.max(maxWidth, size.width()); @@ -94,7 +94,7 @@ public void layout(float x, float y, float width, float height) { @Override public void extractRenderState(GuiGraphicsExtractor extractor) { - for (UIComponent child : this.children) { + for (UIComponent child : this.sortedChildren()) { child.extractRenderState(extractor); } } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ColumnComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ColumnComponent.java index d223b049..10b6698f 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ColumnComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ColumnComponent.java @@ -55,7 +55,7 @@ public MeasuredSize measure(Constraints constraints) { 0, Float.MAX_VALUE ); - for (UIComponent child : this.children) { + for (UIComponent child : this.sortedChildren()) { MeasuredSize size = child.measure(childConstraints); sizes.add(size); totalHeight += size.height(); @@ -89,7 +89,7 @@ public void layout(float x, float y, float width, float height) { } public void extractRenderState(GuiGraphicsExtractor extractor) { - for (UIComponent child : this.children) { + for (UIComponent child : this.sortedChildren()) { child.extractRenderState(extractor); } } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/DropdownComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/DropdownComponent.java index fda80931..b793e484 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/DropdownComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/DropdownComponent.java @@ -109,10 +109,15 @@ public void layout(float x, float y, float width, float height) { // ── 触发器渲染 ── + @Override + public int renderingPriority() { + return this.open ? 100 : 0; + } + @Override public void extractRenderState(GuiGraphicsExtractor extractor) { this.renderTrigger(extractor); - // 弹出层由 DeclarativeScreen 在最后统一渲染 + this.renderPopup(extractor); } private void renderTrigger(GuiGraphicsExtractor extractor) { diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/GridComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/GridComponent.java index 74102272..925414eb 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/GridComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/GridComponent.java @@ -52,7 +52,7 @@ public MeasuredSize measure(Constraints constraints) { List sizes = new ArrayList<>(this.children.size()); Constraints childC = new Constraints(0, Float.MAX_VALUE, 0, Float.MAX_VALUE); - for (UIComponent child : this.children) { + for (UIComponent child : this.sortedChildren()) { MeasuredSize s = child.measure(childC); sizes.add(s); maxW = Math.max(maxW, s.width()); @@ -86,6 +86,6 @@ public void layout(float x, float y, float width, float height) { } public void extractRenderState(GuiGraphicsExtractor extractor) { - for (UIComponent child : this.children) child.extractRenderState(extractor); + for (UIComponent child : this.sortedChildren()) child.extractRenderState(extractor); } } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/RowComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/RowComponent.java index 219032f5..3644a74a 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/RowComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/RowComponent.java @@ -55,7 +55,7 @@ public MeasuredSize measure(Constraints constraints) { constraints.minHeight(), constraints.maxHeight() ); - for (UIComponent child : this.children) { + for (UIComponent child : this.sortedChildren()) { MeasuredSize size = child.measure(childConstraints); sizes.add(size); totalWidth += size.width(); @@ -89,7 +89,7 @@ public void layout(float x, float y, float width, float height) { } public void extractRenderState(GuiGraphicsExtractor extractor) { - for (UIComponent child : this.children) { + for (UIComponent child : this.sortedChildren()) { child.extractRenderState(extractor); } } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ScrollableComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ScrollableComponent.java index ef1f612c..453ce4f7 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ScrollableComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ScrollableComponent.java @@ -68,7 +68,7 @@ public MeasuredSize measure(Constraints constraints) { List sizes = new ArrayList<>(this.children.size()); Constraints childC = new Constraints(0, constraints.maxWidth() - ScrollableComponent.SCROLLBAR_W - 1, 0, Float.MAX_VALUE); - for (UIComponent child : this.children) { + for (UIComponent child : this.sortedChildren()) { MeasuredSize s = child.measure(childC); // 修饰符会扩展尺寸(如 padding),需要计入内容高度 s = child.modifier().foldOut(s, (el, sz) -> el.modifyMeasuredSize(child, childC, sz)); @@ -104,7 +104,7 @@ public void extractRenderState(GuiGraphicsExtractor extractor) { // 裁剪到容器范围 extractor.enableScissor(ix, iy, ix + iw, iy + ih); - for (UIComponent child : this.children) { + for (UIComponent child : this.sortedChildren()) { child.extractRenderState(extractor); } From a1f328f302c7a02f1b58f21ecd07eabb246a1273 Mon Sep 17 00:00:00 2001 From: Gugle Date: Tue, 19 May 2026 17:13:47 +0800 Subject: [PATCH 53/67] feat(ui): add toggle command for SDF layer rendering and update component measurement logic --- .../v2/test/client/AnvilLibTestClient.java | 9 +- .../v2/test/client/gui/SdfGraphicsLayer.java | 88 ++++++++++--------- .../lib/v2/ui/component/BoxComponent.java | 2 +- .../lib/v2/ui/component/ColumnComponent.java | 2 +- .../lib/v2/ui/component/GridComponent.java | 2 +- .../lib/v2/ui/component/RowComponent.java | 2 +- .../v2/ui/component/ScrollableComponent.java | 2 +- 7 files changed, 58 insertions(+), 49 deletions(-) diff --git a/module.test/src/main/java/dev/anvilcraft/lib/v2/test/client/AnvilLibTestClient.java b/module.test/src/main/java/dev/anvilcraft/lib/v2/test/client/AnvilLibTestClient.java index b78b1ace..eb5ea4b0 100644 --- a/module.test/src/main/java/dev/anvilcraft/lib/v2/test/client/AnvilLibTestClient.java +++ b/module.test/src/main/java/dev/anvilcraft/lib/v2/test/client/AnvilLibTestClient.java @@ -8,7 +8,6 @@ import dev.anvilcraft.lib.v2.test.client.screen.DeclarativeTestScreen; import dev.anvilcraft.lib.v2.test.client.screen.GuiTestScreen; import net.minecraft.client.Minecraft; -import net.minecraft.commands.Commands; import net.neoforged.api.distmarker.Dist; import net.neoforged.bus.api.SubscribeEvent; import net.neoforged.fml.common.EventBusSubscriber; @@ -22,6 +21,8 @@ @EventBusSubscriber(modid = AnvilLibTest.MOD_ID) @Mod(value = AnvilLibTest.MOD_ID, dist = Dist.CLIENT) public class AnvilLibTestClient { + public static boolean renderSdfLayer = false; + public AnvilLibTestClient() { } @@ -54,6 +55,12 @@ public static void on(RegisterClientCommandsEvent event) { Minecraft.getInstance().setScreen(new DeclarativeTestScreen()); return 1; }) + ).then( + literal("sdf") + .executes(_ -> { + AnvilLibTestClient.renderSdfLayer = !AnvilLibTestClient.renderSdfLayer; + return 1; + }) ) ); } diff --git a/module.test/src/main/java/dev/anvilcraft/lib/v2/test/client/gui/SdfGraphicsLayer.java b/module.test/src/main/java/dev/anvilcraft/lib/v2/test/client/gui/SdfGraphicsLayer.java index 16298a65..a50af134 100644 --- a/module.test/src/main/java/dev/anvilcraft/lib/v2/test/client/gui/SdfGraphicsLayer.java +++ b/module.test/src/main/java/dev/anvilcraft/lib/v2/test/client/gui/SdfGraphicsLayer.java @@ -2,6 +2,7 @@ import dev.anvilcraft.lib.v2.rendering.sdf.SdfGraphics; import dev.anvilcraft.lib.v2.test.AnvilLibTest; +import dev.anvilcraft.lib.v2.test.client.AnvilLibTestClient; import net.minecraft.client.DeltaTracker; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiGraphicsExtractor; @@ -17,15 +18,16 @@ public class SdfGraphicsLayer implements GuiLayer { @Override public void render( - @NonNull GuiGraphicsExtractor graphics, - @NonNull DeltaTracker tracker + @NonNull GuiGraphicsExtractor graphics, + @NonNull DeltaTracker tracker ) { + if (!AnvilLibTestClient.renderSdfLayer) return; this.timer += tracker.getGameTimeDeltaTicks(); var minecraft = Minecraft.getInstance(); if (minecraft.screen != null) return; - int xMouse = (int)minecraft.mouseHandler.getScaledXPos(minecraft.getWindow()); - int yMouse = (int)minecraft.mouseHandler.getScaledYPos(minecraft.getWindow()); + int xMouse = (int) minecraft.mouseHandler.getScaledXPos(minecraft.getWindow()); + int yMouse = (int) minecraft.mouseHandler.getScaledYPos(minecraft.getWindow()); /*SdfGraphics.getInstance() .center(true) @@ -53,13 +55,13 @@ public void render( .fill() .draw(graphics) .reset();*/ - var sdf = SdfGraphics.getInstance() - .reset() - .rotate(this.timer) - .center(true) + var sdf = SdfGraphics.getInstance() + .reset() + .rotate(this.timer) + .center(true) - .stroke(0) - .fill(); + .stroke(0) + .fill(); this.draw(graphics, sdf, 0, xMouse, yMouse); @@ -76,67 +78,67 @@ public void render( } private void draw( - GuiGraphicsExtractor graphics, - SdfGraphics sdf, - int shift, - int xMouse, int yMouse + GuiGraphicsExtractor graphics, + SdfGraphics sdf, + int shift, + int xMouse, int yMouse ) { this.draw( - graphics, - sdf.box(32, 40 + shift, 40, 20), - xMouse, yMouse + graphics, + sdf.box(32, 40 + shift, 40, 20), + xMouse, yMouse ); this.draw( - graphics, - sdf.box(30, 65 + shift, 40, 20) - .round(5), - xMouse, yMouse + graphics, + sdf.box(30, 65 + shift, 40, 20) + .round(5), + xMouse, yMouse ); sdf.round(0); this.draw( - graphics, - sdf.circle(80, 50 + shift, 20), - xMouse, yMouse + graphics, + sdf.circle(80, 50 + shift, 20), + xMouse, yMouse ); this.draw( - graphics, - sdf.arc(130, 50 + shift, 45, 20, 5), - xMouse, yMouse + graphics, + sdf.arc(130, 50 + shift, 45, 20, 5), + xMouse, yMouse ); this.draw( - graphics, - sdf.sector(180, 50 + shift, 45, 20, 5), - xMouse, yMouse + graphics, + sdf.sector(180, 50 + shift, 45, 20, 5), + xMouse, yMouse ); this.draw( - graphics, - sdf.pie(230, 50 + shift, 45, 20), - xMouse, yMouse + graphics, + sdf.pie(230, 50 + shift, 45, 20), + xMouse, yMouse ); this.draw( - graphics, - sdf.capsule(280, 50 + shift, 8, 10, 18), - xMouse, yMouse + graphics, + sdf.capsule(280, 50 + shift, 8, 10, 18), + xMouse, yMouse ); this.draw( - graphics, - sdf.egg(330, 50 + shift, 2, 10, 12), - xMouse, yMouse + graphics, + sdf.egg(330, 50 + shift, 2, 10, 12), + xMouse, yMouse ); } - + private void draw( - GuiGraphicsExtractor graphics, - SdfGraphics sdf, - int mouseX, int mouseY + GuiGraphicsExtractor graphics, + SdfGraphics sdf, + int mouseX, int mouseY ) { if (sdf.collide(mouseX, mouseY, 0.5f)) { diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/BoxComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/BoxComponent.java index af08b3a9..54a27ff3 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/BoxComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/BoxComponent.java @@ -62,7 +62,7 @@ public MeasuredSize measure(Constraints constraints) { float maxHeight = 0; List sizes = new ArrayList<>(this.children.size()); - for (UIComponent child : this.sortedChildren()) { + for (UIComponent child : this.children) { MeasuredSize size = child.measure(constraints); sizes.add(size); maxWidth = Math.max(maxWidth, size.width()); diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ColumnComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ColumnComponent.java index 10b6698f..6265e651 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ColumnComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ColumnComponent.java @@ -55,7 +55,7 @@ public MeasuredSize measure(Constraints constraints) { 0, Float.MAX_VALUE ); - for (UIComponent child : this.sortedChildren()) { + for (UIComponent child : this.children) { MeasuredSize size = child.measure(childConstraints); sizes.add(size); totalHeight += size.height(); diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/GridComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/GridComponent.java index 925414eb..db6734bc 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/GridComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/GridComponent.java @@ -52,7 +52,7 @@ public MeasuredSize measure(Constraints constraints) { List sizes = new ArrayList<>(this.children.size()); Constraints childC = new Constraints(0, Float.MAX_VALUE, 0, Float.MAX_VALUE); - for (UIComponent child : this.sortedChildren()) { + for (UIComponent child : this.children) { MeasuredSize s = child.measure(childC); sizes.add(s); maxW = Math.max(maxW, s.width()); diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/RowComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/RowComponent.java index 3644a74a..d577f2c6 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/RowComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/RowComponent.java @@ -55,7 +55,7 @@ public MeasuredSize measure(Constraints constraints) { constraints.minHeight(), constraints.maxHeight() ); - for (UIComponent child : this.sortedChildren()) { + for (UIComponent child : this.children) { MeasuredSize size = child.measure(childConstraints); sizes.add(size); totalWidth += size.width(); diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ScrollableComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ScrollableComponent.java index 453ce4f7..0e36cf91 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ScrollableComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ScrollableComponent.java @@ -68,7 +68,7 @@ public MeasuredSize measure(Constraints constraints) { List sizes = new ArrayList<>(this.children.size()); Constraints childC = new Constraints(0, constraints.maxWidth() - ScrollableComponent.SCROLLBAR_W - 1, 0, Float.MAX_VALUE); - for (UIComponent child : this.sortedChildren()) { + for (UIComponent child : this.children) { MeasuredSize s = child.measure(childC); // 修饰符会扩展尺寸(如 padding),需要计入内容高度 s = child.modifier().foldOut(s, (el, sz) -> el.modifyMeasuredSize(child, childC, sz)); From 5d341337a99451dfebf70bccb7987acd906c9837 Mon Sep 17 00:00:00 2001 From: Gugle Date: Tue, 19 May 2026 18:46:48 +0800 Subject: [PATCH 54/67] feat(ui): add sound feedback for UI component interactions and enhance hover state handling --- .../dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java | 3 +++ .../lib/v2/ui/component/CheckboxComponent.java | 4 +++- .../lib/v2/ui/component/DropdownComponent.java | 13 +++++++++---- .../lib/v2/ui/component/SliderComponent.java | 9 +++++++-- .../lib/v2/ui/component/TextInputComponent.java | 4 +++- 5 files changed, 25 insertions(+), 8 deletions(-) diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java index 63411579..412f0434 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java @@ -2,6 +2,7 @@ import dev.anvilcraft.lib.v2.ui.component.ButtonComponent; import dev.anvilcraft.lib.v2.ui.component.DropdownComponent; +import dev.anvilcraft.lib.v2.ui.component.SliderComponent; import net.minecraft.client.gui.GuiGraphicsExtractor; import net.minecraft.client.gui.screens.Screen; import net.minecraft.client.input.CharacterEvent; @@ -217,6 +218,8 @@ private void updateHover(float mouseX, float mouseY) { private void updateHoverRecursive(UIComponent component, float mx, float my) { if (component instanceof ButtonComponent btn) btn.setHovered(btn.hitRect().contains(mx, my)); + if (component instanceof SliderComponent sl) sl.setHovered(sl.hitRect().contains(mx, my)); + if (component instanceof DropdownComponent dd) dd.setHovered(dd.hitRect().contains(mx, my)); for (UIComponent child : component.children()) updateHoverRecursive(child, mx, my); } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/CheckboxComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/CheckboxComponent.java index 10c2b6e1..20fef719 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/CheckboxComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/CheckboxComponent.java @@ -11,6 +11,8 @@ import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiGraphicsExtractor; import net.minecraft.client.input.MouseButtonEvent; +import net.minecraft.client.resources.sounds.SimpleSoundInstance; +import net.minecraft.sounds.SoundEvents; import org.jspecify.annotations.Nullable; import java.util.Collections; @@ -116,7 +118,7 @@ public boolean mouseClicked(MouseButtonEvent event, boolean isDoubleClick) { var mc = Minecraft.getInstance(); int mx = (int) mc.mouseHandler.getScaledXPos(mc.getWindow()); int my = (int) mc.mouseHandler.getScaledYPos(mc.getWindow()); - if (this.hitRect().contains(mx, my)) { this.toggle(); return true; } + if (this.hitRect().contains(mx, my)) { this.toggle(); mc.getSoundManager().play(SimpleSoundInstance.forUI(SoundEvents.UI_BUTTON_CLICK, 1.0f)); return true; } return false; } } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/DropdownComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/DropdownComponent.java index b793e484..ac6720ea 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/DropdownComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/DropdownComponent.java @@ -12,6 +12,8 @@ import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiGraphicsExtractor; import net.minecraft.client.input.MouseButtonEvent; +import net.minecraft.client.resources.sounds.SimpleSoundInstance; +import net.minecraft.sounds.SoundEvents; import net.minecraft.util.Mth; import org.jspecify.annotations.Nullable; @@ -50,6 +52,7 @@ public class DropdownComponent implements UIComponent { private int selectedIndex; @Getter private boolean open; + private boolean hovered; @Getter private float popupScrollY; @Setter @@ -77,8 +80,9 @@ public String selectedOption() { public void setOpen(boolean open) { this.open = open; - if (!open) this.popupScrollY = 0; + if (!this.open) this.popupScrollY = 0; } + public void setHovered(boolean hovered) { this.hovered = hovered; } public void setPopupScrollY(float y) { this.popupScrollY = y; @@ -123,7 +127,7 @@ public void extractRenderState(GuiGraphicsExtractor extractor) { private void renderTrigger(GuiGraphicsExtractor extractor) { var font = Minecraft.getInstance().font; int ix = (int) this.x, iy = (int) this.y, iw = (int) this.width, ih = (int) this.height; - int bg = this.open ? DropdownComponent.HOVER_COLOR : DropdownComponent.BG_COLOR; + int bg = (this.open || this.hovered) ? DropdownComponent.HOVER_COLOR : DropdownComponent.BG_COLOR; extractor.fill(ix, iy, ix + iw, iy + ih, bg); String label = this.options.length > 0 ? this.options[this.selectedIndex] : ""; @@ -334,8 +338,9 @@ public boolean mouseClicked(MouseButtonEvent event, boolean isDoubleClick) { int mx = (int) mc.mouseHandler.getScaledXPos(mc.getWindow()); int my = (int) mc.mouseHandler.getScaledYPos(mc.getWindow()); if (this.isOnPopupScrollbar(mx, my)) { this.startPopupScrollbarDrag(my); return true; } - if (this.clickPopup(mx, my)) return true; - return this.clickTrigger(mx, my); + if (this.clickPopup(mx, my)) { mc.getSoundManager().play(SimpleSoundInstance.forUI(SoundEvents.UI_BUTTON_CLICK, 1.0f)); return true; } + if (this.clickTrigger(mx, my)) { mc.getSoundManager().play(SimpleSoundInstance.forUI(SoundEvents.UI_BUTTON_CLICK, 1.0f)); return true; } + return false; } @Override diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/SliderComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/SliderComponent.java index 19b8c54d..2e76f636 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/SliderComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/SliderComponent.java @@ -11,6 +11,8 @@ import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiGraphicsExtractor; import net.minecraft.client.input.MouseButtonEvent; +import net.minecraft.client.resources.sounds.SimpleSoundInstance; +import net.minecraft.sounds.SoundEvents; import net.minecraft.util.Mth; import org.jspecify.annotations.Nullable; @@ -42,6 +44,7 @@ public class SliderComponent implements UIComponent { private Modifier modifier; @Getter private float value; + private boolean hovered; @Setter private @Nullable Consumer onChange; @@ -55,6 +58,7 @@ public SliderComponent(Modifier modifier, float value, float min, float max, flo this.max = max; this.trackWidth = trackWidth; } + public void setHovered(boolean hovered) { this.hovered = hovered; } @Override @@ -91,7 +95,8 @@ public void extractRenderState(GuiGraphicsExtractor extractor) { float ratio = (this.value - this.min) / (this.max - this.min); float thumbX = this.x + ratio * (this.width - SliderComponent.THUMB_W); int tix = (int) thumbX, tiy = (int) this.y; - extractor.fill(tix, tiy, tix + (int) SliderComponent.THUMB_W, tiy + (int) THUMB_H, THUMB_COLOR); + int thumbColor = this.hovered ? 0xFFCCCCCC : SliderComponent.THUMB_COLOR; + extractor.fill(tix, tiy, tix + (int) SliderComponent.THUMB_W, tiy + (int) SliderComponent.THUMB_H, thumbColor); } /** @@ -119,7 +124,7 @@ public boolean mouseClicked(MouseButtonEvent event, boolean isDoubleClick) { var mc = Minecraft.getInstance(); int mx = (int) mc.mouseHandler.getScaledXPos(mc.getWindow()); int my = (int) mc.mouseHandler.getScaledYPos(mc.getWindow()); - if (this.hitRect().contains(mx, my)) { this.setValueFromMouse(mx); return true; } + if (this.hitRect().contains(mx, my)) { this.setValueFromMouse(mx); mc.getSoundManager().play(SimpleSoundInstance.forUI(SoundEvents.UI_BUTTON_CLICK, 1.0f)); return true; } return false; } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextInputComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextInputComponent.java index ed756295..133c75ad 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextInputComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextInputComponent.java @@ -14,6 +14,8 @@ import net.minecraft.client.input.CharacterEvent; import net.minecraft.client.input.KeyEvent; import net.minecraft.client.input.MouseButtonEvent; +import net.minecraft.client.resources.sounds.SimpleSoundInstance; +import net.minecraft.sounds.SoundEvents; import net.minecraft.util.StringUtil; import org.jspecify.annotations.Nullable; @@ -179,7 +181,7 @@ public boolean mouseClicked(MouseButtonEvent event, boolean isDoubleClick) { var mc = Minecraft.getInstance(); int mx = (int) mc.mouseHandler.getScaledXPos(mc.getWindow()); int my = (int) mc.mouseHandler.getScaledYPos(mc.getWindow()); - if (this.hitRect().contains(mx, my)) { this.setFocused(true); return true; } + if (this.hitRect().contains(mx, my)) { this.setFocused(true); mc.getSoundManager().play(SimpleSoundInstance.forUI(SoundEvents.UI_BUTTON_CLICK, 1.0f)); return true; } return false; } } From 9303b1c08a4c4440d7ba8d58375279986f89136c Mon Sep 17 00:00:00 2001 From: Gugle Date: Tue, 19 May 2026 18:56:44 +0800 Subject: [PATCH 55/67] feat(ui): add hover highlight for dropdown items and improve background color handling --- .../lib/v2/ui/component/DropdownComponent.java | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/DropdownComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/DropdownComponent.java index ac6720ea..70c3f976 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/DropdownComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/DropdownComponent.java @@ -204,14 +204,25 @@ public void renderPopup(GuiGraphicsExtractor extractor) { extractor.enableScissor(px, py, px + pw, py + (int) ph); extractor.fill(px, py, px + pw, py + (int) ph, DropdownComponent.POPUP_BG); + // 计算鼠标悬停项 + var mc = Minecraft.getInstance(); + int mx = (int) mc.mouseHandler.getScaledXPos(mc.getWindow()); + int my = (int) mc.mouseHandler.getScaledYPos(mc.getWindow()); + int hoveredIdx = -1; + if (mx >= px && mx < px + pw && my >= py && my < py + (int) ph) { + hoveredIdx = (int) ((my - py) / itemH); + } + float startY = this.y + this.height + this.popupScrollY; for (int i = 0; i < this.options.length; i++) { float iy = startY + i * itemH; float bottom = iy + itemH; - // 跳过完全不可见的项 if (bottom <= this.y + this.height || iy >= this.y + this.height + ph) continue; - int bg = (i == this.selectedIndex) ? DropdownComponent.POPUP_HOVER : DropdownComponent.POPUP_BG; + int bg; + if (i == this.selectedIndex) bg = DropdownComponent.POPUP_HOVER; + else if (i == hoveredIdx) bg = DropdownComponent.SCROLLBAR_COLOR; // 悬停高亮 + else bg = DropdownComponent.POPUP_BG; int fillTop = Math.max((int) iy, py); int fillBot = Math.min((int) bottom, py + (int) ph); extractor.fill(px, fillTop, px + pw, fillBot, bg); From 6d677ec7421b122fd93d07dab5f25f7479a1301a Mon Sep 17 00:00:00 2001 From: Gugle Date: Tue, 19 May 2026 19:00:16 +0800 Subject: [PATCH 56/67] feat(ui): implement hover state update method for UI components --- .../java/dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java | 6 +----- .../src/main/java/dev/anvilcraft/lib/v2/ui/UIComponent.java | 3 +++ .../dev/anvilcraft/lib/v2/ui/component/ButtonComponent.java | 3 +++ .../anvilcraft/lib/v2/ui/component/DropdownComponent.java | 3 +++ .../dev/anvilcraft/lib/v2/ui/component/SliderComponent.java | 3 +++ 5 files changed, 13 insertions(+), 5 deletions(-) diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java index 412f0434..33cd8b2e 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java @@ -1,8 +1,6 @@ package dev.anvilcraft.lib.v2.ui; -import dev.anvilcraft.lib.v2.ui.component.ButtonComponent; import dev.anvilcraft.lib.v2.ui.component.DropdownComponent; -import dev.anvilcraft.lib.v2.ui.component.SliderComponent; import net.minecraft.client.gui.GuiGraphicsExtractor; import net.minecraft.client.gui.screens.Screen; import net.minecraft.client.input.CharacterEvent; @@ -217,9 +215,7 @@ private void updateHover(float mouseX, float mouseY) { } private void updateHoverRecursive(UIComponent component, float mx, float my) { - if (component instanceof ButtonComponent btn) btn.setHovered(btn.hitRect().contains(mx, my)); - if (component instanceof SliderComponent sl) sl.setHovered(sl.hitRect().contains(mx, my)); - if (component instanceof DropdownComponent dd) dd.setHovered(dd.hitRect().contains(mx, my)); + component.updateHover(mx, my); for (UIComponent child : component.children()) updateHoverRecursive(child, mx, my); } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIComponent.java index 75715177..521ec94a 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIComponent.java @@ -53,6 +53,9 @@ public interface UIComponent { /** 字符输入。返回 true 表示已消费。 */ default boolean charTyped(CharacterEvent event) { return false; } + /** 每帧更新 hover 状态。默认空实现。 */ + default void updateHover(float mouseX, float mouseY) {} + /** * 事件优先级。值越大越先处理。默认 0。 */ diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ButtonComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ButtonComponent.java index aaa6bdd6..40254bad 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ButtonComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ButtonComponent.java @@ -46,6 +46,9 @@ public ButtonComponent(Modifier modifier, String label) { public ButtonComponent onClick(@Nullable Runnable onClick) { this.onClick = onClick; return this; } public void setHovered(boolean hovered) { this.hovered = hovered; } + @Override + public void updateHover(float mx, float my) { this.hovered = this.hitRect().contains(mx, my); } + @Override public List children() { return Collections.emptyList(); } @Override diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/DropdownComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/DropdownComponent.java index 70c3f976..4677b4be 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/DropdownComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/DropdownComponent.java @@ -84,6 +84,9 @@ public void setOpen(boolean open) { } public void setHovered(boolean hovered) { this.hovered = hovered; } + @Override + public void updateHover(float mx, float my) { this.hovered = this.hitRect().contains(mx, my); } + public void setPopupScrollY(float y) { this.popupScrollY = y; } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/SliderComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/SliderComponent.java index 2e76f636..886aeb8e 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/SliderComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/SliderComponent.java @@ -60,6 +60,9 @@ public SliderComponent(Modifier modifier, float value, float min, float max, flo } public void setHovered(boolean hovered) { this.hovered = hovered; } + @Override + public void updateHover(float mx, float my) { this.hovered = this.hitRect().contains(mx, my); } + @Override public List children() { From b7d1406aa14d44a6306e58c958a1341e48b7510f Mon Sep 17 00:00:00 2001 From: Gugle Date: Tue, 19 May 2026 19:05:46 +0800 Subject: [PATCH 57/67] feat(ui): refactor hover state handling and improve code formatting in UI components --- .../dev/anvilcraft/lib/v2/ui/Composition.java | 33 ++--- .../lib/v2/ui/DeclarativeScreen.java | 131 +++++++++--------- .../dev/anvilcraft/lib/v2/ui/Focusable.java | 9 +- .../dev/anvilcraft/lib/v2/ui/UIComponent.java | 84 ++++++++--- .../lib/v2/ui/component/ButtonComponent.java | 56 +++++--- .../v2/ui/component/CheckboxComponent.java | 24 ++-- .../lib/v2/ui/component/ColumnComponent.java | 11 +- .../v2/ui/component/DropdownComponent.java | 118 +++++++++------- .../lib/v2/ui/component/GridComponent.java | 11 +- .../lib/v2/ui/component/RowComponent.java | 11 +- .../v2/ui/component/ScrollableComponent.java | 70 +++++----- .../lib/v2/ui/component/SliderComponent.java | 57 +++++--- .../v2/ui/component/TextInputComponent.java | 26 ++-- .../lib/v2/ui/modifier/SizeElement.java | 4 +- .../resources/META-INF/neoforge.mods.toml | 4 +- 15 files changed, 378 insertions(+), 271 deletions(-) diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Composition.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Composition.java index a24b3cd6..002fd966 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Composition.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Composition.java @@ -40,19 +40,17 @@ public class Composition { // ── slot table ── private final List animatables = new ArrayList<>(); + private final @Nullable UIScope rootScope; /** * 当前正在 emit 的 slot(在 {@link #emit} 期间设置)。 */ - @Nullable - Slot currentSlot; + @Nullable Slot currentSlot; private int currentIndex; private int currentRememberKey; private boolean dirty = true; - // ── 状态 ── @Setter private @Nullable Consumer content; - private final @Nullable UIScope rootScope; public Composition(@Nullable UIScope rootScope) { this.rootScope = rootScope; @@ -144,18 +142,15 @@ public void emit(UIComponent component) { * 将旧组件的运行时状态复制到新组件。 */ private void copyRuntimeState(UIComponent old, UIComponent replacement) { - if (old instanceof ScrollableComponent oldSc - && replacement instanceof ScrollableComponent newSc) { + if (old instanceof ScrollableComponent oldSc && replacement instanceof ScrollableComponent newSc) { newSc.setScrollY(oldSc.scrollY()); } - if (old instanceof TextInputComponent oldTi - && replacement instanceof TextInputComponent newTi) { + if (old instanceof TextInputComponent oldTi && replacement instanceof TextInputComponent newTi) { newTi.setValue(oldTi.value()); newTi.setCursorPos(oldTi.cursorPos()); newTi.setFocused(oldTi.focused()); } - if (old instanceof DropdownComponent oldDd - && replacement instanceof DropdownComponent newDd) { + if (old instanceof DropdownComponent oldDd && replacement instanceof DropdownComponent newDd) { newDd.setOpen(oldDd.open()); newDd.setPopupScrollY(oldDd.popupScrollY()); } @@ -225,31 +220,21 @@ private boolean hasDirtySlots() { private void renderTree(UIComponent component, GuiGraphicsExtractor extractor, Constraints constraints) { // 1. Apply modifier to constraints - Constraints modConstraints = component.modifier().foldIn( - constraints, - (c, el) -> el.modifyConstraints(c) - ); + Constraints modConstraints = component.modifier().foldIn(constraints, (c, el) -> el.modifyConstraints(c)); // 2. Measure MeasuredSize size = component.measure(modConstraints); - size = component.modifier().foldOut( - size, - (el, s) -> el.modifyMeasuredSize(component, modConstraints, s) - ); + size = component.modifier().foldOut(size, (el, s) -> el.modifyMeasuredSize(component, modConstraints, s)); // 3. Layout LayoutRect rect = LayoutRect.of(0, 0, size.width(), size.height()); - rect = component.modifier().foldOut( - rect, - ModifierElement::modifyLayout - ); + rect = component.modifier().foldOut(rect, ModifierElement::modifyLayout); component.layout(rect.x(), rect.y(), rect.width(), rect.height()); // 4. Emit modifier render states (background, border, etc.) final LayoutRect finalRect = rect; component.modifier().foldOut( - extractor, - (el, e) -> { + extractor, (el, e) -> { el.emitRenderState(e, finalRect); return e; } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java index 33cd8b2e..576a500a 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java @@ -22,7 +22,6 @@ } ) public abstract class DeclarativeScreen extends Screen { - private final UIScope rootScope = new RootScope(); @Nullable private Composition composition; @@ -47,14 +46,24 @@ public void extractRenderState(GuiGraphicsExtractor extractor, int mouseX, int m } } + @Override + public boolean keyPressed(KeyEvent event) { + if (event.key() == 256) { + this.onClose(); + return true; + } + if (this.focusOwner != null && this.focusOwner.keyPressed(event)) return true; + return super.keyPressed(event); + } + + // ── 焦点 ── + @Override protected void init() { this.composition = new Composition(this.rootScope); this.composition.setContent(this::content); } - // ── 焦点 ── - private void refreshFocus() { if (this.focusOwner == null) return; for (UIComponent child : this.rootScope.getChildren()) { @@ -67,6 +76,8 @@ private void refreshFocus() { this.focusOwner = null; } + // ── 鼠标点击 ── + private @Nullable UIComponent findFocused(UIComponent component) { if (component instanceof Focusable f && f.focused()) return component; for (UIComponent child : component.children()) { @@ -76,8 +87,6 @@ private void refreshFocus() { return null; } - // ── 鼠标点击 ── - @Override public boolean mouseClicked(MouseButtonEvent event, boolean isDoubleClick) { if (event.button() != 0) return super.mouseClicked(event, isDoubleClick); @@ -90,11 +99,43 @@ public boolean mouseClicked(MouseButtonEvent event, boolean isDoubleClick) { return super.mouseClicked(event, isDoubleClick); } + @Override + public boolean mouseReleased(MouseButtonEvent event) { + for (UIComponent child : this.rootScope.getChildren()) { + dispatchMouseReleased(child, event); + } + return super.mouseReleased(event); + } + + @Override + public boolean mouseDragged(MouseButtonEvent event, double deltaX, double deltaY) { + for (UIComponent child : this.rootScope.getChildren()) { + if (dispatchMouseDragged(child, event, deltaX, deltaY)) return true; + } + return super.mouseDragged(event, deltaX, deltaY); + } + + @Override + public boolean mouseScrolled(double mouseX, double mouseY, double scrollX, double scrollY) { + for (UIComponent child : this.rootScope.getChildren()) { + if (dispatchMouseScrolled(child, mouseX, mouseY, scrollX, scrollY)) return true; + } + return super.mouseScrolled(mouseX, mouseY, scrollX, scrollY); + } + + @Override + public boolean charTyped(CharacterEvent event) { + if (this.focusOwner != null && this.focusOwner.charTyped(event)) return true; + return super.charTyped(event); + } + private void clearAllFocus() { this.focusOwner = null; for (UIComponent child : this.rootScope.getChildren()) clearFocusRecursive(child); } + // ── 鼠标拖拽 ── + private void clearFocusRecursive(UIComponent component) { if (component instanceof Focusable f) f.setFocused(false); for (UIComponent child : component.children()) clearFocusRecursive(child); @@ -104,6 +145,8 @@ private void closeAllDropdowns() { for (UIComponent child : this.rootScope.getChildren()) closeDropdownsRecursive(child); } + // ── 鼠标释放 ── + private void closeDropdownsRecursive(UIComponent component) { if (component instanceof DropdownComponent dd) dd.setOpen(false); for (UIComponent child : component.children()) closeDropdownsRecursive(child); @@ -116,10 +159,13 @@ private boolean dispatchMouseClicked(MouseButtonEvent event, boolean isDouble) { return false; } + // ── 滚轮 ── + private boolean dispatchMouseClickedRecursive(UIComponent component, MouseButtonEvent event, boolean isDouble) { - var sorted = component.children().stream() - .sorted(java.util.Comparator.comparingInt(UIComponent::eventPriority).reversed()) - .toList(); + var sorted = component.children() + .stream() + .sorted(java.util.Comparator.comparingInt(UIComponent::eventPriority).reversed()) + .toList(); for (UIComponent child : sorted) { if (dispatchMouseClickedRecursive(child, event, isDouble)) return true; } @@ -130,82 +176,39 @@ private boolean dispatchMouseClickedRecursive(UIComponent component, MouseButton return false; } - // ── 鼠标拖拽 ── - - @Override - public boolean mouseDragged(MouseButtonEvent event, double deltaX, double deltaY) { - for (UIComponent child : this.rootScope.getChildren()) { - if (dispatchMouseDragged(child, event, deltaX, deltaY)) return true; - } - return super.mouseDragged(event, deltaX, deltaY); - } - private boolean dispatchMouseDragged(UIComponent component, MouseButtonEvent event, double dx, double dy) { - var sorted = component.children().stream() - .sorted(java.util.Comparator.comparingInt(UIComponent::eventPriority).reversed()) - .toList(); + var sorted = component.children() + .stream() + .sorted(java.util.Comparator.comparingInt(UIComponent::eventPriority).reversed()) + .toList(); for (UIComponent child : sorted) { if (dispatchMouseDragged(child, event, dx, dy)) return true; } return component.mouseDragged(event, dx, dy); } - // ── 鼠标释放 ── - - @Override - public boolean mouseReleased(MouseButtonEvent event) { - for (UIComponent child : this.rootScope.getChildren()) { - dispatchMouseReleased(child, event); - } - return super.mouseReleased(event); - } + // ── 键盘 ── private void dispatchMouseReleased(UIComponent component, MouseButtonEvent event) { - var sorted = component.children().stream() - .sorted(java.util.Comparator.comparingInt(UIComponent::eventPriority).reversed()) - .toList(); + var sorted = component.children() + .stream() + .sorted(java.util.Comparator.comparingInt(UIComponent::eventPriority).reversed()) + .toList(); for (UIComponent child : sorted) dispatchMouseReleased(child, event); component.mouseReleased(event); } - // ── 滚轮 ── - - @Override - public boolean mouseScrolled(double mouseX, double mouseY, double scrollX, double scrollY) { - for (UIComponent child : this.rootScope.getChildren()) { - if (dispatchMouseScrolled(child, mouseX, mouseY, scrollX, scrollY)) return true; - } - return super.mouseScrolled(mouseX, mouseY, scrollX, scrollY); - } - private boolean dispatchMouseScrolled(UIComponent component, double mx, double my, double sx, double sy) { - var sorted = component.children().stream() - .sorted(java.util.Comparator.comparingInt(UIComponent::eventPriority).reversed()) - .toList(); + var sorted = component.children() + .stream() + .sorted(java.util.Comparator.comparingInt(UIComponent::eventPriority).reversed()) + .toList(); for (UIComponent child : sorted) { if (dispatchMouseScrolled(child, mx, my, sx, sy)) return true; } return component.mouseScrolled(mx, my, sx, sy); } - // ── 键盘 ── - - @Override - public boolean keyPressed(KeyEvent event) { - if (event.key() == 256) { - this.onClose(); - return true; - } - if (this.focusOwner != null && this.focusOwner.keyPressed(event)) return true; - return super.keyPressed(event); - } - - @Override - public boolean charTyped(CharacterEvent event) { - if (this.focusOwner != null && this.focusOwner.charTyped(event)) return true; - return super.charTyped(event); - } - // ── hover ── private void updateHover(float mouseX, float mouseY) { diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Focusable.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Focusable.java index 45f837b4..090db441 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Focusable.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Focusable.java @@ -5,10 +5,13 @@ * 由 {@link DeclarativeScreen} 的焦点系统驱动。 */ public interface Focusable { - - /** 是否已获取焦点。 */ + /** + * 是否已获取焦点。 + */ boolean focused(); - /** 设置焦点状态。 */ + /** + * 设置焦点状态。 + */ void setFocused(boolean focused); } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIComponent.java index 521ec94a..6beca255 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIComponent.java @@ -20,7 +20,12 @@ * 事件处理方法均有默认空实现,子类只覆写需要的。 * {@link DeclarativeScreen} 负责递归遍历组件树并分发事件。 */ -@SuppressWarnings({"unused", "UnusedReturnValue"}) +@SuppressWarnings( + { + "unused", + "UnusedReturnValue" + } +) public interface UIComponent { Modifier modifier(); @@ -35,44 +40,83 @@ public interface UIComponent { // ── 事件处理(默认空实现,子类覆写)── - /** 鼠标点击。返回 true 表示已消费。 */ - default boolean mouseClicked(MouseButtonEvent event, boolean isDoubleClick) { return false; } + /** + * 鼠标点击。 + * + * @return 返回 true 表示已消费。 + */ + default boolean mouseClicked(MouseButtonEvent event, boolean isDoubleClick) { + return false; + } - /** 鼠标拖拽。返回 true 表示已消费。 */ - default boolean mouseDragged(MouseButtonEvent event, double deltaX, double deltaY) { return false; } + /** + * 鼠标拖拽。 + * + * @return 返回 true 表示已消费。 + */ + default boolean mouseDragged(MouseButtonEvent event, double deltaX, double deltaY) { + return false; + } - /** 鼠标释放。 */ - default boolean mouseReleased(MouseButtonEvent event) { return false; } + /** + * 鼠标释放。 + */ + default boolean mouseReleased(MouseButtonEvent event) { + return false; + } - /** 滚轮滚动。返回 true 表示已消费。 */ - default boolean mouseScrolled(double mouseX, double mouseY, double scrollX, double scrollY) { return false; } + /** + * 滚轮滚动。 + * + * @return 返回 true 表示已消费。 + */ + default boolean mouseScrolled(double mouseX, double mouseY, double scrollX, double scrollY) { + return false; + } - /** 按键按下。返回 true 表示已消费。 */ - default boolean keyPressed(KeyEvent event) { return false; } + /** + * 按键按下。 + * + * @return 返回 true 表示已消费。 + */ + default boolean keyPressed(KeyEvent event) { + return false; + } - /** 字符输入。返回 true 表示已消费。 */ - default boolean charTyped(CharacterEvent event) { return false; } + /** + * 字符输入。 + * + * @return 返回 true 表示已消费。 + */ + default boolean charTyped(CharacterEvent event) { + return false; + } - /** 每帧更新 hover 状态。默认空实现。 */ - default void updateHover(float mouseX, float mouseY) {} + /** + * 每帧更新 hover 状态。默认空实现。 + */ + default void updateHover(float mouseX, float mouseY) { + } /** * 事件优先级。值越大越先处理。默认 0。 */ - default int eventPriority() { return 0; } + default int eventPriority() { + return 0; + } /** * 渲染优先级。值越大越后提交渲染状态(上层)。 * 默认 0。弹出层等需置于顶层的组件可覆写为更高值。 */ - default int renderingPriority() { return 0; } + default int renderingPriority() { + return 0; + } /** * 按渲染优先级排序后的子组件列表(低→高,先渲染的在前)。 */ default List sortedChildren() { - return this.children().stream() - .sorted(java.util.Comparator.comparingInt(UIComponent::renderingPriority)) - .toList(); + return this.children().stream().sorted(java.util.Comparator.comparingInt(UIComponent::renderingPriority)).toList(); } } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ButtonComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ButtonComponent.java index 40254bad..f9cbd9b0 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ButtonComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ButtonComponent.java @@ -19,16 +19,21 @@ import java.util.List; @Accessors(fluent = true) -@SuppressWarnings({"unused", "UnusedReturnValue"}) +@SuppressWarnings( + { + "unused", + "UnusedReturnValue" + } +) public class ButtonComponent implements UIComponent { - - private static final int BG_COLOR = 0xFF404040; + private static final int BG_COLOR = 0xFF404040; private static final int BG_HOVER_COLOR = 0xFF606060; - private static final int TEXT_COLOR = 0xFFFFFFFF; - private static final float PADDING_H = 12; - private static final float PADDING_V = 6; + private static final int TEXT_COLOR = 0xFFFFFFFF; + private static final float PADDING_H = 12; + private static final float PADDING_V = 6; - @Getter @Setter + @Getter + @Setter private Modifier modifier; @Setter private String label; @@ -43,13 +48,19 @@ public ButtonComponent(Modifier modifier, String label) { this.label = label; } - public ButtonComponent onClick(@Nullable Runnable onClick) { this.onClick = onClick; return this; } - public void setHovered(boolean hovered) { this.hovered = hovered; } + public ButtonComponent onClick(@Nullable Runnable onClick) { + this.onClick = onClick; + return this; + } - @Override - public void updateHover(float mx, float my) { this.hovered = this.hitRect().contains(mx, my); } + public void setHovered(boolean hovered) { + this.hovered = hovered; + } - @Override public List children() { return Collections.emptyList(); } + @Override + public List children() { + return Collections.emptyList(); + } @Override public MeasuredSize measure(Constraints constraints) { @@ -83,14 +94,6 @@ public void extractRenderState(GuiGraphicsExtractor extractor) { extractor.text(font, this.label, textX, textY, ButtonComponent.TEXT_COLOR, true); } - public LayoutRect hitRect() { - return LayoutRect.of(this.x, this.y, this.width, this.height); - } - - public void click() { - if (this.onClick != null) this.onClick.run(); - } - @Override public boolean mouseClicked(MouseButtonEvent event, boolean isDoubleClick) { if (event.button() != 0) return false; @@ -104,4 +107,17 @@ public boolean mouseClicked(MouseButtonEvent event, boolean isDoubleClick) { } return false; } + + @Override + public void updateHover(float mx, float my) { + this.hovered = this.hitRect().contains(mx, my); + } + + public LayoutRect hitRect() { + return LayoutRect.of(this.x, this.y, this.width, this.height); + } + + public void click() { + if (this.onClick != null) this.onClick.run(); + } } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/CheckboxComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/CheckboxComponent.java index 20fef719..999d10e5 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/CheckboxComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/CheckboxComponent.java @@ -97,6 +97,20 @@ public void extractRenderState(GuiGraphicsExtractor extractor) { // TODO: 用 font.text() 渲染标签 } + @Override + public boolean mouseClicked(MouseButtonEvent event, boolean isDoubleClick) { + if (event.button() != 0) return false; + var mc = Minecraft.getInstance(); + int mx = (int) mc.mouseHandler.getScaledXPos(mc.getWindow()); + int my = (int) mc.mouseHandler.getScaledYPos(mc.getWindow()); + if (this.hitRect().contains(mx, my)) { + this.toggle(); + mc.getSoundManager().play(SimpleSoundInstance.forUI(SoundEvents.UI_BUTTON_CLICK, 1.0f)); + return true; + } + return false; + } + /** * 切换状态。 */ @@ -111,14 +125,4 @@ public void toggle() { public LayoutRect hitRect() { return LayoutRect.of(this.x, this.y, SIZE, SIZE); } - - @Override - public boolean mouseClicked(MouseButtonEvent event, boolean isDoubleClick) { - if (event.button() != 0) return false; - var mc = Minecraft.getInstance(); - int mx = (int) mc.mouseHandler.getScaledXPos(mc.getWindow()); - int my = (int) mc.mouseHandler.getScaledYPos(mc.getWindow()); - if (this.hitRect().contains(mx, my)) { this.toggle(); mc.getSoundManager().play(SimpleSoundInstance.forUI(SoundEvents.UI_BUTTON_CLICK, 1.0f)); return true; } - return false; - } } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ColumnComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ColumnComponent.java index 6265e651..0c086ef0 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ColumnComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ColumnComponent.java @@ -16,10 +16,15 @@ import java.util.List; @Accessors(fluent = true) -@SuppressWarnings({"unused", "UnusedReturnValue"}) +@SuppressWarnings( + { + "unused", + "UnusedReturnValue" + } +) public class ColumnComponent implements UIComponent { - - @Getter @Setter + @Getter + @Setter private Modifier modifier; @Getter private List children = Collections.emptyList(); diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/DropdownComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/DropdownComponent.java index 4677b4be..ffdffcb9 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/DropdownComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/DropdownComponent.java @@ -82,10 +82,10 @@ public void setOpen(boolean open) { this.open = open; if (!this.open) this.popupScrollY = 0; } - public void setHovered(boolean hovered) { this.hovered = hovered; } - @Override - public void updateHover(float mx, float my) { this.hovered = this.hitRect().contains(mx, my); } + public void setHovered(boolean hovered) { + this.hovered = hovered; + } public void setPopupScrollY(float y) { this.popupScrollY = y; @@ -114,17 +114,68 @@ public void layout(float x, float y, float width, float height) { this.height = height; } + @Override + public void extractRenderState(GuiGraphicsExtractor extractor) { + this.renderTrigger(extractor); + this.renderPopup(extractor); + } + // ── 触发器渲染 ── @Override - public int renderingPriority() { - return this.open ? 100 : 0; + public boolean mouseClicked(MouseButtonEvent event, boolean isDoubleClick) { + if (event.button() != 0) return false; + var mc = Minecraft.getInstance(); + int mx = (int) mc.mouseHandler.getScaledXPos(mc.getWindow()); + int my = (int) mc.mouseHandler.getScaledYPos(mc.getWindow()); + if (this.isOnPopupScrollbar(mx, my)) { + this.startPopupScrollbarDrag(my); + return true; + } + if (this.clickPopup(mx, my)) { + mc.getSoundManager().play(SimpleSoundInstance.forUI(SoundEvents.UI_BUTTON_CLICK, 1.0f)); + return true; + } + if (this.clickTrigger(mx, my)) { + mc.getSoundManager().play(SimpleSoundInstance.forUI(SoundEvents.UI_BUTTON_CLICK, 1.0f)); + return true; + } + return false; } @Override - public void extractRenderState(GuiGraphicsExtractor extractor) { - this.renderTrigger(extractor); - this.renderPopup(extractor); + public boolean mouseDragged(MouseButtonEvent event, double deltaX, double deltaY) { + if (!this.scrollbarDragging()) return false; + var mc = Minecraft.getInstance(); + int my = (int) mc.mouseHandler.getScaledYPos(mc.getWindow()); + this.onPopupScrollbarDrag(my); + return true; + } + + @Override + public boolean mouseReleased(MouseButtonEvent event) { + this.stopPopupScrollbarDrag(); + return false; + } + + // ── 弹出层渲染(延迟调用,确保 z-order) ── + + @Override + public boolean mouseScrolled(double mouseX, double mouseY, double scrollX, double scrollY) { + if (this.open() && this.popupRect().contains((float) mouseX, (float) mouseY)) { + return this.onPopupScroll((float) scrollY); + } + return false; + } + + @Override + public void updateHover(float mx, float my) { + this.hovered = this.hitRect().contains(mx, my); + } + + @Override + public int renderingPriority() { + return this.open ? 100 : 0; } private void renderTrigger(GuiGraphicsExtractor extractor) { @@ -164,7 +215,7 @@ private void renderTrigger(GuiGraphicsExtractor extractor) { } } - // ── 弹出层渲染(延迟调用,确保 z-order) ── + // ── 交互 ── /** * 弹出层 item 高度。 @@ -213,7 +264,7 @@ public void renderPopup(GuiGraphicsExtractor extractor) { int my = (int) mc.mouseHandler.getScaledYPos(mc.getWindow()); int hoveredIdx = -1; if (mx >= px && mx < px + pw && my >= py && my < py + (int) ph) { - hoveredIdx = (int) ((my - py) / itemH); + hoveredIdx = (int) ((my - py - this.popupScrollY) / itemH); } float startY = this.y + this.height + this.popupScrollY; @@ -223,9 +274,13 @@ public void renderPopup(GuiGraphicsExtractor extractor) { if (bottom <= this.y + this.height || iy >= this.y + this.height + ph) continue; int bg; - if (i == this.selectedIndex) bg = DropdownComponent.POPUP_HOVER; - else if (i == hoveredIdx) bg = DropdownComponent.SCROLLBAR_COLOR; // 悬停高亮 - else bg = DropdownComponent.POPUP_BG; + if (i == this.selectedIndex) { + bg = DropdownComponent.POPUP_HOVER; + } else if (i == hoveredIdx) { + bg = DropdownComponent.SCROLLBAR_COLOR; // 悬停高亮 + } else { + bg = DropdownComponent.POPUP_BG; + } int fillTop = Math.max((int) iy, py); int fillBot = Math.min((int) bottom, py + (int) ph); extractor.fill(px, fillTop, px + pw, fillBot, bg); @@ -243,8 +298,6 @@ public void renderPopup(GuiGraphicsExtractor extractor) { } } - // ── 交互 ── - /** * 点击触发器区域 → 切换展开。 */ @@ -344,39 +397,4 @@ private LayoutRect triggerRect() { public LayoutRect hitRect() { return this.triggerRect(); } - - @Override - public boolean mouseClicked(MouseButtonEvent event, boolean isDoubleClick) { - if (event.button() != 0) return false; - var mc = Minecraft.getInstance(); - int mx = (int) mc.mouseHandler.getScaledXPos(mc.getWindow()); - int my = (int) mc.mouseHandler.getScaledYPos(mc.getWindow()); - if (this.isOnPopupScrollbar(mx, my)) { this.startPopupScrollbarDrag(my); return true; } - if (this.clickPopup(mx, my)) { mc.getSoundManager().play(SimpleSoundInstance.forUI(SoundEvents.UI_BUTTON_CLICK, 1.0f)); return true; } - if (this.clickTrigger(mx, my)) { mc.getSoundManager().play(SimpleSoundInstance.forUI(SoundEvents.UI_BUTTON_CLICK, 1.0f)); return true; } - return false; - } - - @Override - public boolean mouseScrolled(double mouseX, double mouseY, double scrollX, double scrollY) { - if (this.open() && this.popupRect().contains((float) mouseX, (float) mouseY)) { - return this.onPopupScroll((float) scrollY); - } - return false; - } - - @Override - public boolean mouseDragged(MouseButtonEvent event, double deltaX, double deltaY) { - if (!this.scrollbarDragging()) return false; - var mc = Minecraft.getInstance(); - int my = (int) mc.mouseHandler.getScaledYPos(mc.getWindow()); - this.onPopupScrollbarDrag(my); - return true; - } - - @Override - public boolean mouseReleased(MouseButtonEvent event) { - this.stopPopupScrollbarDrag(); - return false; - } } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/GridComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/GridComponent.java index db6734bc..d6507916 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/GridComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/GridComponent.java @@ -14,11 +14,16 @@ import java.util.List; @Accessors(fluent = true) -@SuppressWarnings({"unused", "UnusedReturnValue"}) +@SuppressWarnings( + { + "unused", + "UnusedReturnValue" + } +) public class GridComponent implements UIComponent { - private final int columns; - @Getter @Setter + @Getter + @Setter private Modifier modifier; @Getter private List children = Collections.emptyList(); diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/RowComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/RowComponent.java index d577f2c6..1624a9ff 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/RowComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/RowComponent.java @@ -16,10 +16,15 @@ import java.util.List; @Accessors(fluent = true) -@SuppressWarnings({"unused", "UnusedReturnValue"}) +@SuppressWarnings( + { + "unused", + "UnusedReturnValue" + } +) public class RowComponent implements UIComponent { - - @Getter @Setter + @Getter + @Setter private Modifier modifier; @Getter private List children = Collections.emptyList(); diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ScrollableComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ScrollableComponent.java index 0e36cf91..c6b93b69 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ScrollableComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ScrollableComponent.java @@ -122,6 +122,42 @@ public void extractRenderState(GuiGraphicsExtractor extractor) { // ── 滚动 ── + @Override + public boolean mouseClicked(MouseButtonEvent event, boolean isDoubleClick) { + if (event.button() != 0) return false; + var mc = Minecraft.getInstance(); + int mx = (int) mc.mouseHandler.getScaledXPos(mc.getWindow()); + int my = (int) mc.mouseHandler.getScaledYPos(mc.getWindow()); + if (this.isOnScrollbar(mx, my)) { + this.startScrollbarDrag(my); + return true; + } + return false; + } + + @Override + public boolean mouseDragged(MouseButtonEvent event, double deltaX, double deltaY) { + if (!this.scrollbarDragging()) return false; + var mc = Minecraft.getInstance(); + int my = (int) mc.mouseHandler.getScaledYPos(mc.getWindow()); + this.onScrollbarDrag(my); + return true; + } + + @Override + public boolean mouseReleased(MouseButtonEvent event) { + this.stopScrollbarDrag(); + return false; + } + + @Override + public boolean mouseScrolled(double mouseX, double mouseY, double scrollX, double scrollY) { + if (this.hitRect().contains((float) mouseX, (float) mouseY)) { + return this.onScroll((float) scrollY); + } + return false; + } + public boolean onScroll(float amount) { if (this.contentHeight <= this.height) return false; float maxScroll = this.contentHeight - this.height; @@ -167,7 +203,6 @@ public void stopScrollbarDrag() { this.scrollbarDragging = false; } - private float barH() { return Math.max(16, this.height * this.height / this.contentHeight); } @@ -187,37 +222,4 @@ public void setScrollY(float scrollY) { public LayoutRect hitRect() { return LayoutRect.of(this.x, this.y, this.width, this.height); } - - @Override - public boolean mouseClicked(MouseButtonEvent event, boolean isDoubleClick) { - if (event.button() != 0) return false; - var mc = Minecraft.getInstance(); - int mx = (int) mc.mouseHandler.getScaledXPos(mc.getWindow()); - int my = (int) mc.mouseHandler.getScaledYPos(mc.getWindow()); - if (this.isOnScrollbar(mx, my)) { this.startScrollbarDrag(my); return true; } - return false; - } - - @Override - public boolean mouseScrolled(double mouseX, double mouseY, double scrollX, double scrollY) { - if (this.hitRect().contains((float) mouseX, (float) mouseY)) { - return this.onScroll((float) scrollY); - } - return false; - } - - @Override - public boolean mouseDragged(MouseButtonEvent event, double deltaX, double deltaY) { - if (!this.scrollbarDragging()) return false; - var mc = Minecraft.getInstance(); - int my = (int) mc.mouseHandler.getScaledYPos(mc.getWindow()); - this.onScrollbarDrag(my); - return true; - } - - @Override - public boolean mouseReleased(MouseButtonEvent event) { - this.stopScrollbarDrag(); - return false; - } } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/SliderComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/SliderComponent.java index 886aeb8e..d77c1ffc 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/SliderComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/SliderComponent.java @@ -58,11 +58,10 @@ public SliderComponent(Modifier modifier, float value, float min, float max, flo this.max = max; this.trackWidth = trackWidth; } - public void setHovered(boolean hovered) { this.hovered = hovered; } - - @Override - public void updateHover(float mx, float my) { this.hovered = this.hitRect().contains(mx, my); } + public void setHovered(boolean hovered) { + this.hovered = hovered; + } @Override public List children() { @@ -102,6 +101,37 @@ public void extractRenderState(GuiGraphicsExtractor extractor) { extractor.fill(tix, tiy, tix + (int) SliderComponent.THUMB_W, tiy + (int) SliderComponent.THUMB_H, thumbColor); } + @Override + public boolean mouseClicked(MouseButtonEvent event, boolean isDoubleClick) { + if (event.button() != 0) return false; + var mc = Minecraft.getInstance(); + int mx = (int) mc.mouseHandler.getScaledXPos(mc.getWindow()); + int my = (int) mc.mouseHandler.getScaledYPos(mc.getWindow()); + if (this.hitRect().contains(mx, my)) { + this.setValueFromMouse(mx); + mc.getSoundManager().play(SimpleSoundInstance.forUI(SoundEvents.UI_BUTTON_CLICK, 1.0f)); + return true; + } + return false; + } + + @Override + public boolean mouseDragged(MouseButtonEvent event, double deltaX, double deltaY) { + var mc = Minecraft.getInstance(); + int mx = (int) mc.mouseHandler.getScaledXPos(mc.getWindow()); + int my = (int) mc.mouseHandler.getScaledYPos(mc.getWindow()); + if (this.hitRect().contains(mx, my)) { + this.setValueFromMouse(mx); + return true; + } + return false; + } + + @Override + public void updateHover(float mx, float my) { + this.hovered = this.hitRect().contains(mx, my); + } + /** * 根据鼠标 X 坐标更新值。 */ @@ -120,23 +150,4 @@ public void setValueFromMouse(float mouseX) { public LayoutRect hitRect() { return LayoutRect.of(this.x, this.y, this.width, this.height); } - - @Override - public boolean mouseClicked(MouseButtonEvent event, boolean isDoubleClick) { - if (event.button() != 0) return false; - var mc = Minecraft.getInstance(); - int mx = (int) mc.mouseHandler.getScaledXPos(mc.getWindow()); - int my = (int) mc.mouseHandler.getScaledYPos(mc.getWindow()); - if (this.hitRect().contains(mx, my)) { this.setValueFromMouse(mx); mc.getSoundManager().play(SimpleSoundInstance.forUI(SoundEvents.UI_BUTTON_CLICK, 1.0f)); return true; } - return false; - } - - @Override - public boolean mouseDragged(MouseButtonEvent event, double deltaX, double deltaY) { - var mc = Minecraft.getInstance(); - int mx = (int) mc.mouseHandler.getScaledXPos(mc.getWindow()); - int my = (int) mc.mouseHandler.getScaledYPos(mc.getWindow()); - if (this.hitRect().contains(mx, my)) { this.setValueFromMouse(mx); return true; } - return false; - } } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextInputComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextInputComponent.java index 133c75ad..b90f37c8 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextInputComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextInputComponent.java @@ -1,10 +1,10 @@ package dev.anvilcraft.lib.v2.ui.component; import dev.anvilcraft.lib.v2.ui.Constraints; +import dev.anvilcraft.lib.v2.ui.Focusable; import dev.anvilcraft.lib.v2.ui.LayoutRect; import dev.anvilcraft.lib.v2.ui.MeasuredSize; import dev.anvilcraft.lib.v2.ui.Modifier; -import dev.anvilcraft.lib.v2.ui.Focusable; import dev.anvilcraft.lib.v2.ui.UIComponent; import lombok.Getter; import lombok.Setter; @@ -115,6 +115,20 @@ public void extractRenderState(GuiGraphicsExtractor extractor) { } } + @Override + public boolean mouseClicked(MouseButtonEvent event, boolean isDoubleClick) { + if (event.button() != 0) return false; + var mc = Minecraft.getInstance(); + int mx = (int) mc.mouseHandler.getScaledXPos(mc.getWindow()); + int my = (int) mc.mouseHandler.getScaledYPos(mc.getWindow()); + if (this.hitRect().contains(mx, my)) { + this.setFocused(true); + mc.getSoundManager().play(SimpleSoundInstance.forUI(SoundEvents.UI_BUTTON_CLICK, 1.0f)); + return true; + } + return false; + } + @Override public boolean keyPressed(KeyEvent event) { int key = event.key(); @@ -174,14 +188,4 @@ private void fireChange() { public LayoutRect hitRect() { return LayoutRect.of(this.x, this.y, this.width, this.height); } - - @Override - public boolean mouseClicked(MouseButtonEvent event, boolean isDoubleClick) { - if (event.button() != 0) return false; - var mc = Minecraft.getInstance(); - int mx = (int) mc.mouseHandler.getScaledXPos(mc.getWindow()); - int my = (int) mc.mouseHandler.getScaledYPos(mc.getWindow()); - if (this.hitRect().contains(mx, my)) { this.setFocused(true); mc.getSoundManager().play(SimpleSoundInstance.forUI(SoundEvents.UI_BUTTON_CLICK, 1.0f)); return true; } - return false; - } } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/SizeElement.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/SizeElement.java index f8c80af9..005b2e0a 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/SizeElement.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/modifier/SizeElement.java @@ -15,7 +15,9 @@ public Constraints modifyConstraints(Constraints constraints) { float minW = this.minWidthHint() > 0 ? Math.max(constraints.minWidth(), this.minWidthHint()) : constraints.minWidth(); float maxW = this.maxWidthHint() < Float.MAX_VALUE ? Math.min(constraints.maxWidth(), this.maxWidthHint()) : constraints.maxWidth(); float minH = this.minHeightHint() > 0 ? Math.max(constraints.minHeight(), this.minHeightHint()) : constraints.minHeight(); - float maxH = this.maxHeightHint() < Float.MAX_VALUE ? Math.min(constraints.maxHeight(), this.maxHeightHint()) : constraints.maxHeight(); + float maxH = this.maxHeightHint() < Float.MAX_VALUE + ? Math.min(constraints.maxHeight(), this.maxHeightHint()) + : constraints.maxHeight(); return constraints.copy(minW, maxW, minH, maxH); } } diff --git a/module.ui/src/main/resources/META-INF/neoforge.mods.toml b/module.ui/src/main/resources/META-INF/neoforge.mods.toml index 27f7ba75..70445b6c 100644 --- a/module.ui/src/main/resources/META-INF/neoforge.mods.toml +++ b/module.ui/src/main/resources/META-INF/neoforge.mods.toml @@ -23,9 +23,9 @@ displayName = "${mod_name}" #mandatory # A URL to query for updates for this mod. See the JSON update specification https://docs.neoforge.net/docs/misc/updatechecker/ #updateJSONURL="https://change.me.example.invalid/updates.json" #optional # A URL for the "homepage" for this mod, displayed in the mod UI -displayURL="https://github.com/Anvil-Dev/AnvilLib" +displayURL = "https://github.com/Anvil-Dev/AnvilLib" # A file name (in the root of the mod JAR) containing a logo for display -logoFile="icon.png" #optional +logoFile = "icon.png" #optional # A text field displayed in the mod UI #credits="" #optional # A text field displayed in the mod UI From 02b36a4c2638cd51ba86e5a24839168d025f1b08 Mon Sep 17 00:00:00 2001 From: Gugle Date: Tue, 19 May 2026 19:15:02 +0800 Subject: [PATCH 58/67] feat(ui): fix scrollbar position calculations in DropdownComponent and update mod name formatting --- module.ui/gradle.properties | 2 +- .../lib/v2/ui/component/DropdownComponent.java | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/module.ui/gradle.properties b/module.ui/gradle.properties index 1a717a8d..9a815e39 100644 --- a/module.ui/gradle.properties +++ b/module.ui/gradle.properties @@ -1,4 +1,4 @@ ## Mod Properties mod_id=anvillib_ui -mod_name=AnvilLib-Ui +mod_name=AnvilLib-UI mod_description=A simple declarative UI library \ No newline at end of file diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/DropdownComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/DropdownComponent.java index ffdffcb9..aa99a949 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/DropdownComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/DropdownComponent.java @@ -347,11 +347,12 @@ public boolean onPopupScroll(float amount) { public boolean isOnPopupScrollbar(float mx, float my) { if (!this.open) return false; float ph = this.popupHeight(); + int py = (int) (this.y + this.height); float totalH = this.options.length * this.itemHeight(); if (totalH <= ph) return false; float bh = Math.max(12, ph * ph / totalH); float maxScroll = totalH - ph; - float by = this.y + this.height + (-this.popupScrollY / maxScroll) * (ph - bh); + float by = py + (-this.popupScrollY / maxScroll) * (ph - bh); int bx = (int) (this.x + this.width - DropdownComponent.SCROLLBAR_W - 1); return mx >= bx && mx < bx + DropdownComponent.SCROLLBAR_W && my >= by && my < by + bh; } @@ -359,21 +360,23 @@ public boolean isOnPopupScrollbar(float mx, float my) { public void startPopupScrollbarDrag(float my) { this.scrollbarDragging = true; float ph = this.popupHeight(); + int py = (int) (this.y + this.height); float totalH = this.options.length * this.itemHeight(); float bh = Math.max(12, ph * ph / totalH); float maxScroll = totalH - ph; - float by = this.y + this.height + (-this.popupScrollY / maxScroll) * (ph - bh); + float by = py + (-this.popupScrollY / maxScroll) * (ph - bh); this.dragAnchorY = my - by; } public void onPopupScrollbarDrag(float my) { if (!this.scrollbarDragging) return; float ph = this.popupHeight(); + int py = (int) (this.y + this.height); float totalH = this.options.length * this.itemHeight(); float bh = Math.max(12, ph * ph / totalH); float maxScroll = totalH - ph; float newBarY = my - this.dragAnchorY; - float ratio = Mth.clamp(newBarY / (ph - bh), 0f, 1f); + float ratio = Mth.clamp((newBarY - py) / (ph - bh), 0f, 1f); this.popupScrollY = -(ratio * maxScroll); } From 20d68d9a54bf20edcd758be2d3becefbf98b7bc3 Mon Sep 17 00:00:00 2001 From: Gugle Date: Tue, 19 May 2026 19:28:16 +0800 Subject: [PATCH 59/67] feat(ui): add dragging state to SliderComponent and update mouse event handling --- .../dev/anvilcraft/lib/v2/ui/Composition.java | 4 ++++ .../lib/v2/ui/component/SliderComponent.java | 20 ++++++++++++++----- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Composition.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Composition.java index 002fd966..ad9c1b76 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Composition.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Composition.java @@ -2,6 +2,7 @@ import dev.anvilcraft.lib.v2.ui.component.DropdownComponent; import dev.anvilcraft.lib.v2.ui.component.ScrollableComponent; +import dev.anvilcraft.lib.v2.ui.component.SliderComponent; import dev.anvilcraft.lib.v2.ui.component.TextInputComponent; import dev.anvilcraft.lib.v2.ui.modifier.ModifierElement; import lombok.Setter; @@ -154,6 +155,9 @@ private void copyRuntimeState(UIComponent old, UIComponent replacement) { newDd.setOpen(oldDd.open()); newDd.setPopupScrollY(oldDd.popupScrollY()); } + if (old instanceof SliderComponent oldSl && replacement instanceof SliderComponent newSl) { + newSl.setDragging(oldSl.dragging()); + } } // ── 每帧入口 ── diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/SliderComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/SliderComponent.java index d77c1ffc..fd6f0e69 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/SliderComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/SliderComponent.java @@ -45,6 +45,8 @@ public class SliderComponent implements UIComponent { @Getter private float value; private boolean hovered; + @Getter + private boolean dragging; @Setter private @Nullable Consumer onChange; @@ -63,6 +65,10 @@ public void setHovered(boolean hovered) { this.hovered = hovered; } + public void setDragging(boolean dragging) { + this.dragging = dragging; + } + @Override public List children() { return Collections.emptyList(); @@ -108,6 +114,7 @@ public boolean mouseClicked(MouseButtonEvent event, boolean isDoubleClick) { int mx = (int) mc.mouseHandler.getScaledXPos(mc.getWindow()); int my = (int) mc.mouseHandler.getScaledYPos(mc.getWindow()); if (this.hitRect().contains(mx, my)) { + this.dragging = true; this.setValueFromMouse(mx); mc.getSoundManager().play(SimpleSoundInstance.forUI(SoundEvents.UI_BUTTON_CLICK, 1.0f)); return true; @@ -117,13 +124,16 @@ public boolean mouseClicked(MouseButtonEvent event, boolean isDoubleClick) { @Override public boolean mouseDragged(MouseButtonEvent event, double deltaX, double deltaY) { + if (!this.dragging) return false; var mc = Minecraft.getInstance(); int mx = (int) mc.mouseHandler.getScaledXPos(mc.getWindow()); - int my = (int) mc.mouseHandler.getScaledYPos(mc.getWindow()); - if (this.hitRect().contains(mx, my)) { - this.setValueFromMouse(mx); - return true; - } + this.setValueFromMouse(mx); + return true; + } + + @Override + public boolean mouseReleased(MouseButtonEvent event) { + this.dragging = false; return false; } From 08549b5c0db807ee856d460b3726e37cdc82cffa Mon Sep 17 00:00:00 2001 From: Gugle Date: Tue, 19 May 2026 19:33:11 +0800 Subject: [PATCH 60/67] feat(ui): streamline runtime state copying in UI components --- .../dev/anvilcraft/lib/v2/ui/Composition.java | 27 +------------------ .../dev/anvilcraft/lib/v2/ui/UIComponent.java | 6 +++++ .../v2/ui/component/DropdownComponent.java | 8 ++++++ .../v2/ui/component/ScrollableComponent.java | 7 +++++ .../lib/v2/ui/component/SliderComponent.java | 7 +++++ .../v2/ui/component/TextInputComponent.java | 9 +++++++ 6 files changed, 38 insertions(+), 26 deletions(-) diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Composition.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Composition.java index ad9c1b76..f951465c 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Composition.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Composition.java @@ -1,9 +1,5 @@ package dev.anvilcraft.lib.v2.ui; -import dev.anvilcraft.lib.v2.ui.component.DropdownComponent; -import dev.anvilcraft.lib.v2.ui.component.ScrollableComponent; -import dev.anvilcraft.lib.v2.ui.component.SliderComponent; -import dev.anvilcraft.lib.v2.ui.component.TextInputComponent; import dev.anvilcraft.lib.v2.ui.modifier.ModifierElement; import lombok.Setter; import net.minecraft.client.gui.GuiGraphicsExtractor; @@ -127,7 +123,7 @@ public void emit(UIComponent component) { slot = this.slots.get(this.currentIndex); UIComponent old = slot.component; if (old != null && old.getClass() == component.getClass()) { - this.copyRuntimeState(old, component); + component.copyRuntimeState(old); } slot.component = component; } else { @@ -139,27 +135,6 @@ public void emit(UIComponent component) { this.currentIndex++; } - /** - * 将旧组件的运行时状态复制到新组件。 - */ - private void copyRuntimeState(UIComponent old, UIComponent replacement) { - if (old instanceof ScrollableComponent oldSc && replacement instanceof ScrollableComponent newSc) { - newSc.setScrollY(oldSc.scrollY()); - } - if (old instanceof TextInputComponent oldTi && replacement instanceof TextInputComponent newTi) { - newTi.setValue(oldTi.value()); - newTi.setCursorPos(oldTi.cursorPos()); - newTi.setFocused(oldTi.focused()); - } - if (old instanceof DropdownComponent oldDd && replacement instanceof DropdownComponent newDd) { - newDd.setOpen(oldDd.open()); - newDd.setPopupScrollY(oldDd.popupScrollY()); - } - if (old instanceof SliderComponent oldSl && replacement instanceof SliderComponent newSl) { - newSl.setDragging(oldSl.dragging()); - } - } - // ── 每帧入口 ── /** diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIComponent.java index 6beca255..489b8dbc 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIComponent.java @@ -119,4 +119,10 @@ default int renderingPriority() { default List sortedChildren() { return this.children().stream().sorted(java.util.Comparator.comparingInt(UIComponent::renderingPriority)).toList(); } + + /** + * 将旧组件的运行时状态复制到新组件。 + */ + default void copyRuntimeState(UIComponent old) { + } } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/DropdownComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/DropdownComponent.java index aa99a949..c437f37c 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/DropdownComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/DropdownComponent.java @@ -400,4 +400,12 @@ private LayoutRect triggerRect() { public LayoutRect hitRect() { return this.triggerRect(); } + + @Override + public void copyRuntimeState(UIComponent old) { + if (old instanceof DropdownComponent oldDd) { + this.setOpen(oldDd.open()); + this.setPopupScrollY(oldDd.popupScrollY()); + } + } } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ScrollableComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ScrollableComponent.java index c6b93b69..c7958ffb 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ScrollableComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ScrollableComponent.java @@ -222,4 +222,11 @@ public void setScrollY(float scrollY) { public LayoutRect hitRect() { return LayoutRect.of(this.x, this.y, this.width, this.height); } + + @Override + public void copyRuntimeState(UIComponent old) { + if (old instanceof ScrollableComponent oldSc) { + this.setScrollY(oldSc.scrollY()); + } + } } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/SliderComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/SliderComponent.java index fd6f0e69..4b408a66 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/SliderComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/SliderComponent.java @@ -160,4 +160,11 @@ public void setValueFromMouse(float mouseX) { public LayoutRect hitRect() { return LayoutRect.of(this.x, this.y, this.width, this.height); } + + @Override + public void copyRuntimeState(UIComponent old) { + if (old instanceof SliderComponent oldSl) { + this.setDragging(oldSl.dragging()); + } + } } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextInputComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextInputComponent.java index b90f37c8..c6e5aa9c 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextInputComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextInputComponent.java @@ -188,4 +188,13 @@ private void fireChange() { public LayoutRect hitRect() { return LayoutRect.of(this.x, this.y, this.width, this.height); } + + @Override + public void copyRuntimeState(UIComponent old) { + if (old instanceof TextInputComponent oldTi) { + this.setValue(oldTi.value()); + this.setCursorPos(oldTi.cursorPos()); + this.setFocused(oldTi.focused()); + } + } } From bb72491ecfdcf383ff34a412d5f9460726b834b0 Mon Sep 17 00:00:00 2001 From: Gugle Date: Tue, 19 May 2026 19:35:54 +0800 Subject: [PATCH 61/67] feat(ui): enhance TextInputComponent with cursor scrolling and display position management --- .../v2/ui/component/TextInputComponent.java | 64 ++++++++++++++----- 1 file changed, 47 insertions(+), 17 deletions(-) diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextInputComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextInputComponent.java index c6e5aa9c..827b20c4 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextInputComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextInputComponent.java @@ -48,6 +48,7 @@ public class TextInputComponent implements UIComponent, Focusable { private boolean focused; @Setter private @Nullable Consumer onChange; + @Getter private int displayPos; @Getter private int cursorPos; @@ -62,10 +63,23 @@ public TextInputComponent(Modifier modifier, @Nullable String placeholder) { public void setValue(@Nullable String value) { this.value = value != null ? value : ""; this.cursorPos = this.value.length(); + this.scrollTo(this.cursorPos); } public void setCursorPos(int pos) { this.cursorPos = Math.clamp(pos, 0, this.value.length()); + this.scrollTo(this.cursorPos); + } + + private void scrollTo(int pos) { + if (pos < this.displayPos) { this.displayPos = pos; return; } + var font = Minecraft.getInstance().font; + float visibleW = this.width - PADDING_H * 2; + while (this.displayPos < pos) { + String segment = this.value.substring(this.displayPos, pos); + if (font.width(segment) <= visibleW) break; + this.displayPos++; + } } public void setFocused(boolean focused) { @@ -97,6 +111,7 @@ public void layout(float x, float y, float width, float height) { @Override public void extractRenderState(GuiGraphicsExtractor extractor) { int ix = (int) this.x, iy = (int) this.y, iw = (int) this.width, ih = (int) this.height; + extractor.enableScissor(ix, iy, ix + iw, iy + ih); extractor.fill(ix, iy, ix + iw, iy + ih, TextInputComponent.BG_COLOR); var font = Minecraft.getInstance().font; @@ -104,15 +119,19 @@ public void extractRenderState(GuiGraphicsExtractor extractor) { int textY = (int) (this.y + (this.height + font.lineHeight) / 2f - font.lineHeight); boolean hasText = !this.value.isEmpty(); - String display = hasText ? this.value : this.placeholder; - int color = hasText ? TextInputComponent.TEXT_COLOR : TextInputComponent.PLACEHOLDER_COLOR; - extractor.text(font, display, textX, textY, color); + if (hasText) { + String visible = this.value.substring(this.displayPos); + extractor.text(font, visible, textX, textY, TextInputComponent.TEXT_COLOR); - if (this.focused && hasText) { - String before = this.value.substring(0, Math.min(this.cursorPos, this.value.length())); - int cursorX = (int) (this.x + TextInputComponent.PADDING_H + font.width(before)); - extractor.fill(cursorX, iy + 2, cursorX + 1, iy + ih - 2, TextInputComponent.CURSOR_COLOR); + if (this.focused) { + String before = this.value.substring(this.displayPos, Math.min(this.cursorPos, this.value.length())); + int cursorX = textX + font.width(before); + extractor.fill(cursorX, iy + 2, cursorX + 1, iy + ih - 2, TextInputComponent.CURSOR_COLOR); + } + } else { + extractor.text(font, this.placeholder, textX, textY, TextInputComponent.PLACEHOLDER_COLOR); } + extractor.disableScissor(); } @Override @@ -123,6 +142,17 @@ public boolean mouseClicked(MouseButtonEvent event, boolean isDoubleClick) { int my = (int) mc.mouseHandler.getScaledYPos(mc.getWindow()); if (this.hitRect().contains(mx, my)) { this.setFocused(true); + // 根据点击位置设置光标 + var font = Minecraft.getInstance().font; + float relX = mx - (this.x + PADDING_H); + String visible = this.value.substring(this.displayPos); + int pos = this.displayPos; + for (int i = 0; i < visible.length(); i++) { + if (font.width(visible.substring(0, i + 1)) > relX) break; + pos++; + } + this.cursorPos = Math.clamp(pos, 0, this.value.length()); + this.scrollTo(this.cursorPos); mc.getSoundManager().play(SimpleSoundInstance.forUI(SoundEvents.UI_BUTTON_CLICK, 1.0f)); return true; } @@ -136,6 +166,7 @@ public boolean keyPressed(KeyEvent event) { if (this.cursorPos > 0) { this.value = new StringBuilder(this.value).deleteCharAt(this.cursorPos - 1).toString(); this.cursorPos--; + this.scrollTo(this.cursorPos); this.fireChange(); } return true; @@ -143,26 +174,21 @@ public boolean keyPressed(KeyEvent event) { if (key == 261) { if (this.cursorPos < this.value.length()) { this.value = new StringBuilder(this.value).deleteCharAt(this.cursorPos).toString(); + this.scrollTo(this.cursorPos); this.fireChange(); } return true; } if (key == 263) { - if (this.cursorPos > 0) this.cursorPos--; + if (this.cursorPos > 0) { this.cursorPos--; this.scrollTo(this.cursorPos); } return true; } if (key == 262) { - if (this.cursorPos < this.value.length()) this.cursorPos++; - return true; - } - if (key == 268) { - this.cursorPos = 0; - return true; - } - if (key == 269) { - this.cursorPos = this.value.length(); + if (this.cursorPos < this.value.length()) { this.cursorPos++; this.scrollTo(this.cursorPos); } return true; } + if (key == 268) { this.cursorPos = 0; this.scrollTo(this.cursorPos); return true; } + if (key == 269) { this.cursorPos = this.value.length(); this.scrollTo(this.cursorPos); return true; } return false; } @@ -178,6 +204,7 @@ public boolean charTyped(CharacterEvent event) { private void insertText(String text) { this.value = new StringBuilder(this.value).insert(this.cursorPos, text).toString(); this.cursorPos += text.length(); + this.scrollTo(this.cursorPos); this.fireChange(); } @@ -189,12 +216,15 @@ public LayoutRect hitRect() { return LayoutRect.of(this.x, this.y, this.width, this.height); } + public void setDisplayPos(int pos) { this.displayPos = pos; } + @Override public void copyRuntimeState(UIComponent old) { if (old instanceof TextInputComponent oldTi) { this.setValue(oldTi.value()); this.setCursorPos(oldTi.cursorPos()); this.setFocused(oldTi.focused()); + this.setDisplayPos(oldTi.displayPos()); } } } From cf35327ed303b445f535c3144cb063f45768f117 Mon Sep 17 00:00:00 2001 From: Gugle Date: Tue, 19 May 2026 19:51:55 +0800 Subject: [PATCH 62/67] feat(ui): enhance TextInputComponent with text selection and improved cursor handling --- .../v2/ui/component/TextInputComponent.java | 217 ++++++++++++------ 1 file changed, 151 insertions(+), 66 deletions(-) diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextInputComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextInputComponent.java index 827b20c4..82092682 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextInputComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/TextInputComponent.java @@ -24,23 +24,19 @@ import java.util.function.Consumer; @Accessors(fluent = true) -@SuppressWarnings( - { - "unused", - "UnusedReturnValue" - } -) +@SuppressWarnings({"unused", "UnusedReturnValue"}) public class TextInputComponent implements UIComponent, Focusable { + private static final int BG_COLOR = 0xFF202020; private static final int TEXT_COLOR = 0xFFFFFFFF; private static final int PLACEHOLDER_COLOR = 0xFF555555; private static final int CURSOR_COLOR = 0xFFFFFFFF; + private static final int HIGHLIGHT_COLOR = 0xFF8888FF; private static final float PADDING_H = 4; private static final float PADDING_V = 4; private static final float WIDTH = 160; private final String placeholder; - @Getter - @Setter + @Getter @Setter private Modifier modifier; @Getter private String value = ""; @@ -52,6 +48,8 @@ public class TextInputComponent implements UIComponent, Focusable { private int displayPos; @Getter private int cursorPos; + @Getter + private int highlightPos; private float x, y, width, height; @@ -63,11 +61,17 @@ public TextInputComponent(Modifier modifier, @Nullable String placeholder) { public void setValue(@Nullable String value) { this.value = value != null ? value : ""; this.cursorPos = this.value.length(); + this.highlightPos = this.cursorPos; this.scrollTo(this.cursorPos); } public void setCursorPos(int pos) { + this.setCursorPos(pos, false); + } + + public void setCursorPos(int pos, boolean extendSelection) { this.cursorPos = Math.clamp(pos, 0, this.value.length()); + if (!extendSelection) this.highlightPos = this.cursorPos; this.scrollTo(this.cursorPos); } @@ -75,123 +79,197 @@ private void scrollTo(int pos) { if (pos < this.displayPos) { this.displayPos = pos; return; } var font = Minecraft.getInstance().font; float visibleW = this.width - PADDING_H * 2; - while (this.displayPos < pos) { - String segment = this.value.substring(this.displayPos, pos); - if (font.width(segment) <= visibleW) break; + // 光标右溢出时,右移 displayPos 直到光标可见 + while (this.displayPos < pos && font.width(this.value.substring(this.displayPos, pos)) > visibleW) { this.displayPos++; } } - public void setFocused(boolean focused) { - this.focused = focused; - } + public void setFocused(boolean focused) { this.focused = focused; } + public void setDisplayPos(int pos) { this.displayPos = pos; } - @Override - public List children() { - return Collections.emptyList(); + /** 获取选中文本。 */ + public String getHighlighted() { + int start = Math.min(this.cursorPos, this.highlightPos); + int end = Math.max(this.cursorPos, this.highlightPos); + return this.value.substring(start, end); } + @Override public List children() { return Collections.emptyList(); } + @Override public MeasuredSize measure(Constraints constraints) { var font = Minecraft.getInstance().font; - return MeasuredSize.of( - constraints.constrainWidth(TextInputComponent.WIDTH), - constraints.constrainHeight(font.lineHeight + PADDING_V * 2) - ); + return MeasuredSize.of(constraints.constrainWidth(WIDTH), constraints.constrainHeight(font.lineHeight + PADDING_V * 2)); } @Override public void layout(float x, float y, float width, float height) { - this.x = x; - this.y = y; - this.width = width; - this.height = height; + this.x = x; this.y = y; this.width = width; this.height = height; } @Override public void extractRenderState(GuiGraphicsExtractor extractor) { int ix = (int) this.x, iy = (int) this.y, iw = (int) this.width, ih = (int) this.height; extractor.enableScissor(ix, iy, ix + iw, iy + ih); - extractor.fill(ix, iy, ix + iw, iy + ih, TextInputComponent.BG_COLOR); + extractor.fill(ix, iy, ix + iw, iy + ih, BG_COLOR); var font = Minecraft.getInstance().font; - int textX = ix + (int) TextInputComponent.PADDING_H; + int textX = ix + (int) PADDING_H; int textY = (int) (this.y + (this.height + font.lineHeight) / 2f - font.lineHeight); boolean hasText = !this.value.isEmpty(); if (hasText) { String visible = this.value.substring(this.displayPos); - extractor.text(font, visible, textX, textY, TextInputComponent.TEXT_COLOR); + int selStart = Math.min(this.cursorPos, this.highlightPos) - this.displayPos; + int selEnd = Math.max(this.cursorPos, this.highlightPos) - this.displayPos; + + if (selStart < selEnd) { + // 选中区域前 + if (selStart > 0) { + String before = visible.substring(0, Math.min(selStart, visible.length())); + extractor.text(font, before, textX, textY, TEXT_COLOR); + textX += font.width(before); + } + // 选中区域(反色高亮) + String selected = visible.substring(Math.max(0, selStart), Math.min(selEnd, visible.length())); + int selW = font.width(selected); + if (selW > 0) { + extractor.fill(textX, iy + 1, textX + selW, iy + ih - 1, HIGHLIGHT_COLOR); + extractor.text(font, selected, textX, textY, TEXT_COLOR); + textX += selW; + } + // 选中区域后 + if (selEnd < visible.length()) { + extractor.text(font, visible.substring(selEnd), textX, textY, TEXT_COLOR); + } + } else { + extractor.text(font, visible, textX, textY, TEXT_COLOR); + } if (this.focused) { String before = this.value.substring(this.displayPos, Math.min(this.cursorPos, this.value.length())); - int cursorX = textX + font.width(before); - extractor.fill(cursorX, iy + 2, cursorX + 1, iy + ih - 2, TextInputComponent.CURSOR_COLOR); + int cursorX = ix + (int) PADDING_H + font.width(before); + extractor.fill(cursorX, iy + 2, cursorX + 1, iy + ih - 2, CURSOR_COLOR); } } else { - extractor.text(font, this.placeholder, textX, textY, TextInputComponent.PLACEHOLDER_COLOR); + extractor.text(font, this.placeholder, textX, textY, PLACEHOLDER_COLOR); } extractor.disableScissor(); } + // ── 鼠标 ── + @Override public boolean mouseClicked(MouseButtonEvent event, boolean isDoubleClick) { if (event.button() != 0) return false; var mc = Minecraft.getInstance(); int mx = (int) mc.mouseHandler.getScaledXPos(mc.getWindow()); - int my = (int) mc.mouseHandler.getScaledYPos(mc.getWindow()); - if (this.hitRect().contains(mx, my)) { + if (this.hitRect().contains(mx, (float) mc.mouseHandler.getScaledYPos(mc.getWindow()))) { this.setFocused(true); - // 根据点击位置设置光标 - var font = Minecraft.getInstance().font; - float relX = mx - (this.x + PADDING_H); - String visible = this.value.substring(this.displayPos); - int pos = this.displayPos; - for (int i = 0; i < visible.length(); i++) { - if (font.width(visible.substring(0, i + 1)) > relX) break; - pos++; - } - this.cursorPos = Math.clamp(pos, 0, this.value.length()); - this.scrollTo(this.cursorPos); + int pos = posFromMouse(mx); + this.setCursorPos(pos, event.hasShiftDown()); mc.getSoundManager().play(SimpleSoundInstance.forUI(SoundEvents.UI_BUTTON_CLICK, 1.0f)); return true; } return false; } + @Override + public boolean mouseDragged(MouseButtonEvent event, double deltaX, double deltaY) { + if (!this.focused) return false; + var mc = Minecraft.getInstance(); + int mx = (int) mc.mouseHandler.getScaledXPos(mc.getWindow()); + this.setCursorPos(posFromMouse(mx), true); + return true; + } + + private int posFromMouse(int mx) { + var font = Minecraft.getInstance().font; + float relX = mx - (this.x + PADDING_H); + String visible = this.value.substring(this.displayPos); + float visibleW = font.width(visible); + // 鼠标在可见区域外右侧 → 定位到文本末尾,scrollTo 会自动跟进 + if (relX >= visibleW) return this.value.length(); + // 鼠标在可见区域外左侧 → 逐字符左移 displayPos + if (relX <= 0) return Math.max(0, this.displayPos - 1); + int pos = this.displayPos; + for (int i = 0; i < visible.length(); i++) { + if (font.width(visible.substring(0, i + 1)) > relX) break; + pos++; + } + return Math.clamp(pos, 0, this.value.length()); + } + + // ── 键盘 ── + @Override public boolean keyPressed(KeyEvent event) { int key = event.key(); - if (key == 259) { - if (this.cursorPos > 0) { - this.value = new StringBuilder(this.value).deleteCharAt(this.cursorPos - 1).toString(); - this.cursorPos--; - this.scrollTo(this.cursorPos); - this.fireChange(); + boolean shift = event.hasShiftDown(); + boolean ctrl = event.hasControlDownWithQuirk(); + + // 剪贴板操作(默认分支处理,因 key code 不在 259-269 范围) + if (key < 259 || key > 269) { + if (event.isSelectAll()) { this.setCursorPos(this.value.length()); this.highlightPos = 0; return true; } + if (event.isCopy()) { Minecraft.getInstance().keyboardHandler.setClipboard(this.getHighlighted()); return true; } + if (event.isPaste()) { this.insertText(Minecraft.getInstance().keyboardHandler.getClipboard()); return true; } + if (event.isCut()) { + Minecraft.getInstance().keyboardHandler.setClipboard(this.getHighlighted()); + this.insertText(""); + return true; } + return false; + } + + if (key == 259) { // Backspace + if (this.highlightPos != this.cursorPos) { this.insertText(""); return true; } + if (this.cursorPos > 0) { this.deleteChars(-1); return true; } return true; } - if (key == 261) { - if (this.cursorPos < this.value.length()) { - this.value = new StringBuilder(this.value).deleteCharAt(this.cursorPos).toString(); - this.scrollTo(this.cursorPos); - this.fireChange(); - } + if (key == 261) { // Delete + if (this.highlightPos != this.cursorPos) { this.insertText(""); return true; } + if (this.cursorPos < this.value.length()) { this.deleteChars(1); return true; } return true; } - if (key == 263) { - if (this.cursorPos > 0) { this.cursorPos--; this.scrollTo(this.cursorPos); } + if (key == 262) { // Right + if (ctrl) { this.setCursorPos(this.getWordPosition(1), shift); } + else { this.setCursorPos(this.cursorPos + 1, shift); } return true; } - if (key == 262) { - if (this.cursorPos < this.value.length()) { this.cursorPos++; this.scrollTo(this.cursorPos); } + if (key == 263) { // Left + if (ctrl) { this.setCursorPos(this.getWordPosition(-1), shift); } + else { this.setCursorPos(this.cursorPos - 1, shift); } return true; } - if (key == 268) { this.cursorPos = 0; this.scrollTo(this.cursorPos); return true; } - if (key == 269) { this.cursorPos = this.value.length(); this.scrollTo(this.cursorPos); return true; } + if (key == 268) { this.setCursorPos(0, shift); return true; } // Home + if (key == 269) { this.setCursorPos(this.value.length(), shift); return true; } // End return false; } + private void deleteChars(int dir) { + int start = Math.min(this.cursorPos, this.cursorPos + dir); + int end = Math.max(this.cursorPos, this.cursorPos + dir); + if (start == end) return; + this.value = new StringBuilder(this.value).delete(start, end).toString(); + this.setCursorPos(start); + this.fireChange(); + } + + private int getWordPosition(int dir) { + if (this.value.isEmpty()) return 0; + if (dir < 0) { + int i = this.cursorPos; + while (i > 0 && this.value.charAt(i - 1) == ' ') i--; + while (i > 0 && this.value.charAt(i - 1) != ' ') i--; + return i; + } + int i = this.cursorPos; + while (i < this.value.length() && this.value.charAt(i) != ' ') i++; + while (i < this.value.length() && this.value.charAt(i) == ' ') i++; + return i; + } + @Override public boolean charTyped(CharacterEvent event) { if (!event.isAllowedChatCharacter()) return false; @@ -202,8 +280,16 @@ public boolean charTyped(CharacterEvent event) { } private void insertText(String text) { - this.value = new StringBuilder(this.value).insert(this.cursorPos, text).toString(); - this.cursorPos += text.length(); + if (this.highlightPos != this.cursorPos) { + int start = Math.min(this.cursorPos, this.highlightPos); + int end = Math.max(this.cursorPos, this.highlightPos); + this.value = new StringBuilder(this.value).replace(start, end, text).toString(); + this.cursorPos = start + text.length(); + } else { + this.value = new StringBuilder(this.value).insert(this.cursorPos, text).toString(); + this.cursorPos += text.length(); + } + this.highlightPos = this.cursorPos; this.scrollTo(this.cursorPos); this.fireChange(); } @@ -216,13 +302,12 @@ public LayoutRect hitRect() { return LayoutRect.of(this.x, this.y, this.width, this.height); } - public void setDisplayPos(int pos) { this.displayPos = pos; } - @Override public void copyRuntimeState(UIComponent old) { if (old instanceof TextInputComponent oldTi) { this.setValue(oldTi.value()); this.setCursorPos(oldTi.cursorPos()); + this.highlightPos = oldTi.highlightPos(); this.setFocused(oldTi.focused()); this.setDisplayPos(oldTi.displayPos()); } From cc555f3c0afd553886ddbd1790d321477bc00717 Mon Sep 17 00:00:00 2001 From: Gugle Date: Tue, 19 May 2026 20:36:31 +0800 Subject: [PATCH 63/67] feat(ui): update TODO.md with component status and recent enhancements --- module.ui/TODO.md | 82 ++++++++++++++++++++++++++--------------------- 1 file changed, 46 insertions(+), 36 deletions(-) diff --git a/module.ui/TODO.md b/module.ui/TODO.md index 5c431a27..1781ef1b 100644 --- a/module.ui/TODO.md +++ b/module.ui/TODO.md @@ -26,29 +26,29 @@ ## 基础组件 -| 组件 | 状态 | 用途 | -|-------------------------|-------|----------------------------------------| -| **Text** | ✅ 已实现 | 单行文字,支持颜色/阴影/对齐(LEFT/CENTER/RIGHT) | -| **Button** | ✅ 已实现 | 可点击按钮,原版配色,hover 状态,点击音效 | -| **Image** | ✅ 已实现 | 纹理精灵渲染(`blitSprite`) | -| **Spacer** (≡ Blank) | ✅ 已实现 | 固定尺寸空白占位 | -| **Checkbox** | ✅ 已实现 | 复选框,16×16 深灰外框 + 选中时白色内填充 | -| **Slider** | ✅ 已实现 | 水平滑块,点击/拖拽设值,`Consumer` 回调 | -| **TextInput** | ✅ 已实现 | 单行输入框,placeholder 占位,`charTyped` 多语言输入 | -| **Divider** | ❎ 未实现 | 分割线(水平/垂直) | -| **Toggle** | ❎ 未实现 | 开关切换(不同于 Checkbox 的方块填充风格) | -| **Radio** | ❎ 未实现 | 单选按钮 | -| **Progress** | ❎ 未实现 | 进度条(线性/圆形) | -| **LoadingProgress** | ❎ 未实现 | 加载动画 | -| **TextArea** | ❎ 未实现 | 多行文本输入框 | -| **Search** | ❎ 未实现 | 搜索输入框 | -| **Select** | ❎ 未实现 | 下拉选择器 | -| **Menu** / **MenuItem** | ❎ 未实现 | 右键菜单 / 弹出菜单 | -| **Hyperlink** | ❎ 未实现 | 超链接文字 | -| **Marquee** | ❎ 未实现 | 跑马灯滚动文字 | -| **Rating** | ❎ 未实现 | 星级评分 | -| **Badge** | ❎ 未实现 | 角标/红点提示 | -| **QRCode** | ❎ 未实现 | 二维码显示 | +| 组件 | 状态 | 用途 | +|-------------------------|-------|-----------------------------------------| +| **Text** | ✅ 已实现 | 单行文字,支持颜色/阴影/对齐(LEFT/CENTER/RIGHT) | +| **Button** | ✅ 已实现 | 可点击按钮,原版配色,hover 状态,点击音效 | +| **Image** | ✅ 已实现 | 纹理精灵渲染(`blitSprite`) | +| **Spacer** (≡ Blank) | ✅ 已实现 | 固定尺寸空白占位 | +| **Checkbox** | ✅ 已实现 | 复选框,16×16 深灰外框 + 选中白色内填充,点击音效 | +| **Slider** | ✅ 已实现 | 水平滑块,点击/拖拽设值,hover 高亮,点击音效,拖拽出界继续跟随 | +| **TextInput** | ✅ 已实现 | 单行输入框,placeholder,选择/复制/粘贴/剪切/全选,光标跟随滚动 | +| **Dropdown** (≡ Select) | ✅ 已实现 | 下拉菜单,可拖拽滚动条,hover 高亮,SDF 三角箭头 | +| **Divider** | ❎ 未实现 | 分割线(水平/垂直) | +| **Toggle** | ❎ 未实现 | 开关切换(不同于 Checkbox 的方块填充风格) | +| **Radio** | ❎ 未实现 | 单选按钮 | +| **Progress** | ❎ 未实现 | 进度条(线性/圆形) | +| **LoadingProgress** | ❎ 未实现 | 加载动画 | +| **TextArea** | ❎ 未实现 | 多行文本输入框 | +| **Search** | ❎ 未实现 | 搜索输入框 | +| **Menu** / **MenuItem** | ❎ 未实现 | 右键菜单 / 弹出菜单 | +| **Hyperlink** | ❎ 未实现 | 超链接文字 | +| **Marquee** | ❎ 未实现 | 跑马灯滚动文字 | +| **Rating** | ❎ 未实现 | 星级评分 | +| **Badge** | ❎ 未实现 | 角标/红点提示 | +| **QRCode** | ❎ 未实现 | 二维码显示 | --- @@ -64,13 +64,13 @@ ## 交互 / 手势 -| 组件 | 状态 | 用途 | -|-------------------------|-------|----------------------------------------------| -| **onClick** (≡ Gesture) | ✅ 已实现 | 点击事件,命中测试 + Button/Checkbox/Slider/TextInput | -| **onHover** | ✅ 已实现 | hover 状态更新(`updateHoverRecursive`) | -| **onScroll** | ✅ 已实现 | 滚轮事件 → Scrollable 路由 | -| **onDrag** | ✅ 已实现 | 拖拽事件 → Slider + Scrollable 滚动条 | -| **onKey** | ✅ 已实现 | 键盘事件 → `KeyInputHandler` + `charTyped` | +| 组件 | 状态 | 用途 | +|-------------------------|-------|-------------------------------------------------------| +| **onClick** (≡ Gesture) | ✅ 已实现 | 点击事件,全体系统一递归分发,各组件覆写自身逻辑 | +| **onHover** | ✅ 已实现 | `UIComponent.updateHover()`,Button/Slider/Dropdown 覆写 | +| **onScroll** | ✅ 已实现 | `UIComponent.mouseScrolled()` 递归分发 | +| **onDrag** | ✅ 已实现 | `UIComponent.mouseDragged()` 递归分发 | +| **onKey** | ✅ 已实现 | `UIComponent.keyPressed()`/`charTyped()` 焦点路由 | --- @@ -100,15 +100,25 @@ ## 引擎核心 -| 组件 | 状态 | 用途 | -|-----------------------|-------|----------------------------------------------------------------| -| **Composition** | ✅ 已实现 | Slot table 引擎:emit / recompose / invalidate / copyRuntimeState | -| **DeclarativeScreen** | ✅ 已实现 | Screen 宿主:dirty check → recompose → measure → layout → render | +| 组件 | 状态 | 用途 | +|-----------------------|-------|----------------------------------------------------------------------| +| **Composition** | ✅ 已实现 | Slot table 引擎:emit / recompose / invalidate / copyRuntimeState / ref | +| **DeclarativeScreen** | ✅ 已实现 | Screen 宿主:事件递归分发,焦点管理,hover 更新,无 `instanceof` 硬编码 | --- ## 统计 - 总计参考组件:**52** -- 已实现:**25**(含引擎核心 + 状态管理 + 修饰符) -- 未实现:**27** +- 已实现:**26**(含引擎核心 + 状态管理 + 修饰符 + Select/Dropdown) +- 未实现:**26** + +### 近期大更新 + +- `UIComponent` 事件系统:`mouseClicked`/`mouseDragged`/`mouseScrolled`/`keyPressed`/`charTyped`/`updateHover`/`eventPriority`/ + `renderingPriority`/`sortedChildren` +- `DeclarativeScreen` 不再 `instanceof` 硬编码,统一递归分发事件 +- `SdfGraphics.triangle()` — 三角形 SDF 渲染 +- `Scrollable` / `Dropdown` 弹出层滚动条可鼠标拖拽 +- `TextInput` 支持选择/复制/粘贴/剪切/全选/单词跳转,光标自动跟随滚动 +- `copyRuntimeState` 移至各组件覆写,Composition 统一调用 From fba6d3bd5e785c1686f2606eea115c6c5237f3c0 Mon Sep 17 00:00:00 2001 From: Gugle Date: Tue, 19 May 2026 20:47:44 +0800 Subject: [PATCH 64/67] feat(ui): implement FlexComponent and FlexScope for flexible layout management --- module.ui/TODO.md | 6 +- .../dev/anvilcraft/lib/v2/ui/UIScope.java | 20 +++ .../lib/v2/ui/component/FlexComponent.java | 124 ++++++++++++++++++ .../lib/v2/ui/component/FlexScope.java | 60 +++++++++ 4 files changed, 207 insertions(+), 3 deletions(-) create mode 100644 module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/FlexComponent.java create mode 100644 module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/FlexScope.java diff --git a/module.ui/TODO.md b/module.ui/TODO.md index 1781ef1b..18e6c65e 100644 --- a/module.ui/TODO.md +++ b/module.ui/TODO.md @@ -13,7 +13,7 @@ | **Box** (≡ Stack) | ✅ 已实现 | 层叠布局,子组件按声明顺序从底到顶重叠 | | **Grid** | ✅ 已实现 | 网格布局,指定列数,自动换行 | | **Scrollable** (≡ Scroll) | ✅ 已实现 | 可滚动容器,maxHeight 超限时裁剪 + 滚动条 + 拖拽 | -| **Flex** | ❎ 未实现 | 弹性布局,子组件按权重分配空间 | +| **Flex** | ✅ 已实现 | 弹性布局,子组件按 flexGrow 权重分配主轴剩余空间 | | **List** / **LazyColumn** | ❎ 未实现 | 虚拟化长列表,仅渲染可见区域 | | **Tabs** | ❎ 未实现 | 标签页切换容器 | | **Swiper** | ❎ 未实现 | 轮播/滑动切换容器 | @@ -110,8 +110,8 @@ ## 统计 - 总计参考组件:**52** -- 已实现:**26**(含引擎核心 + 状态管理 + 修饰符 + Select/Dropdown) -- 未实现:**26** +- 已实现:**27**(含引擎核心 + 状态管理 + 修饰符 + Select/Dropdown + Flex) +- 未实现:**25** ### 近期大更新 diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIScope.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIScope.java index d121394c..49957222 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIScope.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIScope.java @@ -5,6 +5,8 @@ import dev.anvilcraft.lib.v2.ui.component.CheckboxComponent; import dev.anvilcraft.lib.v2.ui.component.ColumnComponent; import dev.anvilcraft.lib.v2.ui.component.DropdownComponent; +import dev.anvilcraft.lib.v2.ui.component.FlexComponent; +import dev.anvilcraft.lib.v2.ui.component.FlexScope; import dev.anvilcraft.lib.v2.ui.component.GridComponent; import dev.anvilcraft.lib.v2.ui.component.ImageComponent; import dev.anvilcraft.lib.v2.ui.component.RowComponent; @@ -262,6 +264,24 @@ public BoxComponent Box(Consumer content) { return c; } + /** + * 创建弹性布局容器。子组件按 flexGrow 权重分配剩余空间。 + * 用法:{@code scope.Flex(Direction.ROW, f -> { f.flexGrow(2).Text("Wide"); f.Text("Normal"); });} + * + * @param direction 主轴方向(ROW 或 COLUMN) + * @param content 子组件声明 lambda + */ + public FlexComponent Flex(FlexComponent.Direction direction, Consumer content) { + FlexComponent c = new FlexComponent(Modifier.NONE, direction); + FlexScope inner = new FlexScope(); + content.accept(inner); + c.setChildren(inner.getChildren()); + c.setChildFlexGrows(inner.childWeights()); + this.addChild(c); + Composition.current().emit(c); + return c; + } + /** * 创建网格布局容器。 * diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/FlexComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/FlexComponent.java new file mode 100644 index 00000000..502db44c --- /dev/null +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/FlexComponent.java @@ -0,0 +1,124 @@ +package dev.anvilcraft.lib.v2.ui.component; + +import dev.anvilcraft.lib.v2.ui.Alignment; +import dev.anvilcraft.lib.v2.ui.Constraints; +import dev.anvilcraft.lib.v2.ui.MeasuredSize; +import dev.anvilcraft.lib.v2.ui.Modifier; +import dev.anvilcraft.lib.v2.ui.UIComponent; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; +import net.minecraft.client.gui.GuiGraphicsExtractor; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * 弹性布局。子组件按权重(flexGrow)分配主轴上的剩余空间。 + * 主轴方向通过 {@link Direction} 指定。 + */ +@Accessors(fluent = true) +@SuppressWarnings({"unused", "UnusedReturnValue"}) +public class FlexComponent implements UIComponent { + + public enum Direction { ROW, COLUMN } + + @Getter @Setter + private Modifier modifier; + @Getter + private List children = Collections.emptyList(); + private final Direction direction; + private List childSizes = Collections.emptyList(); + private List childFlexGrows = Collections.emptyList(); + + @Setter + private float spacing; + @Setter + private Alignment.Horizontal crossAlignH = Alignment.Horizontal.Start; + @Setter + private Alignment.Vertical crossAlignV = Alignment.Vertical.Top; + + private float x, y, width, height; + + public FlexComponent(Modifier modifier, Direction direction) { + this.modifier = modifier; + this.direction = direction; + } + + public void setChildren(List children) { this.children = List.copyOf(children); } + public void setChildFlexGrows(List flexGrows) { this.childFlexGrows = List.copyOf(flexGrows); } + + @Override + public MeasuredSize measure(Constraints constraints) { + if (this.children.isEmpty()) return MeasuredSize.ZERO; + + boolean isRow = this.direction == Direction.ROW; + float totalMain = 0; + float maxCross = 0; + List sizes = new ArrayList<>(this.children.size()); + Constraints childC = isRow + ? new Constraints(0, Float.MAX_VALUE, constraints.minHeight(), constraints.maxHeight()) + : new Constraints(constraints.minWidth(), constraints.maxWidth(), 0, Float.MAX_VALUE); + + for (UIComponent child : this.children) { + MeasuredSize s = child.measure(childC); + sizes.add(s); + totalMain += isRow ? s.width() : s.height(); + maxCross = Math.max(maxCross, isRow ? s.height() : s.width()); + } + totalMain += this.spacing * (this.children.size() - 1); + + this.childSizes = sizes; + if (isRow) { + return MeasuredSize.of(constraints.constrainWidth(totalMain), constraints.constrainHeight(maxCross)); + } + return MeasuredSize.of(constraints.constrainWidth(maxCross), constraints.constrainHeight(totalMain)); + } + + @Override + public void layout(float x, float y, float width, float height) { + this.x = x; this.y = y; this.width = width; this.height = height; + if (this.children.isEmpty()) return; + + boolean isRow = this.direction == Direction.ROW; + float totalFixed = 0; + float totalWeight = 0; + for (int i = 0; i < this.children.size(); i++) { + float w = i < this.childFlexGrows.size() ? this.childFlexGrows.get(i) : 1f; + if (w <= 0) { + totalFixed += isRow ? this.childSizes.get(i).width() : this.childSizes.get(i).height(); + } else { + totalWeight += w; + } + } + float available = (isRow ? width : height) - totalFixed - this.spacing * (this.children.size() - 1); + + float currentMain = isRow ? x : y; + for (int i = 0; i < this.children.size(); i++) { + UIComponent child = this.children.get(i); + MeasuredSize size = this.childSizes.get(i); + float w = i < this.childFlexGrows.size() ? this.childFlexGrows.get(i) : 1f; + float childMain; + if (w <= 0 || totalWeight <= 0) { + childMain = isRow ? size.width() : size.height(); + } else { + childMain = (isRow ? size.width() : size.height()) + (w / totalWeight) * Math.max(0, available); + } + if (isRow) { + float childY = y + this.crossAlignV.align(height, size.height()); + child.layout(currentMain, childY, childMain, size.height()); + currentMain += childMain + this.spacing; + } else { + float childX = x + this.crossAlignH.align(width, size.width()); + child.layout(childX, currentMain, size.width(), childMain); + currentMain += childMain + this.spacing; + } + } + } + + @Override + public void extractRenderState(GuiGraphicsExtractor extractor) { + for (UIComponent child : this.sortedChildren()) child.extractRenderState(extractor); + } +} diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/FlexScope.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/FlexScope.java new file mode 100644 index 00000000..02a22e9c --- /dev/null +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/FlexScope.java @@ -0,0 +1,60 @@ +package dev.anvilcraft.lib.v2.ui.component; + +import dev.anvilcraft.lib.v2.ui.Composition; +import dev.anvilcraft.lib.v2.ui.Modifier; +import dev.anvilcraft.lib.v2.ui.Ref; +import dev.anvilcraft.lib.v2.ui.UIScope; + +import java.util.ArrayList; +import java.util.List; + +/** + * {@link FlexComponent} 的子组件作用域。 + * 通过 {@link #flexGrow(float)} 设置下一个子组件的弹性权重,默认 1。 + */ +public class FlexScope extends UIScope { + + private final List pendingWeights = new ArrayList<>(); + private final List childWeights = new ArrayList<>(); + + /** 设置下一个子组件的弹性权重。0 = 固定尺寸。需在子组件声明前调用。 */ + public FlexScope flexGrow(float weight) { + this.pendingWeights.add(weight); + return this; + } + + private float takeWeight() { + float w = !this.pendingWeights.isEmpty() ? this.pendingWeights.removeFirst() : 1f; + this.childWeights.add(w); + return w; + } + + @Override + public TextComponent Text(String text) { this.takeWeight(); return super.Text(text); } + + @Override + public ButtonComponent Button(String label) { this.takeWeight(); return super.Button(label); } + + @Override + public CheckboxComponent Checkbox(String label, boolean checked) { this.takeWeight(); return super.Checkbox(label, checked); } + + @Override + public CheckboxComponent Checkbox(String label, Ref vModel) { this.takeWeight(); return super.Checkbox(label, vModel); } + + @Override + public SliderComponent Slider(float value, float min, float max, float width) { this.takeWeight(); return super.Slider(value, min, max, width); } + + @Override + public SliderComponent Slider(float min, float max, float width, Ref vModel) { this.takeWeight(); return super.Slider(min, max, width, vModel); } + + @Override + public TextInputComponent TextInput(String placeholder) { this.takeWeight(); return super.TextInput(placeholder); } + + @Override + public TextInputComponent TextInput(String placeholder, Ref vModel) { this.takeWeight(); return super.TextInput(placeholder, vModel); } + + @Override + public ImageComponent Image(net.minecraft.resources.Identifier sprite, float width, float height) { this.takeWeight(); return super.Image(sprite, width, height); } + + public List childWeights() { return List.copyOf(this.childWeights); } +} From c83b698f8f16d1ddbab7a2309edc0cc5b08905fe Mon Sep 17 00:00:00 2001 From: Gugle Date: Tue, 19 May 2026 21:17:02 +0800 Subject: [PATCH 65/67] feat(ui): add LazyColumnComponent for virtualized vertical lists with scrolling support --- module.ui/TODO.md | 8 +- .../dev/anvilcraft/lib/v2/ui/UIScope.java | 17 ++ .../v2/ui/component/LazyColumnComponent.java | 194 ++++++++++++++++++ 3 files changed, 215 insertions(+), 4 deletions(-) create mode 100644 module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/LazyColumnComponent.java diff --git a/module.ui/TODO.md b/module.ui/TODO.md index 18e6c65e..ca491da9 100644 --- a/module.ui/TODO.md +++ b/module.ui/TODO.md @@ -13,8 +13,8 @@ | **Box** (≡ Stack) | ✅ 已实现 | 层叠布局,子组件按声明顺序从底到顶重叠 | | **Grid** | ✅ 已实现 | 网格布局,指定列数,自动换行 | | **Scrollable** (≡ Scroll) | ✅ 已实现 | 可滚动容器,maxHeight 超限时裁剪 + 滚动条 + 拖拽 | -| **Flex** | ✅ 已实现 | 弹性布局,子组件按 flexGrow 权重分配主轴剩余空间 | -| **List** / **LazyColumn** | ❎ 未实现 | 虚拟化长列表,仅渲染可见区域 | +| **Flex** | ✅ 已实现 | 弹性布局,子组件按 flexGrow 权重分配主轴剩余空间 | +| **List** / **LazyColumn** | ✅ 已实现 | 虚拟化长列表,固定项高,仅渲染可见项,滚轮+滚动条拖拽 | | **Tabs** | ❎ 未实现 | 标签页切换容器 | | **Swiper** | ❎ 未实现 | 轮播/滑动切换容器 | | **SideBarContainer** | ❎ 未实现 | 侧边栏抽屉容器 | @@ -110,8 +110,8 @@ ## 统计 - 总计参考组件:**52** -- 已实现:**27**(含引擎核心 + 状态管理 + 修饰符 + Select/Dropdown + Flex) -- 未实现:**25** +- 已实现:**28**(含引擎核心 + 状态管理 + 修饰符 + Select/Dropdown + Flex + LazyColumn) +- 未实现:**24** ### 近期大更新 diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIScope.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIScope.java index 49957222..ad4b95e9 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIScope.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIScope.java @@ -9,6 +9,7 @@ import dev.anvilcraft.lib.v2.ui.component.FlexScope; import dev.anvilcraft.lib.v2.ui.component.GridComponent; import dev.anvilcraft.lib.v2.ui.component.ImageComponent; +import dev.anvilcraft.lib.v2.ui.component.LazyColumnComponent; import dev.anvilcraft.lib.v2.ui.component.RowComponent; import dev.anvilcraft.lib.v2.ui.component.ScrollableComponent; import dev.anvilcraft.lib.v2.ui.component.SliderComponent; @@ -314,4 +315,20 @@ public ScrollableComponent Scrollable(float maxHeight, Consumer Composition.current().emit(c); return c; } + + /** + * 创建虚拟化纵向列表。仅渲染可见区域,支持滚轮和滚动条拖拽。 + * + * @param itemHeight 单项高度(像素) + * @param maxHeight 列表最大可见高度 + * @param items 数据源列表 + * @param builder 列表项构建函数 + */ + public LazyColumnComponent LazyColumn(float itemHeight, float maxHeight, + List items, LazyColumnComponent.ItemBuilder builder) { + LazyColumnComponent c = new LazyColumnComponent(Modifier.NONE, itemHeight, maxHeight, items, builder); + this.addChild(c); + Composition.current().emit(c); + return c; + } } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/LazyColumnComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/LazyColumnComponent.java new file mode 100644 index 00000000..73dca0aa --- /dev/null +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/LazyColumnComponent.java @@ -0,0 +1,194 @@ +package dev.anvilcraft.lib.v2.ui.component; + +import dev.anvilcraft.lib.v2.ui.Constraints; +import dev.anvilcraft.lib.v2.ui.LayoutRect; +import dev.anvilcraft.lib.v2.ui.MeasuredSize; +import dev.anvilcraft.lib.v2.ui.Modifier; +import dev.anvilcraft.lib.v2.ui.UIComponent; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.util.Mth; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * 虚拟化纵向列表。仅渲染可见区域的项,支持滚动。 + * 每项固定高度,通过 {@code itemHeight} 指定。 + */ +@Accessors(fluent = true) +@SuppressWarnings({"unused", "UnusedReturnValue"}) +public class LazyColumnComponent implements UIComponent { + + private static final int SCROLLBAR_COLOR = 0xFF888888; + private static final int SCROLLBAR_BG = 0xFF333333; + private static final int SCROLLBAR_W = 4; + + @Getter @Setter + private Modifier modifier; + private final float itemHeight; + private final float maxHeight; + private final List items; + private final ItemBuilder builder; + private List children = Collections.emptyList(); + private float scrollOffset; + + @Getter + private boolean scrollbarDragging; + private float dragAnchorY; + + private float x, y, width, height; + + /** 列表项构建函数接口。 */ + @FunctionalInterface + public interface ItemBuilder { + UIComponent build(T item); + } + + public LazyColumnComponent(Modifier modifier, float itemHeight, float maxHeight, + List items, ItemBuilder builder) { + this.modifier = modifier; + this.itemHeight = itemHeight; + this.maxHeight = maxHeight; + this.items = List.copyOf(items); + this.builder = builder; + } + + @Override + public List children() { return this.children; } + + @Override + public MeasuredSize measure(Constraints constraints) { + float contentH = this.items.size() * this.itemHeight; + float displayH = Math.min(contentH, this.maxHeight); + return MeasuredSize.of(constraints.constrainWidth(200), constraints.constrainHeight(displayH)); + } + + @Override + public void layout(float x, float y, float width, float height) { + this.x = x; this.y = y; this.width = width; this.height = height; + rebuildChildren(); + float maxScroll = Math.max(0, this.items.size() * this.itemHeight - height); + this.scrollOffset = Mth.clamp(this.scrollOffset, 0, maxScroll); + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + private void rebuildChildren() { + int first = (int) (this.scrollOffset / this.itemHeight); + int visible = Math.min((int) (this.height / this.itemHeight) + 1, this.items.size() - first); + List newChildren = new ArrayList<>(visible); + for (int i = 0; i < visible; i++) { + Object item = this.items.get(first + i); + newChildren.add(((ItemBuilder) this.builder).build(item)); + } + this.children = newChildren; + // Position each child + for (int i = 0; i < visible; i++) { + UIComponent child = newChildren.get(i); + float childY = this.y - (this.scrollOffset % this.itemHeight) + i * this.itemHeight; + child.measure(Constraints.NONE); + child.layout(this.x, childY, this.width, this.itemHeight); + } + } + + @Override + public void extractRenderState(GuiGraphicsExtractor extractor) { + int ix = (int) this.x, iy = (int) this.y, iw = (int) this.width, ih = (int) this.height; + extractor.enableScissor(ix, iy, ix + iw, iy + ih); + + for (UIComponent child : this.children) { + child.extractRenderState(extractor); + } + + extractor.disableScissor(); + + // 滚动条 + float contentH = this.items.size() * this.itemHeight; + if (contentH > this.height) { + float bh = Math.max(16, this.height * this.height / contentH); + float maxScroll = contentH - this.height; + float by = this.y + (this.scrollOffset / maxScroll) * (this.height - bh); + int bx = (int) (this.x + this.width - SCROLLBAR_W - 1); + extractor.fill(bx, iy, bx + SCROLLBAR_W, iy + ih, SCROLLBAR_BG); + extractor.fill(bx, (int) by, bx + SCROLLBAR_W, (int) (by + bh), SCROLLBAR_COLOR); + } + } + + // ── 滚动 ── + + @Override + public boolean mouseScrolled(double mouseX, double mouseY, double scrollX, double scrollY) { + if (!this.hitRect().contains((float) mouseX, (float) mouseY)) return false; + this.scrollOffset = Mth.clamp( + this.scrollOffset + (float) (-scrollY * 20), + 0, Math.max(0, this.items.size() * this.itemHeight - this.height) + ); + return true; + } + + public boolean isOnScrollbar(float mx, float my) { + float contentH = this.items.size() * this.itemHeight; + if (contentH <= this.height) return false; + float bh = Math.max(16, this.height * this.height / contentH); + float maxScroll = contentH - this.height; + float by = this.y + (this.scrollOffset / maxScroll) * (this.height - bh); + int bx = (int) (this.x + this.width - SCROLLBAR_W - 1); + return mx >= bx && mx < bx + SCROLLBAR_W && my >= by && my < by + bh; + } + + public void startScrollbarDrag(float my) { + this.scrollbarDragging = true; + float contentH = this.items.size() * this.itemHeight; + float bh = Math.max(16, this.height * this.height / contentH); + float maxScroll = contentH - this.height; + float by = this.y + (this.scrollOffset / maxScroll) * (this.height - bh); + this.dragAnchorY = my - by; + } + + public void onScrollbarDrag(float my) { + if (!this.scrollbarDragging) return; + float contentH = this.items.size() * this.itemHeight; + float bh = Math.max(16, this.height * this.height / contentH); + float maxScroll = contentH - this.height; + float newBarY = my - this.dragAnchorY; + float ratio = Mth.clamp((newBarY - this.y) / (this.height - bh), 0f, 1f); + this.scrollOffset = ratio * maxScroll; + } + + public void stopScrollbarDrag() { this.scrollbarDragging = false; } + + public LayoutRect hitRect() { + return LayoutRect.of(this.x, this.y, this.width, this.height); + } + + @Override + public boolean mouseClicked(net.minecraft.client.input.MouseButtonEvent event, boolean isDouble) { + if (event.button() != 0) return false; + var mc = net.minecraft.client.Minecraft.getInstance(); + int mx = (int) mc.mouseHandler.getScaledXPos(mc.getWindow()); + int my = (int) mc.mouseHandler.getScaledYPos(mc.getWindow()); + if (this.isOnScrollbar(mx, my)) { this.startScrollbarDrag(my); return true; } + for (UIComponent child : this.children) { + if (child.mouseClicked(event, isDouble)) return true; + } + return false; + } + + @Override + public boolean mouseDragged(net.minecraft.client.input.MouseButtonEvent event, double dx, double dy) { + if (!this.scrollbarDragging) return false; + var mc = net.minecraft.client.Minecraft.getInstance(); + int my = (int) mc.mouseHandler.getScaledYPos(mc.getWindow()); + this.onScrollbarDrag(my); + return true; + } + + @Override + public boolean mouseReleased(net.minecraft.client.input.MouseButtonEvent event) { + this.stopScrollbarDrag(); + return false; + } +} From d4914277b830a778803eca107a820e13bca13749 Mon Sep 17 00:00:00 2001 From: Gugle Date: Tue, 19 May 2026 22:43:38 +0800 Subject: [PATCH 66/67] feat(ui): optimize component initialization and enhance resizing behavior --- .../anvilcraft/lib/v2/ui/DeclarativeScreen.java | 14 ++++++++++++-- .../lib/v2/ui/component/ScrollableComponent.java | 4 ++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java index 576a500a..5e58fe66 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/DeclarativeScreen.java @@ -60,8 +60,18 @@ public boolean keyPressed(KeyEvent event) { @Override protected void init() { - this.composition = new Composition(this.rootScope); - this.composition.setContent(this::content); + if (this.composition == null) { + this.composition = new Composition(this.rootScope); + this.composition.setContent(this::content); + } + } + + /** + * 窗口 resize 时不重建组件树,只标记 dirty 用新尺寸 recompose。 + */ + @Override + protected void repositionElements() { + if (this.composition != null) this.composition.invalidate(); } private void refreshFocus() { diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ScrollableComponent.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ScrollableComponent.java index c7958ffb..eaf3b660 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ScrollableComponent.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/component/ScrollableComponent.java @@ -88,6 +88,10 @@ public void layout(float x, float y, float width, float height) { this.width = width; this.height = height; + // resize 后重新钳制滚动位置 + float maxScroll = Math.max(0, this.contentHeight - height); + this.scrollY = Mth.clamp(this.scrollY, -maxScroll, 0); + float currentY = y + this.scrollY; float childW = width - ScrollableComponent.SCROLLBAR_W - 1; for (int i = 0; i < this.children.size(); i++) { From 7e60dcab9b415c50a3fef4bd4db17f09fbb9c633 Mon Sep 17 00:00:00 2001 From: Gugle Date: Tue, 19 May 2026 22:48:52 +0800 Subject: [PATCH 67/67] feat(ui): refactor Ref class methods for improved state management and update UI components to use new method signatures --- .../client/screen/DeclarativeTestScreen.java | 14 ++++++------- .../java/dev/anvilcraft/lib/v2/ui/Ref.java | 21 ++++++++++++++++--- .../dev/anvilcraft/lib/v2/ui/UIScope.java | 10 ++++----- 3 files changed, 30 insertions(+), 15 deletions(-) diff --git a/module.test/src/main/java/dev/anvilcraft/lib/v2/test/client/screen/DeclarativeTestScreen.java b/module.test/src/main/java/dev/anvilcraft/lib/v2/test/client/screen/DeclarativeTestScreen.java index c671fef5..0052d42c 100644 --- a/module.test/src/main/java/dev/anvilcraft/lib/v2/test/client/screen/DeclarativeTestScreen.java +++ b/module.test/src/main/java/dev/anvilcraft/lib/v2/test/client/screen/DeclarativeTestScreen.java @@ -49,10 +49,10 @@ protected void content(UIScope scope) { // ── 2. Button + 状态 ── col.Text("2) Button + state (Counter):"); col.Row(row -> { - row.Button("-").onClick(() -> counter.setValue(counter.getValue() - 1)); - row.Text(" " + counter.getValue() + " ").align(TextComponent.Align.CENTER); - row.Button("+").onClick(() -> counter.setValue(counter.getValue() + 1)); - row.Button("Reset").onClick(() -> counter.setValue(0)); + row.Button("-").onClick(() -> counter.accept(counter.get() - 1)); + row.Text(" " + counter.get() + " ").align(TextComponent.Align.CENTER); + row.Button("+").onClick(() -> counter.accept(counter.get() + 1)); + row.Button("Reset").onClick(() -> counter.accept(0)); }).spacing(4); col.Spacer(0, 4); @@ -82,7 +82,7 @@ protected void content(UIScope scope) { col.Text("5) Checkbox:"); col.Row(row -> { row.Checkbox("Enable feature", checked); - row.Text(" Enabled: " + checked.getValue()); + row.Text(" Enabled: " + checked.get()); }).spacing(4); col.Spacer(0, 4); @@ -92,7 +92,7 @@ protected void content(UIScope scope) { col.Text("6) Slider:"); col.Row(row -> { row.Slider(0, 100, 100, sliderVal); - row.Text(" " + sliderVal.getValue().intValue() + "%"); + row.Text(" " + sliderVal.get().intValue() + "%"); }).spacing(4); col.Spacer(0, 4); @@ -101,7 +101,7 @@ protected void content(UIScope scope) { col.Text("7) TextInput:"); col.Row(row -> { row.TextInput("Enter text...", text); - row.Text(" Value: '" + text.getValue() + "'"); + row.Text(" Value: '" + text.get() + "'"); }).spacing(4); col.Spacer(0, 4); diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Ref.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Ref.java index a1391ffd..53223878 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Ref.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/Ref.java @@ -2,9 +2,13 @@ import org.jspecify.annotations.Nullable; +import java.util.HashMap; import java.util.HashSet; +import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Supplier; /** * 可观察状态持有者。 @@ -20,8 +24,10 @@ "UnusedReturnValue" } ) -public class Ref { +public class Ref implements Supplier, Consumer { final Set readers = new HashSet<>(); + final Map>> observers = new HashMap<>(); + private long used = 0; private T value; public Ref(T initialValue) { @@ -31,7 +37,7 @@ public Ref(T initialValue) { /** * 读取当前值。若在 composition emission 期间调用,记录此 slot 为 reader。 */ - public T getValue() { + public T get() { Composition comp = Composition.currentOrNull(); if (comp != null && comp.currentSlot != null) { comp.currentSlot.addReadState(this); @@ -43,7 +49,7 @@ public T getValue() { /** * 设置新值。若值发生变化,标记所有 reader slot 为脏。 */ - public void setValue(T newValue) { + public void accept(T newValue) { if (!Objects.equals(this.value, newValue)) { this.value = newValue; for (Composition.Slot slot : this.readers) { @@ -56,4 +62,13 @@ public void setValue(T newValue) { public String toString() { return "State(" + this.value + ")"; } + + public Long watch(Consumer> watcher) { + this.observers.put(this.used, watcher); + return this.used++; + } + + public Consumer> unwatch(Long id) { + return this.observers.remove(id); + } } diff --git a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIScope.java b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIScope.java index ad4b95e9..42d79cb5 100644 --- a/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIScope.java +++ b/module.ui/src/main/java/dev/anvilcraft/lib/v2/ui/UIScope.java @@ -128,8 +128,8 @@ public CheckboxComponent Checkbox(String label, boolean checked) { * @param vModel {@code Ref},点击时自动同步值,无需手动 onToggle */ public CheckboxComponent Checkbox(String label, Ref vModel) { - CheckboxComponent c = new CheckboxComponent(Modifier.NONE, label, vModel.getValue() != null && vModel.getValue()); - c.onToggle(() -> vModel.setValue(!vModel.getValue())); + CheckboxComponent c = new CheckboxComponent(Modifier.NONE, label, vModel.get() != null && vModel.get()); + c.onToggle(() -> vModel.accept(!vModel.get())); this.addChild(c); Composition.current().emit(c); return c; @@ -173,8 +173,8 @@ public SliderComponent Slider(float value, float min, float max, float width) { * @param vModel {@code Ref},拖拽时自动同步值 */ public SliderComponent Slider(float min, float max, float width, Ref vModel) { - SliderComponent c = new SliderComponent(Modifier.NONE, vModel.getValue() == null ? 0 : vModel.getValue(), min, max, width); - c.onChange(vModel::setValue); + SliderComponent c = new SliderComponent(Modifier.NONE, vModel.get() == null ? 0 : vModel.get(), min, max, width); + c.onChange(vModel::accept); this.addChild(c); Composition.current().emit(c); return c; @@ -200,7 +200,7 @@ public TextInputComponent TextInput(String placeholder) { */ public TextInputComponent TextInput(String placeholder, Ref vModel) { TextInputComponent c = new TextInputComponent(Modifier.NONE, placeholder); - c.onChange(vModel::setValue); + c.onChange(vModel::accept); this.addChild(c); Composition.current().emit(c); return c;