Compare commits
54 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b639bc6c4f | |||
| 7ee04c78cd | |||
| 7b58addf7e | |||
| ae52be591b | |||
| 93ab7690ba | |||
| 68c3f88181 | |||
| 830864aa8a | |||
| 5f34cb34b2 | |||
| b9d2fe60fd | |||
| 06bc4fcd55 | |||
| 42cb84b610 | |||
| f8607d114c | |||
| 56230dd340 | |||
| df9fdf26c8 | |||
| 8f8fa8f8d1 | |||
| 07f98c119a | |||
| d49194fc4d | |||
| e61914bad4 | |||
| 015d90519f | |||
| 7b4f0abfb8 | |||
| 0667a44e14 | |||
| 0852b8463b | |||
| 1436fda858 | |||
| 0826908068 | |||
| 7ceb9e571d | |||
| 38c2f6b6f9 | |||
| b9f06fef7a | |||
| 373f1c1010 | |||
| e80d693d85 | |||
| ef60ddf6a8 | |||
| de0ff2e263 | |||
| 13a435e9c1 | |||
| 725338cd5a | |||
| acd14f46a8 | |||
| ef78f023ed | |||
| 85b6109792 | |||
| 6c5738c863 | |||
| 1174e471f6 | |||
| a618ff46fe | |||
| 1fa5cd316e | |||
| 11e5d37563 | |||
| 9f86c20129 | |||
| 8fb1c9fb42 | |||
| 5f08337726 | |||
| 0bc80d5df3 | |||
| 2432443ea6 | |||
| fa138971e7 | |||
| d7982599d1 | |||
| f16c80aa0f | |||
| ef5adf8274 | |||
| 2ffc65b1b1 | |||
| d4a17bd98d | |||
| dcf5f434c5 | |||
| 78cd973a33 |
@@ -33,3 +33,74 @@ jobs:
|
|||||||
libssl-dev
|
libssl-dev
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: ./scripts/test-linux.sh
|
run: ./scripts/test-linux.sh
|
||||||
|
|
||||||
|
# Emulator smoke tests: boot the built disc/EBOOT for a fixed window and
|
||||||
|
# confirm the emulator doesn't crash. Not yet verified against a real
|
||||||
|
# runner (no CI run has exercised these) -- continue-on-error so a
|
||||||
|
# flaky/broken emulator step doesn't block the required Linux test job.
|
||||||
|
run-tests-gamecube-dolphin:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
continue-on-error: true
|
||||||
|
container:
|
||||||
|
image: ghcr.io/extremscorner/libogc2:latest
|
||||||
|
steps:
|
||||||
|
- name: Install Node.js
|
||||||
|
run: apt-get update && apt-get install -y nodejs
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: Install additional dependencies
|
||||||
|
run: |
|
||||||
|
apt-get install -y \
|
||||||
|
python3-pip python3-polib python3-pil \
|
||||||
|
python3-dotenv python3-pyqt5 python3-opengl xorriso \
|
||||||
|
dolphin-emu xvfb
|
||||||
|
dkp-pacman -Syu --noconfirm
|
||||||
|
dkp-pacman -S --needed --noconfirm \
|
||||||
|
gamecube-sdl2 ppc-liblzma ppc-libzip \
|
||||||
|
gamecube-tools ppc-libmad ppc-zlib-ng ppc-bzip2 ppc-zstd
|
||||||
|
- name: Build GameCube ISO and boot it in Dolphin
|
||||||
|
run: ./scripts/test-gamecube-dolphin.sh
|
||||||
|
|
||||||
|
run-tests-wii-dolphin:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
continue-on-error: true
|
||||||
|
container:
|
||||||
|
image: ghcr.io/extremscorner/libogc2:latest
|
||||||
|
steps:
|
||||||
|
- name: Install Node.js
|
||||||
|
run: apt-get update && apt-get install -y nodejs
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: Install additional dependencies
|
||||||
|
run: |
|
||||||
|
apt-get install -y \
|
||||||
|
python3-pip python3-polib python3-pil \
|
||||||
|
python3-dotenv python3-pyqt5 python3-opengl xorriso \
|
||||||
|
dolphin-emu xvfb
|
||||||
|
dkp-pacman -Syu --noconfirm
|
||||||
|
dkp-pacman -S --needed --noconfirm \
|
||||||
|
gamecube-sdl2 ppc-liblzma ppc-libzip \
|
||||||
|
gamecube-tools ppc-libmad ppc-zlib-ng ppc-bzip2 ppc-zstd
|
||||||
|
- name: Build Wii ISO and boot it in Dolphin
|
||||||
|
run: ./scripts/test-wii-dolphin.sh
|
||||||
|
|
||||||
|
run-tests-psp-ppsspp:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
continue-on-error: true
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: Setup pspdev
|
||||||
|
uses: ./.github/actions/setup-pspdev
|
||||||
|
- name: Install PPSSPPHeadless build dependencies
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y git cmake ninja-build libsdl2-dev zlib1g-dev
|
||||||
|
- name: Build PPSSPPHeadless
|
||||||
|
run: |
|
||||||
|
git clone --recursive --depth 1 https://github.com/hrydgard/ppsspp.git /tmp/ppsspp
|
||||||
|
cmake -S /tmp/ppsspp -B /tmp/ppsspp/build -DCMAKE_BUILD_TYPE=Release
|
||||||
|
cmake --build /tmp/ppsspp/build --target PPSSPPHeadless -- -j$(nproc)
|
||||||
|
echo "PPSSPP_HEADLESS_BIN=/tmp/ppsspp/build/PPSSPPHeadless" >> "$GITHUB_ENV"
|
||||||
|
- name: Build PSP EBOOT and boot it in PPSSPPHeadless
|
||||||
|
run: ./scripts/test-psp-ppsspp.sh
|
||||||
|
|||||||
@@ -1,455 +0,0 @@
|
|||||||
# Dusk — Claude Code rules
|
|
||||||
|
|
||||||
## File headers
|
|
||||||
Every C, H, and JS file starts with:
|
|
||||||
|
|
||||||
```c
|
|
||||||
/**
|
|
||||||
* Copyright (c) 2026 Dominic Masters
|
|
||||||
*
|
|
||||||
* This software is released under the MIT License.
|
|
||||||
* https://opensource.org/licenses/MIT
|
|
||||||
*/
|
|
||||||
```
|
|
||||||
|
|
||||||
JS files use `//` comment style instead.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## C conventions
|
|
||||||
|
|
||||||
### Types
|
|
||||||
Always use the project-defined aliases instead of bare C primitives:
|
|
||||||
|
|
||||||
| Use | Not |
|
|
||||||
|-----------|--------------|
|
|
||||||
| `bool_t` | `bool` |
|
|
||||||
| `int_t` | `int` |
|
|
||||||
| `float_t` | `float` |
|
|
||||||
| `char_t` | `char` |
|
|
||||||
|
|
||||||
Use `uint8_t`, `uint16_t`, `int32_t`, etc. for fixed-width integers.
|
|
||||||
All struct and enum types end in `_t` (`animation_t`, `errorret_t`, …).
|
|
||||||
|
|
||||||
### Naming
|
|
||||||
- **Functions** — snake_case, prefixed with their module:
|
|
||||||
`assetLock()`, `entityPositionInit()`, `moduleAssetBatchCtor()`
|
|
||||||
- **Struct fields** — camelCase: `keyframeCount`, `localPosition`
|
|
||||||
- **Macros / constants** — UPPER_SNAKE_CASE:
|
|
||||||
`ENTITY_ID_INVALID`, `ERROR_OK`, `COMPONENT_TYPE_COUNT`
|
|
||||||
- **Files** — snake_case matching the primary type: `entityposition.c`,
|
|
||||||
`moduleassetbatch.c`
|
|
||||||
|
|
||||||
### Header files (`.h`)
|
|
||||||
- Use `#pragma once` — no include guards.
|
|
||||||
- Declare every public function, `#define`, and `extern` global.
|
|
||||||
- Write a JSDoc block (`/** … */`) above every declaration explaining
|
|
||||||
purpose, `@param`s, and `@returns`.
|
|
||||||
- Only include headers that the `.h` file itself strictly requires for
|
|
||||||
the types it exposes. Move everything else to the `.c` file.
|
|
||||||
Do not use forward declarations as a workaround — use the real
|
|
||||||
include in the `.c` file instead.
|
|
||||||
|
|
||||||
### Implementation files (`.c`)
|
|
||||||
- Contain function bodies only; no declarations.
|
|
||||||
- Pull in whatever additional includes the implementation needs.
|
|
||||||
- Do not use `static` or `inline` on **functions**. Every function,
|
|
||||||
including internal helpers, must be declared in the matching `.h` and
|
|
||||||
defined in the `.c` file. Internal helpers belong near the bottom of
|
|
||||||
the `.c` file, not at the top with a `static` qualifier.
|
|
||||||
`static` and `inline` on functions are only appropriate when the
|
|
||||||
function body is written directly inside a `.h` file.
|
|
||||||
`static` on **variables** (file-scope state) is fine and expected.
|
|
||||||
|
|
||||||
### Formatting
|
|
||||||
- Hard-wrap all lines at **80 characters**.
|
|
||||||
|
|
||||||
### Error handling
|
|
||||||
Return `errorret_t` from fallible functions. Use these macros:
|
|
||||||
|
|
||||||
```c
|
|
||||||
errorOk(); // return success
|
|
||||||
errorThrow("msg %d", val); // return failure with message
|
|
||||||
errorChain(someCall()); // propagate failure, continue on success
|
|
||||||
errorIsOk(ret) / errorIsNotOk(ret) // test a result
|
|
||||||
errorCatch(ret); // handle + free an error
|
|
||||||
```
|
|
||||||
|
|
||||||
Never return raw error codes or use `errno` for in-engine errors.
|
|
||||||
|
|
||||||
### Memory
|
|
||||||
Use the project allocator — never raw `malloc`/`free`:
|
|
||||||
|
|
||||||
```c
|
|
||||||
memoryAllocate(size) // allocate
|
|
||||||
memoryFree(ptr) // free
|
|
||||||
memoryZero(dest, size) // zero a block
|
|
||||||
memoryCopy(dest, src, size) // copy
|
|
||||||
```
|
|
||||||
|
|
||||||
### Asserts
|
|
||||||
Prefer specific assert macros over bare `assert()`:
|
|
||||||
|
|
||||||
```c
|
|
||||||
assertNotNull(ptr, "msg");
|
|
||||||
assertTrue(cond, "msg");
|
|
||||||
assertFalse(cond, "msg");
|
|
||||||
assertUnreachable("msg");
|
|
||||||
assertIsMainThread("msg");
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Build system
|
|
||||||
Each subdirectory has its own `CMakeLists.txt` that adds sources with:
|
|
||||||
|
|
||||||
```cmake
|
|
||||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
|
||||||
PUBLIC
|
|
||||||
myfile.c
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
Never add source files to the root `CMakeLists.txt` directly.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Platform support
|
|
||||||
|
|
||||||
### Targets
|
|
||||||
Set `DUSK_TARGET_SYSTEM` at CMake configure time to select a platform:
|
|
||||||
|
|
||||||
| `DUSK_TARGET_SYSTEM` | Macro defined | Platform |
|
|
||||||
|----------------------|-------------------|------------------|
|
|
||||||
| `linux` | `DUSK_LINUX` | Linux desktop |
|
|
||||||
| `knulli` | `DUSK_KNULLI` | Knulli (handheld)|
|
|
||||||
| `psp` | `DUSK_PSP` | Sony PSP |
|
|
||||||
| `vita` | `DUSK_VITA` | PlayStation Vita |
|
|
||||||
| `gamecube` | `DUSK_GAMECUBE` | Nintendo GameCube|
|
|
||||||
| `wii` | `DUSK_WII` | Nintendo Wii |
|
|
||||||
|
|
||||||
### Layer structure
|
|
||||||
```
|
|
||||||
src/dusk/ core, platform-agnostic game logic
|
|
||||||
src/duskgl/ OpenGL abstraction (Linux, Knulli, PSP, Vita)
|
|
||||||
src/dusksdl2/ SDL2 window + input (Linux, Knulli, PSP, Vita)
|
|
||||||
src/dusklinux/ Linux + Knulli platform impl
|
|
||||||
src/duskpsp/ PSP platform impl
|
|
||||||
src/duskvita/ Vita platform impl
|
|
||||||
src/duskdolphin/ GameCube / Wii platform impl (no SDL2/OpenGL)
|
|
||||||
```
|
|
||||||
|
|
||||||
Dolphin is the only target that bypasses SDL2 and OpenGL entirely —
|
|
||||||
it uses native GameCube/Wii rendering and input APIs.
|
|
||||||
|
|
||||||
### Platform guards
|
|
||||||
Use the compile-time macros for platform-specific code:
|
|
||||||
|
|
||||||
```c
|
|
||||||
#ifdef DUSK_PSP
|
|
||||||
// PSP-only path
|
|
||||||
#elif defined(DUSK_GAMECUBE) || defined(DUSK_WII)
|
|
||||||
// GameCube / Wii path
|
|
||||||
#else
|
|
||||||
// Generic / Linux fallback
|
|
||||||
#endif
|
|
||||||
```
|
|
||||||
|
|
||||||
Additional capability macros set per-target:
|
|
||||||
`DUSK_SDL2`, `DUSK_OPENGL`, `DUSK_OPENGL_ES`, `DUSK_OPENGL_LEGACY`,
|
|
||||||
`DUSK_INPUT_GAMEPAD`, `DUSK_INPUT_KEYBOARD`, `DUSK_INPUT_POINTER`,
|
|
||||||
`DUSK_PLATFORM_ENDIAN_BIG` / `DUSK_PLATFORM_ENDIAN_LITTLE`.
|
|
||||||
|
|
||||||
### Abstraction pattern
|
|
||||||
Platform-specific implementations are wired in via `#define` macros in
|
|
||||||
each platform's `displayplatform.h` / `inputplatform.h` etc., which
|
|
||||||
the core calls through. Functions that a platform does not support are
|
|
||||||
simply left undefined — the core guards calls with `#ifdef`.
|
|
||||||
|
|
||||||
### Adding platform-specific code
|
|
||||||
- Put it under `src/dusk<platform>/` in the matching subsystem folder.
|
|
||||||
- Gate any core call-site with the appropriate `#ifdef DUSK_<PLATFORM>`
|
|
||||||
or capability macro.
|
|
||||||
- Keep the `src/dusk/` core free of platform ifdefs — delegate through
|
|
||||||
the platform header macros instead.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Adding a new asset loader type
|
|
||||||
1. Add an enum value to `assetloadertype_t` (before `_COUNT`) in
|
|
||||||
`src/dusk/asset/loader/assetloader.h`.
|
|
||||||
2. Add fields to the input/loading/output unions in `assetloader.h`.
|
|
||||||
3. Implement `assetXxxLoaderSync`, `assetXxxLoaderAsync`, and
|
|
||||||
`assetXxxDispose` in a new `src/dusk/asset/loader/xxx/` directory.
|
|
||||||
4. Register the three callbacks in `ASSET_LOADER_CALLBACKS[]` in
|
|
||||||
`src/dusk/asset/loader/assetloader.c`.
|
|
||||||
5. If user-facing, create a JS module (see below) and a `.d.ts` file.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Adding a new entity component
|
|
||||||
1. Create `src/dusk/entity/component/<category>/entityMyComp.h/.c` with
|
|
||||||
struct `entityMyComp_t`, `entityMyCompInit()`, and optionally
|
|
||||||
`entityMyCompDispose()`.
|
|
||||||
2. Add the include to `src/dusk/entity/componentlist.h` header block.
|
|
||||||
3. Add a row to `src/dusk/entity/componentlist.h`:
|
|
||||||
```c
|
|
||||||
X(MYCOMP, entityMyComp_t, myComp, entityMyCompInit, NULL, NULL)
|
|
||||||
```
|
|
||||||
This auto-generates the enum, union field, and definition entry.
|
|
||||||
4. If JS-facing, create the script module and `.d.ts` (see below).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Adding a new script (JS) module
|
|
||||||
1. Create `src/dusk/script/module/<category>/moduleMyMod.h/.c`.
|
|
||||||
- Declare `extern scriptproto_t MODULE_MYMOD_PROTO;` in the header.
|
|
||||||
- Use `moduleBaseFunction(name)` to define JS-callable functions.
|
|
||||||
- Register props/funcs in `moduleMyModInit()` with
|
|
||||||
`scriptProtoDefineProp` / `scriptProtoDefineFunc` /
|
|
||||||
`scriptProtoDefineStaticFunc`.
|
|
||||||
2. `#include` the header in
|
|
||||||
`src/dusk/script/module/modulelist.c` and call
|
|
||||||
`moduleMyModInit()` in `moduleListInit()` (and `Dispose` in
|
|
||||||
`moduleListDispose()`).
|
|
||||||
3. For component modules also register in
|
|
||||||
`src/dusk/script/module/entity/component/modulecomponentlist.c`
|
|
||||||
so `entity.add()` returns the typed wrapper.
|
|
||||||
4. Create `types/<category>/mymod.d.ts` and add a
|
|
||||||
`/// <reference path="..." />` line to `types/index.d.ts`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Script module type declarations
|
|
||||||
Whenever a `src/dusk/script/module/**/*.c` file is created or modified,
|
|
||||||
check whether the corresponding `types/**/*.d.ts` needs updating and
|
|
||||||
apply any changes before finishing the task.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## JavaScript (asset scripts)
|
|
||||||
- Use `var` for module-level state; `const` for values that never
|
|
||||||
change.
|
|
||||||
- Always use semicolons.
|
|
||||||
- Scene objects are plain objects (`var scene = {}`) with assigned
|
|
||||||
methods.
|
|
||||||
- Export via `module.exports = scene`.
|
|
||||||
- Async scene init should use `async function` and `await`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Coding style
|
|
||||||
|
|
||||||
### ASCII only
|
|
||||||
Source files (`.c`, `.h`, `.js`) must contain only ASCII characters (U+0000–U+007F).
|
|
||||||
Non-ASCII characters are banned even in comments and string literals.
|
|
||||||
Use ASCII-only substitutes instead:
|
|
||||||
- `--` or `-` instead of `—` (em dash)
|
|
||||||
- `->` instead of `→` (arrow)
|
|
||||||
- `x` or `*` instead of `×` (multiplication)
|
|
||||||
|
|
||||||
Only non-script asset files (e.g. `.po` locale files) may contain non-ASCII text.
|
|
||||||
|
|
||||||
### Indentation
|
|
||||||
2 spaces. No tabs.
|
|
||||||
|
|
||||||
### Keyword and operator spacing
|
|
||||||
No space between a keyword or function name and its opening parenthesis:
|
|
||||||
|
|
||||||
```c
|
|
||||||
if(!ptr) return;
|
|
||||||
for(uint8_t i = 0; i < count; i++) {
|
|
||||||
while(entry->state != DONE) {
|
|
||||||
switch(type) {
|
|
||||||
sizeof(assetbatch_t)
|
|
||||||
memoryZero(ptr, size)
|
|
||||||
```
|
|
||||||
|
|
||||||
Spaces around all binary operators and after every comma:
|
|
||||||
|
|
||||||
```c
|
|
||||||
pos->flags |= ENTITY_POSITION_FLAG_WORLD_DIRTY;
|
|
||||||
(size_t)end - (size_t)start
|
|
||||||
foo(a, b, c)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Braces
|
|
||||||
Opening brace on the **same line** as the statement (K&R style) for all
|
|
||||||
constructs — functions, `if`, `else`, `for`, `while`, `switch`:
|
|
||||||
|
|
||||||
```c
|
|
||||||
void assetEntryLock(assetentry_t *entry) {
|
|
||||||
...
|
|
||||||
}
|
|
||||||
|
|
||||||
if(dirty) {
|
|
||||||
...
|
|
||||||
} else {
|
|
||||||
...
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Guard returns
|
|
||||||
Short guards go on one line with no braces:
|
|
||||||
|
|
||||||
```c
|
|
||||||
if(!ptr) return;
|
|
||||||
if(!b || !b->batch) return jerry_undefined();
|
|
||||||
if(!(flags & DIRTY)) return;
|
|
||||||
```
|
|
||||||
|
|
||||||
### Blank lines
|
|
||||||
- One blank line between functions; no blank line at the start or end of
|
|
||||||
a function body.
|
|
||||||
- One blank line between logical blocks inside a function body.
|
|
||||||
- No trailing blank lines at the end of a file.
|
|
||||||
|
|
||||||
### Pointer placement
|
|
||||||
`*` is attached to the variable name, not the type:
|
|
||||||
|
|
||||||
```c
|
|
||||||
assetentry_t *entry
|
|
||||||
const char_t *name
|
|
||||||
void *ptr
|
|
||||||
uint8_t *d = (uint8_t *)dest;
|
|
||||||
```
|
|
||||||
|
|
||||||
### Casts
|
|
||||||
Space between cast and operand:
|
|
||||||
|
|
||||||
```c
|
|
||||||
(assetbatch_t *)user
|
|
||||||
(uint8_t *)dest
|
|
||||||
(textureformat_t)v
|
|
||||||
```
|
|
||||||
|
|
||||||
### Return
|
|
||||||
No parentheses around the return value:
|
|
||||||
|
|
||||||
```c
|
|
||||||
return ptr;
|
|
||||||
return MEMORY_POINTERS_IN_USE;
|
|
||||||
```
|
|
||||||
|
|
||||||
### switch / case
|
|
||||||
`case` indented 2 spaces from `switch`; body indented 2 more from `case`:
|
|
||||||
|
|
||||||
```c
|
|
||||||
switch(type) {
|
|
||||||
case ASSET_LOADER_TYPE_TEXTURE:
|
|
||||||
descs[i].input.texture = (textureformat_t)v;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Multi-line function signatures
|
|
||||||
When parameters don't fit on one line, put each on its own line indented
|
|
||||||
2 spaces; the closing `) {` (definition) or `);` (declaration) goes on
|
|
||||||
its own line at column 0:
|
|
||||||
|
|
||||||
```c
|
|
||||||
void assetEntryInit(
|
|
||||||
assetentry_t *entry,
|
|
||||||
const char_t *name,
|
|
||||||
const assetloadertype_t type,
|
|
||||||
assetloaderinput_t *input
|
|
||||||
) {
|
|
||||||
|
|
||||||
errorret_t memoryCompare(
|
|
||||||
const void *a,
|
|
||||||
const void *b,
|
|
||||||
const size_t size
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
### Structs and enums
|
|
||||||
Anonymous inner struct or enum with a `typedef`, `_t` suffix, closing
|
|
||||||
brace and name on the same line:
|
|
||||||
|
|
||||||
```c
|
|
||||||
typedef struct {
|
|
||||||
errorcode_t code;
|
|
||||||
char_t *message;
|
|
||||||
} errorstate_t;
|
|
||||||
|
|
||||||
typedef enum {
|
|
||||||
ASSET_LOADER_TYPE_NULL,
|
|
||||||
ASSET_LOADER_TYPE_COUNT
|
|
||||||
} assetloadertype_t;
|
|
||||||
```
|
|
||||||
|
|
||||||
### Designated initialisers
|
|
||||||
Spaces inside braces; `.field = value`:
|
|
||||||
|
|
||||||
```c
|
|
||||||
jsassetentry_t e = { .entry = entry };
|
|
||||||
assetbatchloadedpend_t init = { .batch = batch };
|
|
||||||
```
|
|
||||||
|
|
||||||
### Ternary operator
|
|
||||||
Spaces around `?` and `:`:
|
|
||||||
|
|
||||||
```c
|
|
||||||
const float val = psx > 0.0f ? pt[0][0] / psx : 0.0f;
|
|
||||||
```
|
|
||||||
|
|
||||||
### const placement
|
|
||||||
`const` before the type, `*` attached to the variable:
|
|
||||||
|
|
||||||
```c
|
|
||||||
const char_t *name
|
|
||||||
const void *src
|
|
||||||
const size_t size
|
|
||||||
```
|
|
||||||
|
|
||||||
### Comments in `.c` files
|
|
||||||
- Do not use section dividers (`/* ---- ... ---- */`). Just let the
|
|
||||||
functions follow one another with a single blank line between them.
|
|
||||||
- Multi-line explanatory comments inside function bodies use `//` lines:
|
|
||||||
```c
|
|
||||||
// Script modules are freed; orphaned JS wrapper objects now get GC'd
|
|
||||||
// so their finalizers fire before assetDispose() checks ref counts.
|
|
||||||
jerry_heap_gc(JERRY_GC_PRESSURE_HIGH);
|
|
||||||
```
|
|
||||||
- Do not use `/* */` for inline or inline-block comments inside `.c`
|
|
||||||
function bodies.
|
|
||||||
|
|
||||||
### Comments in `.h` files
|
|
||||||
Every public declaration gets a Javadoc block (`/** … */`) with
|
|
||||||
`@param` and `@returns` where relevant. Keep it on the lines immediately
|
|
||||||
above the declaration with no blank line in between.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Color system
|
|
||||||
|
|
||||||
Colors are defined in `src/dusk/display/color.csv` and code-generated
|
|
||||||
into a `color.h` header by `tools/color/csv/__main__.py`.
|
|
||||||
|
|
||||||
Each row in the CSV has `name,r,g,b,a` with channel values in `[0.0, 1.0]`.
|
|
||||||
The script emits four `#define` variants per color plus a bare alias:
|
|
||||||
|
|
||||||
```
|
|
||||||
COLOR_<NAME>_4B color4b(r8, g8, b8, a8) // default alias target
|
|
||||||
COLOR_<NAME>_3B color3b(r8, g8, b8)
|
|
||||||
COLOR_<NAME>_3F color3f(rf, gf, bf)
|
|
||||||
COLOR_<NAME>_4F color4f(rf, gf, bf, af)
|
|
||||||
COLOR_<NAME> COLOR_<NAME>_4B
|
|
||||||
```
|
|
||||||
|
|
||||||
`color_t` is `color4b_t` (four `uint8_t` channels).
|
|
||||||
|
|
||||||
To add a new color, append a row to `color.csv` and rebuild — do not
|
|
||||||
hand-edit the generated header.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Tests
|
|
||||||
- Tests live in `test/` mirroring `src/dusk/` structure.
|
|
||||||
- Use cmocka; include `dusktest.h`.
|
|
||||||
- Test functions: `static void test_something(void **state)`.
|
|
||||||
- After each test, assert `memoryGetAllocatedCount() == 0` to catch
|
|
||||||
leaks.
|
|
||||||
- Build with `-DDUSK_BUILD_TESTS=ON`.
|
|
||||||
+12
-1
@@ -13,6 +13,7 @@ cmake_policy(SET CMP0079 NEW)
|
|||||||
# set(FETCHCONTENT_UPDATES_DISCONNECTED ON)
|
# set(FETCHCONTENT_UPDATES_DISCONNECTED ON)
|
||||||
|
|
||||||
option(DUSK_BUILD_TESTS "Enable tests" OFF)
|
option(DUSK_BUILD_TESTS "Enable tests" OFF)
|
||||||
|
option(DUSK_NETWORKING "Enable networking support" OFF)
|
||||||
|
|
||||||
set(DUSK_GAME_NAME "Dusk" CACHE STRING "Game display name")
|
set(DUSK_GAME_NAME "Dusk" CACHE STRING "Game display name")
|
||||||
set(DUSK_GAME_AUTHOR "YourWishes" CACHE STRING "Game author / coder")
|
set(DUSK_GAME_AUTHOR "YourWishes" CACHE STRING "Game author / coder")
|
||||||
@@ -90,6 +91,12 @@ target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME}
|
|||||||
DUSK_VERSION="${DUSK_VERSION}"
|
DUSK_VERSION="${DUSK_VERSION}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if(DUSK_NETWORKING)
|
||||||
|
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
|
||||||
|
DUSK_NETWORKING
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
|
||||||
# Toolchains
|
# Toolchains
|
||||||
include(cmake/targets/${DUSK_TARGET_SYSTEM}.cmake)
|
include(cmake/targets/${DUSK_TARGET_SYSTEM}.cmake)
|
||||||
|
|
||||||
@@ -116,7 +123,11 @@ if(DUSK_BUILD_TESTS)
|
|||||||
endif()
|
endif()
|
||||||
|
|
||||||
# Build assets
|
# Build assets
|
||||||
file(GLOB_RECURSE DUSK_ASSET_FILES CONFIGURE_DEPENDS "${DUSK_ASSETS_DIR}/*")
|
# Deliberately not CONFIGURE_DEPENDS: that reruns the full CMake configure
|
||||||
|
# step (which invalidates generated headers and forces a huge rebuild) on
|
||||||
|
# every single asset edit. Re-run cmake manually when assets are added or
|
||||||
|
# removed.
|
||||||
|
file(GLOB_RECURSE DUSK_ASSET_FILES "${DUSK_ASSETS_DIR}/*")
|
||||||
add_custom_command(
|
add_custom_command(
|
||||||
OUTPUT "${DUSK_ASSETS_ZIP}"
|
OUTPUT "${DUSK_ASSETS_ZIP}"
|
||||||
COMMAND ${CMAKE_COMMAND} -E make_directory "${DUSK_ASSETS_DIR}"
|
COMMAND ${CMAKE_COMMAND} -E make_directory "${DUSK_ASSETS_DIR}"
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Copyright (c) 2026 Dominic Masters
|
||||||
|
#
|
||||||
|
# This software is released under the MIT License.
|
||||||
|
# https://opensource.org/licenses/MIT
|
||||||
|
|
||||||
|
add_subdirectory(debug)
|
||||||
|
add_subdirectory(frame)
|
||||||
|
add_subdirectory(focus)
|
||||||
|
add_subdirectory(overlay)
|
||||||
|
add_subdirectory(transition)
|
||||||
|
add_subdirectory(widget)
|
||||||
|
|
||||||
|
# Sources
|
||||||
|
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||||
|
PUBLIC
|
||||||
|
ui.c
|
||||||
|
uielement.c
|
||||||
|
)
|
||||||
@@ -7,5 +7,4 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
|||||||
PUBLIC
|
PUBLIC
|
||||||
uiconsole.c
|
uiconsole.c
|
||||||
uifps.c
|
uifps.c
|
||||||
uiplayerpos.c
|
|
||||||
)
|
)
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "uiconsole.h"
|
||||||
|
#include "assert/assert.h"
|
||||||
|
#include "console/console.h"
|
||||||
|
#include "display/screen/screen.h"
|
||||||
|
#include "display/text/text.h"
|
||||||
|
#include "display/spritebatch/spritebatch.h"
|
||||||
|
#include "display/shader/shaderunlit.h"
|
||||||
|
#include "display/mesh/mesh.h"
|
||||||
|
#include "util/memory.h"
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
mesh_t mesh;
|
||||||
|
bool_t built;
|
||||||
|
meshvertex_t vertices[UI_CONSOLE_CACHE_VERTEX_MAX];
|
||||||
|
int32_t vertexCount;
|
||||||
|
int32_t cachedScanX;
|
||||||
|
int32_t cachedScanY;
|
||||||
|
} uiconsolecache_t;
|
||||||
|
|
||||||
|
uiconsolecache_t UI_CONSOLE_CACHE;
|
||||||
|
|
||||||
|
errorret_t uiConsoleDraw(void) {
|
||||||
|
if(!CONSOLE.visible) errorOk();
|
||||||
|
|
||||||
|
if(
|
||||||
|
CONSOLE.dirty ||
|
||||||
|
UI_CONSOLE_CACHE.cachedScanX != SCREEN.scanX ||
|
||||||
|
UI_CONSOLE_CACHE.cachedScanY != SCREEN.scanY
|
||||||
|
) {
|
||||||
|
errorChain(uiConsoleRebuild());
|
||||||
|
}
|
||||||
|
|
||||||
|
if(UI_CONSOLE_CACHE.vertexCount == 0) errorOk();
|
||||||
|
|
||||||
|
shadermaterial_t material = {
|
||||||
|
.unlit = {
|
||||||
|
.color = COLOR_RED,
|
||||||
|
.texture = FONT_DEFAULT.texture
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
errorChain(shaderBind(&SHADER_UNLIT));
|
||||||
|
errorChain(shaderSetMaterial(&SHADER_UNLIT, &material));
|
||||||
|
return meshDraw(&UI_CONSOLE_CACHE.mesh, 0, UI_CONSOLE_CACHE.vertexCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t uiConsoleRebuild(void) {
|
||||||
|
int32_t spriteCount = 0;
|
||||||
|
for(uint32_t i = 0; i < CONSOLE_HISTORY_MAX; i++) {
|
||||||
|
char_t c;
|
||||||
|
int32_t j = 0;
|
||||||
|
while((c = CONSOLE.line[i][j++]) != '\0') {
|
||||||
|
if(c != ' ' && c != '\n') spriteCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assertTrue(
|
||||||
|
spriteCount <= UI_CONSOLE_CACHE_GLYPH_MAX,
|
||||||
|
"Console history exceeds fixed cache capacity"
|
||||||
|
);
|
||||||
|
|
||||||
|
const int32_t vertexCount = spriteCount * QUAD_VERTEX_COUNT;
|
||||||
|
UI_CONSOLE_CACHE.vertexCount = vertexCount;
|
||||||
|
|
||||||
|
if(vertexCount > 0) {
|
||||||
|
const float_t lineH = (float_t)FONT_DEFAULT.tileset->tileHeight;
|
||||||
|
const float_t charW = (float_t)FONT_DEFAULT.tileset->tileWidth;
|
||||||
|
|
||||||
|
int32_t index = 0;
|
||||||
|
int32_t row = 0;
|
||||||
|
for(uint32_t i = 0; i < CONSOLE_HISTORY_MAX; i++) {
|
||||||
|
float_t posX = (float_t)SCREEN.scanX;
|
||||||
|
float_t posY = (float_t)SCREEN.scanY + lineH * (float_t)row;
|
||||||
|
|
||||||
|
char_t c;
|
||||||
|
int32_t j = 0;
|
||||||
|
while((c = CONSOLE.line[i][j++]) != '\0') {
|
||||||
|
if(c == '\n') {
|
||||||
|
posX = (float_t)SCREEN.scanX;
|
||||||
|
posY += lineH;
|
||||||
|
row++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(c == ' ') {
|
||||||
|
posX += charW;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const spritebatchsprite_t sprite = textGetSprite(
|
||||||
|
(vec2){ posX, posY }, c, &FONT_DEFAULT
|
||||||
|
);
|
||||||
|
spriteBatchBufferToMesh(
|
||||||
|
&sprite, 1,
|
||||||
|
&UI_CONSOLE_CACHE.vertices[index], QUAD_VERTEX_COUNT
|
||||||
|
);
|
||||||
|
|
||||||
|
index += QUAD_VERTEX_COUNT;
|
||||||
|
posX += charW;
|
||||||
|
}
|
||||||
|
|
||||||
|
row++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!UI_CONSOLE_CACHE.built) {
|
||||||
|
errorChain(meshInit(
|
||||||
|
&UI_CONSOLE_CACHE.mesh,
|
||||||
|
QUAD_PRIMITIVE_TYPE,
|
||||||
|
UI_CONSOLE_CACHE_VERTEX_MAX,
|
||||||
|
UI_CONSOLE_CACHE.vertices
|
||||||
|
));
|
||||||
|
UI_CONSOLE_CACHE.built = true;
|
||||||
|
} else {
|
||||||
|
errorChain(meshFlush(&UI_CONSOLE_CACHE.mesh, 0, vertexCount));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
UI_CONSOLE_CACHE.cachedScanX = SCREEN.scanX;
|
||||||
|
UI_CONSOLE_CACHE.cachedScanY = SCREEN.scanY;
|
||||||
|
CONSOLE.dirty = false;
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t uiConsoleDispose(void) {
|
||||||
|
if(UI_CONSOLE_CACHE.built) {
|
||||||
|
errorChain(meshDispose(&UI_CONSOLE_CACHE.mesh));
|
||||||
|
}
|
||||||
|
|
||||||
|
memoryZero(&UI_CONSOLE_CACHE, sizeof(uiconsolecache_t));
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include "error/error.h"
|
||||||
|
#include "display/mesh/quad.h"
|
||||||
|
|
||||||
|
// Fixed capacity for the console's cached glyph mesh, in glyphs -- the
|
||||||
|
// mesh/vertex buffer is sized to this once and never grown/shrunk, so
|
||||||
|
// history producing more non-space characters than this asserts (see
|
||||||
|
// uiConsoleRebuild) instead of reallocating.
|
||||||
|
#define UI_CONSOLE_CACHE_GLYPH_MAX 512
|
||||||
|
#define UI_CONSOLE_CACHE_VERTEX_MAX (\
|
||||||
|
UI_CONSOLE_CACHE_GLYPH_MAX * QUAD_VERTEX_COUNT \
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the console history into the scan-safe area, drawing a mesh
|
||||||
|
* that is only rebuilt when the history changes or the scan-safe origin
|
||||||
|
* moves, rather than rebuffering vertices every frame. No-ops when the
|
||||||
|
* console is not visible.
|
||||||
|
*
|
||||||
|
* @return Any error that occurs.
|
||||||
|
*/
|
||||||
|
errorret_t uiConsoleDraw(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rebuilds the cached mesh for the console's current history and
|
||||||
|
* scan-safe origin. Called automatically by uiConsoleDraw() whenever
|
||||||
|
* needed; only needs calling directly to force an immediate rebuild.
|
||||||
|
* The underlying vertex buffer is a fixed-size array (see
|
||||||
|
* UI_CONSOLE_CACHE_VERTEX_MAX) -- asserts if the history produces more
|
||||||
|
* glyphs than that capacity, rather than growing it.
|
||||||
|
*
|
||||||
|
* @return Any error that occurs.
|
||||||
|
*/
|
||||||
|
errorret_t uiConsoleRebuild(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Frees the mesh and vertex buffer built up by uiConsoleDraw().
|
||||||
|
*
|
||||||
|
* @return Any error that occurs.
|
||||||
|
*/
|
||||||
|
errorret_t uiConsoleDispose(void);
|
||||||
@@ -7,13 +7,21 @@
|
|||||||
|
|
||||||
#include "uifps.h"
|
#include "uifps.h"
|
||||||
#include "time/time.h"
|
#include "time/time.h"
|
||||||
#include "display/spritebatch/spritebatch.h"
|
#include "util/string.h"
|
||||||
#include "display/text/text.h"
|
#include "display/text/text.h"
|
||||||
#include "display/screen/screen.h"
|
#include "display/screen/screen.h"
|
||||||
#include "engine/engine.h"
|
#include "engine/engine.h"
|
||||||
|
|
||||||
uifps_t UIFPS;
|
uifps_t UIFPS;
|
||||||
|
|
||||||
|
errorret_t uiFPSInit() {
|
||||||
|
uiLabelInit(&UIFPS.fpsLabel, &FONT_DEFAULT);
|
||||||
|
uiLabelInit(&UIFPS.versionLabel, &FONT_DEFAULT);
|
||||||
|
uiLabelSetText(&UIFPS.versionLabel, ENGINE.version);
|
||||||
|
uiLabelSetColor(&UIFPS.versionLabel, color(255, 255, 255, 128));
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
errorret_t uiFPSDraw() {
|
errorret_t uiFPSDraw() {
|
||||||
char_t fpsText[32];
|
char_t fpsText[32];
|
||||||
|
|
||||||
@@ -50,23 +58,21 @@ errorret_t uiFPSDraw() {
|
|||||||
textColor = COLOR_RED;
|
textColor = COLOR_RED;
|
||||||
}
|
}
|
||||||
|
|
||||||
errorChain(textDraw(
|
uiLabelSetColor(&UIFPS.fpsLabel, textColor);
|
||||||
(float_t)SCREEN.scanX,
|
if(stringCompare(fpsText, UIFPS.fpsLabel.text) != 0) {
|
||||||
(float_t)SCREEN.scanY,
|
uiLabelSetText(&UIFPS.fpsLabel, fpsText);
|
||||||
fpsText, textColor,
|
}
|
||||||
&FONT_DEFAULT
|
errorChain(uiLabelDraw(
|
||||||
|
&UIFPS.fpsLabel, (float_t)SCREEN.scanX, (float_t)SCREEN.scanY
|
||||||
));
|
));
|
||||||
errorChain(spriteBatchFlush());
|
|
||||||
|
|
||||||
int32_t versionWidth, versionHeight;
|
int32_t versionWidth, versionHeight;
|
||||||
textMeasure(ENGINE.version, &FONT_DEFAULT, &versionWidth, &versionHeight);
|
uiLabelGetSize(&UIFPS.versionLabel, &versionWidth, &versionHeight);
|
||||||
errorChain(textDraw(
|
errorChain(uiLabelDraw(
|
||||||
|
&UIFPS.versionLabel,
|
||||||
(float_t)(SCREEN.scanX + SCREEN.scanWidth - versionWidth),
|
(float_t)(SCREEN.scanX + SCREEN.scanWidth - versionWidth),
|
||||||
(float_t)(SCREEN.scanY + SCREEN.scanHeight - versionHeight),
|
(float_t)(SCREEN.scanY + SCREEN.scanHeight - versionHeight)
|
||||||
ENGINE.version,
|
|
||||||
color(255, 255, 255, 128),
|
|
||||||
&FONT_DEFAULT
|
|
||||||
));
|
));
|
||||||
|
|
||||||
return spriteBatchFlush();
|
errorOk();
|
||||||
}
|
}
|
||||||
@@ -8,14 +8,24 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include "error/error.h"
|
#include "error/error.h"
|
||||||
#include "time/timeepoch.h"
|
#include "time/timeepoch.h"
|
||||||
|
#include "ui/widget/uilabel.h"
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
dusktimeepoch_t lastTick;
|
dusktimeepoch_t lastTick;
|
||||||
float_t fpsAverage;
|
float_t fpsAverage;
|
||||||
|
uilabel_t fpsLabel;
|
||||||
|
uilabel_t versionLabel;
|
||||||
} uifps_t;
|
} uifps_t;
|
||||||
|
|
||||||
extern uifps_t UIFPS;
|
extern uifps_t UIFPS;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the FPS counter, caching the (static) version label.
|
||||||
|
*
|
||||||
|
* @return Any error that occurs.
|
||||||
|
*/
|
||||||
|
errorret_t uiFPSInit();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Draws the FPS counter on the screen, and also does the update (for now).
|
* Draws the FPS counter on the screen, and also does the update (for now).
|
||||||
*
|
*
|
||||||
@@ -12,11 +12,11 @@
|
|||||||
#include "time/time.h"
|
#include "time/time.h"
|
||||||
|
|
||||||
const uifocusdirmap_t UI_FOCUS_DIR_MAP[] = {
|
const uifocusdirmap_t UI_FOCUS_DIR_MAP[] = {
|
||||||
{ INPUT_ACTION_UP, UI_FOCUS_DIRECTION_UP, 0, -1 },
|
{ INPUT_BIND_UP, UI_FOCUS_DIRECTION_UP, 0, -1 },
|
||||||
{ INPUT_ACTION_DOWN, UI_FOCUS_DIRECTION_DOWN, 0, 1 },
|
{ INPUT_BIND_DOWN, UI_FOCUS_DIRECTION_DOWN, 0, 1 },
|
||||||
{ INPUT_ACTION_LEFT, UI_FOCUS_DIRECTION_LEFT, -1, 0 },
|
{ INPUT_BIND_LEFT, UI_FOCUS_DIRECTION_LEFT, -1, 0 },
|
||||||
{ INPUT_ACTION_RIGHT, UI_FOCUS_DIRECTION_RIGHT, 1, 0 },
|
{ INPUT_BIND_RIGHT, UI_FOCUS_DIRECTION_RIGHT, 1, 0 },
|
||||||
{ INPUT_ACTION_NULL, UI_FOCUS_DIRECTION_NONE, 0, 0 },
|
{ INPUT_BIND_NULL, UI_FOCUS_DIRECTION_NONE, 0, 0 },
|
||||||
};
|
};
|
||||||
|
|
||||||
uifocus_t UI_FOCUS;
|
uifocus_t UI_FOCUS;
|
||||||
@@ -108,7 +108,7 @@ void uiFocusMoveDirection(
|
|||||||
) {
|
) {
|
||||||
for(
|
for(
|
||||||
const uifocusdirmap_t *m = UI_FOCUS_DIR_MAP;
|
const uifocusdirmap_t *m = UI_FOCUS_DIR_MAP;
|
||||||
m->action != INPUT_ACTION_NULL;
|
m->action != INPUT_BIND_NULL;
|
||||||
m++
|
m++
|
||||||
) {
|
) {
|
||||||
if(m->direction != dir) continue;
|
if(m->direction != dir) continue;
|
||||||
@@ -142,19 +142,19 @@ void uiFocusUpdate(void) {
|
|||||||
|
|
||||||
uifocusitem_t *item = &UI_FOCUS.items[UI_FOCUS.count - 1];
|
uifocusitem_t *item = &UI_FOCUS.items[UI_FOCUS.count - 1];
|
||||||
|
|
||||||
if(inputPressed(INPUT_ACTION_ACCEPT)) {
|
if(inputPressed(INPUT_BIND_ACCEPT)) {
|
||||||
if(item->selected != NULL) item->selected(item);
|
if(item->selected != NULL) item->selected(item);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(inputPressed(INPUT_ACTION_CANCEL)) {
|
if(inputPressed(INPUT_BIND_CANCEL)) {
|
||||||
uiFocusPop();
|
uiFocusPop();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for(
|
for(
|
||||||
const uifocusdirmap_t *m = UI_FOCUS_DIR_MAP;
|
const uifocusdirmap_t *m = UI_FOCUS_DIR_MAP;
|
||||||
m->action != INPUT_ACTION_NULL;
|
m->action != INPUT_BIND_NULL;
|
||||||
m++
|
m++
|
||||||
) {
|
) {
|
||||||
if(!inputPressed(m->action)) continue;
|
if(!inputPressed(m->action)) continue;
|
||||||
@@ -167,7 +167,7 @@ void uiFocusUpdate(void) {
|
|||||||
bool_t held = false;
|
bool_t held = false;
|
||||||
for(
|
for(
|
||||||
const uifocusdirmap_t *m = UI_FOCUS_DIR_MAP;
|
const uifocusdirmap_t *m = UI_FOCUS_DIR_MAP;
|
||||||
m->action != INPUT_ACTION_NULL;
|
m->action != INPUT_BIND_NULL;
|
||||||
m++
|
m++
|
||||||
) {
|
) {
|
||||||
if(m->direction != UI_FOCUS.direction) continue;
|
if(m->direction != UI_FOCUS.direction) continue;
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
#define UI_FOCUS_HOLD_REPEAT 0.1f
|
#define UI_FOCUS_HOLD_REPEAT 0.1f
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
inputaction_t action;
|
inputbind_t action;
|
||||||
uifocusdirection_t direction;
|
uifocusdirection_t direction;
|
||||||
int8_t dx;
|
int8_t dx;
|
||||||
int8_t dy;
|
int8_t dy;
|
||||||
@@ -31,7 +31,7 @@ typedef struct {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Mapping of input actions to focus directions, terminated by an
|
* Mapping of input actions to focus directions, terminated by an
|
||||||
* entry with action == INPUT_ACTION_NULL.
|
* entry with action == INPUT_BIND_NULL.
|
||||||
*/
|
*/
|
||||||
extern const uifocusdirmap_t UI_FOCUS_DIR_MAP[];
|
extern const uifocusdirmap_t UI_FOCUS_DIR_MAP[];
|
||||||
|
|
||||||
@@ -3,10 +3,10 @@
|
|||||||
# This software is released under the MIT License.
|
# This software is released under the MIT License.
|
||||||
# https://opensource.org/licenses/MIT
|
# https://opensource.org/licenses/MIT
|
||||||
|
|
||||||
# Sources
|
|
||||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||||
PUBLIC
|
PUBLIC
|
||||||
battle.c
|
uiframe.c
|
||||||
battlefighter.c
|
uiconfirm.c
|
||||||
party.c
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
add_subdirectory(settings)
|
||||||
+1
-3
@@ -13,7 +13,6 @@
|
|||||||
#include "ui/frame/uiframe.h"
|
#include "ui/frame/uiframe.h"
|
||||||
#include "ui/frame/uiconfirm.h"
|
#include "ui/frame/uiconfirm.h"
|
||||||
#include "util/memory.h"
|
#include "util/memory.h"
|
||||||
#include "display/spritebatch/spritebatch.h"
|
|
||||||
#include "display/text/text.h"
|
#include "display/text/text.h"
|
||||||
#include "display/screen/screen.h"
|
#include "display/screen/screen.h"
|
||||||
#include "assert/assert.h"
|
#include "assert/assert.h"
|
||||||
@@ -149,7 +148,7 @@ errorret_t uiSettingsDraw(void) {
|
|||||||
const float_t y = (float_t)SCREEN.scanY +
|
const float_t y = (float_t)SCREEN.scanY +
|
||||||
((float_t)SCREEN.scanHeight - height) * 0.5f;
|
((float_t)SCREEN.scanHeight - height) * 0.5f;
|
||||||
|
|
||||||
errorChain(uiFrameDraw(x, y, width, height));
|
errorChain(uiFrameDrawCached(&UI_SETTINGS.frameCache, x, y, width, height));
|
||||||
|
|
||||||
const float_t contentX = x + UI_FRAME_START_X;
|
const float_t contentX = x + UI_FRAME_START_X;
|
||||||
const float_t contentY = y + UI_FRAME_START_Y;
|
const float_t contentY = y + UI_FRAME_START_Y;
|
||||||
@@ -174,7 +173,6 @@ errorret_t uiSettingsDraw(void) {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
errorChain(spriteBatchFlush());
|
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -8,6 +8,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include "error/error.h"
|
#include "error/error.h"
|
||||||
#include "ui/widget/uimenu.h"
|
#include "ui/widget/uimenu.h"
|
||||||
|
#include "ui/frame/uiframe.h"
|
||||||
#include "uisettingsdata.h"
|
#include "uisettingsdata.h"
|
||||||
|
|
||||||
#define UI_SETTINGS_TAB_COUNT 4
|
#define UI_SETTINGS_TAB_COUNT 4
|
||||||
@@ -29,6 +30,7 @@ typedef struct {
|
|||||||
char_t tabLabels[UI_SETTINGS_TAB_COUNT][UI_SETTINGS_TAB_LABEL_MAX];
|
char_t tabLabels[UI_SETTINGS_TAB_COUNT][UI_SETTINGS_TAB_LABEL_MAX];
|
||||||
char_t applyLabel[UI_SETTINGS_APPLY_LABEL_MAX];
|
char_t applyLabel[UI_SETTINGS_APPLY_LABEL_MAX];
|
||||||
uisettingsdata_t data;
|
uisettingsdata_t data;
|
||||||
|
uiframecache_t frameCache;
|
||||||
} uisettings_t;
|
} uisettings_t;
|
||||||
|
|
||||||
extern uisettings_t UI_SETTINGS;
|
extern uisettings_t UI_SETTINGS;
|
||||||
+2
@@ -12,6 +12,7 @@
|
|||||||
#include "locale/localemanager.h"
|
#include "locale/localemanager.h"
|
||||||
#include "asset/loader/locale/assetlocaleloader.h"
|
#include "asset/loader/locale/assetlocaleloader.h"
|
||||||
#include "input/input.h"
|
#include "input/input.h"
|
||||||
|
#include "save/savesettings.h"
|
||||||
|
|
||||||
void uiSettingsInputSelected(
|
void uiSettingsInputSelected(
|
||||||
const uimenu_t *menu,
|
const uimenu_t *menu,
|
||||||
@@ -66,6 +67,7 @@ void uiSettingsInputApply(void) {
|
|||||||
&UI_SETTINGS.data.input.items[UI_SETTINGS_INPUT_INDEX_DEADZONE].slider
|
&UI_SETTINGS.data.input.items[UI_SETTINGS_INPUT_INDEX_DEADZONE].slider
|
||||||
);
|
);
|
||||||
#endif
|
#endif
|
||||||
|
errorCatch(errorPrint(saveSettingsWrite()));
|
||||||
uiMenuClose(&UI_SETTINGS.data.input.menu);
|
uiMenuClose(&UI_SETTINGS.data.input.menu);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -9,7 +9,6 @@
|
|||||||
#include "uiframe.h"
|
#include "uiframe.h"
|
||||||
#include "assert/assert.h"
|
#include "assert/assert.h"
|
||||||
#include "util/memory.h"
|
#include "util/memory.h"
|
||||||
#include "util/string.h"
|
|
||||||
#include "util/math.h"
|
#include "util/math.h"
|
||||||
#include "display/screen/screen.h"
|
#include "display/screen/screen.h"
|
||||||
#include "display/text/text.h"
|
#include "display/text/text.h"
|
||||||
@@ -39,6 +38,7 @@ void uiConfirmClosed(const uimenu_t *menu) {
|
|||||||
|
|
||||||
errorret_t uiConfirmInit(void) {
|
errorret_t uiConfirmInit(void) {
|
||||||
memoryZero(&UI_CONFIRM, sizeof(uiconfirm_t));
|
memoryZero(&UI_CONFIRM, sizeof(uiconfirm_t));
|
||||||
|
uiLabelInit(&UI_CONFIRM.textLabel, &FONT_DEFAULT);
|
||||||
|
|
||||||
MENU_BEGIN(
|
MENU_BEGIN(
|
||||||
&UI_CONFIRM.menu, UI_CONFIRM.items, uiConfirmSelected, uiConfirmClosed, NULL
|
&UI_CONFIRM.menu, UI_CONFIRM.items, uiConfirmSelected, uiConfirmClosed, NULL
|
||||||
@@ -69,10 +69,9 @@ errorret_t uiConfirmDraw(void) {
|
|||||||
errorChain(
|
errorChain(
|
||||||
spriteBatchBuffer(&backdropSprite, 1, &SHADER_UNLIT, backdropMaterial)
|
spriteBatchBuffer(&backdropSprite, 1, &SHADER_UNLIT, backdropMaterial)
|
||||||
);
|
);
|
||||||
errorChain(spriteBatchFlush());
|
|
||||||
|
|
||||||
int32_t textW, textH;
|
int32_t textW, textH;
|
||||||
textMeasure(UI_CONFIRM.text, &FONT_DEFAULT, &textW, &textH);
|
uiLabelGetSize(&UI_CONFIRM.textLabel, &textW, &textH);
|
||||||
|
|
||||||
float_t rowHeight = (float_t)FONT_DEFAULT.tileset->tileHeight;
|
float_t rowHeight = (float_t)FONT_DEFAULT.tileset->tileHeight;
|
||||||
float_t width = mathMax(
|
float_t width = mathMax(
|
||||||
@@ -83,18 +82,17 @@ errorret_t uiConfirmDraw(void) {
|
|||||||
float_t x = (float_t)SCREEN.scanX + ((float_t)SCREEN.scanWidth - width) * 0.5f;
|
float_t x = (float_t)SCREEN.scanX + ((float_t)SCREEN.scanWidth - width) * 0.5f;
|
||||||
float_t y = (float_t)SCREEN.scanY + ((float_t)SCREEN.scanHeight - height) * 0.5f;
|
float_t y = (float_t)SCREEN.scanY + ((float_t)SCREEN.scanHeight - height) * 0.5f;
|
||||||
|
|
||||||
errorChain(uiFrameDraw(x, y, width, height));
|
errorChain(uiFrameDrawCached(&UI_CONFIRM.frameCache, x, y, width, height));
|
||||||
|
|
||||||
float_t contentX = x + UI_FRAME_START_X;
|
float_t contentX = x + UI_FRAME_START_X;
|
||||||
float_t contentY = y + UI_FRAME_START_Y;
|
float_t contentY = y + UI_FRAME_START_Y;
|
||||||
float_t contentWidth = width - (UI_FRAME_START_X * 2);
|
float_t contentWidth = width - (UI_FRAME_START_X * 2);
|
||||||
|
|
||||||
errorChain(textDraw(contentX, contentY, UI_CONFIRM.text, COLOR_WHITE, &FONT_DEFAULT));
|
errorChain(uiLabelDraw(&UI_CONFIRM.textLabel, contentX, contentY));
|
||||||
|
|
||||||
float_t buttonsY = contentY + rowHeight + UI_FRAME_PADDING_Y;
|
float_t buttonsY = contentY + rowHeight + UI_FRAME_PADDING_Y;
|
||||||
errorChain(uiMenuDraw(&UI_CONFIRM.menu, contentX, buttonsY, contentWidth, rowHeight));
|
errorChain(uiMenuDraw(&UI_CONFIRM.menu, contentX, buttonsY, contentWidth, rowHeight));
|
||||||
|
|
||||||
errorChain(spriteBatchFlush());
|
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,7 +110,7 @@ void uiConfirmOpen(
|
|||||||
void *user
|
void *user
|
||||||
) {
|
) {
|
||||||
assertNotNull(question, "Question cannot be NULL");
|
assertNotNull(question, "Question cannot be NULL");
|
||||||
stringCopy(UI_CONFIRM.text, question, UI_CONFIRM_TEXT_MAX);
|
uiLabelSetText(&UI_CONFIRM.textLabel, question);
|
||||||
UI_CONFIRM.callback = callback;
|
UI_CONFIRM.callback = callback;
|
||||||
UI_CONFIRM.user = user;
|
UI_CONFIRM.user = user;
|
||||||
UI_CONFIRM.result = false;
|
UI_CONFIRM.result = false;
|
||||||
@@ -8,8 +8,9 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include "error/error.h"
|
#include "error/error.h"
|
||||||
#include "ui/widget/uimenu.h"
|
#include "ui/widget/uimenu.h"
|
||||||
|
#include "ui/widget/uilabel.h"
|
||||||
|
#include "ui/frame/uiframe.h"
|
||||||
|
|
||||||
#define UI_CONFIRM_TEXT_MAX 256
|
|
||||||
#define UI_CONFIRM_MIN_WIDTH 160.0f
|
#define UI_CONFIRM_MIN_WIDTH 160.0f
|
||||||
#define UI_CONFIRM_INDEX_CONFIRM 0
|
#define UI_CONFIRM_INDEX_CONFIRM 0
|
||||||
#define UI_CONFIRM_INDEX_CANCEL 1
|
#define UI_CONFIRM_INDEX_CANCEL 1
|
||||||
@@ -25,12 +26,13 @@
|
|||||||
typedef void (*uiconfirmcallback_t)(const bool_t result, void *user);
|
typedef void (*uiconfirmcallback_t)(const bool_t result, void *user);
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
char_t text[UI_CONFIRM_TEXT_MAX];
|
uilabel_t textLabel;
|
||||||
uimenu_t menu;
|
uimenu_t menu;
|
||||||
uimenuitem_t items[UI_CONFIRM_ITEM_COUNT];
|
uimenuitem_t items[UI_CONFIRM_ITEM_COUNT];
|
||||||
uiconfirmcallback_t callback;
|
uiconfirmcallback_t callback;
|
||||||
void *user;
|
void *user;
|
||||||
bool_t result;
|
bool_t result;
|
||||||
|
uiframecache_t frameCache;
|
||||||
} uiconfirm_t;
|
} uiconfirm_t;
|
||||||
|
|
||||||
extern uiconfirm_t UI_CONFIRM;
|
extern uiconfirm_t UI_CONFIRM;
|
||||||
@@ -72,6 +72,17 @@ errorret_t uiFrameDraw(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
spritebatchsprite_t sprites[9];
|
spritebatchsprite_t sprites[9];
|
||||||
|
uiFrameBuildSprites(sprites, x, y, width, height);
|
||||||
|
return spriteBatchBuffer(sprites, 9, &SHADER_UNLIT, material);
|
||||||
|
}
|
||||||
|
|
||||||
|
void uiFrameBuildSprites(
|
||||||
|
spritebatchsprite_t sprites[9],
|
||||||
|
const float_t x,
|
||||||
|
const float_t y,
|
||||||
|
const float_t width,
|
||||||
|
const float_t height
|
||||||
|
) {
|
||||||
float_t tileW = (float_t)UI_FRAME_BORDER_WIDTH;
|
float_t tileW = (float_t)UI_FRAME_BORDER_WIDTH;
|
||||||
float_t tileH = (float_t)UI_FRAME_BORDER_HEIGHT;
|
float_t tileH = (float_t)UI_FRAME_BORDER_HEIGHT;
|
||||||
|
|
||||||
@@ -112,8 +123,37 @@ errorret_t uiFrameDraw(
|
|||||||
&UI_FRAME.tileset, 2, 2,
|
&UI_FRAME.tileset, 2, 2,
|
||||||
x + width - tileW, y + height - tileH, tileW, tileH
|
x + width - tileW, y + height - tileH, tileW, tileH
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return spriteBatchBuffer(sprites, 9, &SHADER_UNLIT, material);
|
errorret_t uiFrameDrawCached(
|
||||||
|
uiframecache_t *cache,
|
||||||
|
const float_t x,
|
||||||
|
const float_t y,
|
||||||
|
const float_t width,
|
||||||
|
const float_t height
|
||||||
|
) {
|
||||||
|
assertNotNull(cache, "Frame cache cannot be NULL");
|
||||||
|
|
||||||
|
if(
|
||||||
|
!cache->built ||
|
||||||
|
cache->lastX != x || cache->lastY != y ||
|
||||||
|
cache->lastWidth != width || cache->lastHeight != height
|
||||||
|
) {
|
||||||
|
uiFrameBuildSprites(cache->sprites, x, y, width, height);
|
||||||
|
cache->lastX = x;
|
||||||
|
cache->lastY = y;
|
||||||
|
cache->lastWidth = width;
|
||||||
|
cache->lastHeight = height;
|
||||||
|
cache->built = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
shadermaterial_t material = {
|
||||||
|
.unlit = {
|
||||||
|
.color = COLOR_WHITE,
|
||||||
|
.texture = &UI_FRAME.texture
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return spriteBatchBuffer(cache->sprites, 9, &SHADER_UNLIT, material);
|
||||||
}
|
}
|
||||||
|
|
||||||
errorret_t uiFrameDispose(void) {
|
errorret_t uiFrameDispose(void) {
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include "error/error.h"
|
||||||
|
#include "display/texture/texture.h"
|
||||||
|
#include "display/texture/tileset.h"
|
||||||
|
#include "display/spritebatch/spritebatchsprite.h"
|
||||||
|
|
||||||
|
#define UI_FRAME_BORDER_WIDTH 6
|
||||||
|
#define UI_FRAME_BORDER_HEIGHT 6
|
||||||
|
#define UI_FRAME_PADDING_X 2
|
||||||
|
#define UI_FRAME_PADDING_Y 2
|
||||||
|
#define UI_FRAME_START_X (UI_FRAME_BORDER_WIDTH + UI_FRAME_PADDING_X)
|
||||||
|
#define UI_FRAME_START_Y (UI_FRAME_BORDER_HEIGHT + UI_FRAME_PADDING_Y)
|
||||||
|
#define UI_FRAME_TILE_WIDTH 1
|
||||||
|
#define UI_FRAME_TILE_HEIGHT 1
|
||||||
|
#define UI_FRAME_TEXTURE_WIDTH (UI_FRAME_TILE_WIDTH * 3)
|
||||||
|
#define UI_FRAME_TEXTURE_HEIGHT (UI_FRAME_TILE_HEIGHT * 3)
|
||||||
|
#define UI_FRAME_TEXTURE_WIDTH_POW2 4
|
||||||
|
#define UI_FRAME_TEXTURE_HEIGHT_POW2 4
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
tileset_t tileset;
|
||||||
|
texture_t texture;
|
||||||
|
color_t pixels[UI_FRAME_BORDER_WIDTH * UI_FRAME_BORDER_HEIGHT];
|
||||||
|
} uiframe_t;
|
||||||
|
|
||||||
|
extern uiframe_t UI_FRAME;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the global UI_FRAME: builds the dummy texture and
|
||||||
|
* configures the tileset. Call once after displayInit().
|
||||||
|
*
|
||||||
|
* @return Any error that occurs.
|
||||||
|
*/
|
||||||
|
errorret_t uiFrameInit(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draws UI_FRAME using 9-slice rendering from its 3x3 tileset.
|
||||||
|
* Pushes quads to the sprite batch without flushing.
|
||||||
|
*
|
||||||
|
* @param x Screen x position.
|
||||||
|
* @param y Screen y position.
|
||||||
|
* @param width Total width of the frame.
|
||||||
|
* @param height Total height of the frame.
|
||||||
|
* @return Any error that occurs.
|
||||||
|
*/
|
||||||
|
errorret_t uiFrameDraw(
|
||||||
|
const float_t x,
|
||||||
|
const float_t y,
|
||||||
|
const float_t width,
|
||||||
|
const float_t height
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds the 9 sprites for a 9-slice frame at the given rect. Used
|
||||||
|
* internally by uiFrameDraw and uiFrameDrawCached -- call directly only
|
||||||
|
* if you need the raw sprites instead of buffering them.
|
||||||
|
*
|
||||||
|
* @param sprites Destination array of exactly 9 sprites.
|
||||||
|
* @param x Screen x position.
|
||||||
|
* @param y Screen y position.
|
||||||
|
* @param width Total width of the frame.
|
||||||
|
* @param height Total height of the frame.
|
||||||
|
*/
|
||||||
|
void uiFrameBuildSprites(
|
||||||
|
spritebatchsprite_t sprites[9],
|
||||||
|
const float_t x,
|
||||||
|
const float_t y,
|
||||||
|
const float_t width,
|
||||||
|
const float_t height
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A frame's cached 9-slice sprites, plus the rect they were built for.
|
||||||
|
* Owned by whichever widget/screen draws a frame repeatedly (dialogs,
|
||||||
|
* textboxes, settings panels) so uiFrameDrawCached can skip rebuilding
|
||||||
|
* the sprites when the rect hasn't moved since the last draw.
|
||||||
|
*/
|
||||||
|
typedef struct {
|
||||||
|
spritebatchsprite_t sprites[9];
|
||||||
|
bool_t built;
|
||||||
|
float_t lastX;
|
||||||
|
float_t lastY;
|
||||||
|
float_t lastWidth;
|
||||||
|
float_t lastHeight;
|
||||||
|
} uiframecache_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draws a 9-slice frame using a caller-owned cache, only rebuilding the
|
||||||
|
* sprites (via uiFrameBuildSprites) when x/y/width/height differ from
|
||||||
|
* the last call -- unlike uiFrameDraw, which rebuilds unconditionally
|
||||||
|
* every time. Prefer this for frames redrawn every frame at a fixed or
|
||||||
|
* rarely-changing rect (dialogs, panels); use plain uiFrameDraw for
|
||||||
|
* genuinely one-off or per-frame-varying rects.
|
||||||
|
*
|
||||||
|
* @param cache Caller-owned cache, zero-initialized before first use.
|
||||||
|
* @param x Screen x position.
|
||||||
|
* @param y Screen y position.
|
||||||
|
* @param width Total width of the frame.
|
||||||
|
* @param height Total height of the frame.
|
||||||
|
* @return Any error that occurs.
|
||||||
|
*/
|
||||||
|
errorret_t uiFrameDrawCached(
|
||||||
|
uiframecache_t *cache,
|
||||||
|
const float_t x,
|
||||||
|
const float_t y,
|
||||||
|
const float_t width,
|
||||||
|
const float_t height
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disposes of the global UI_FRAME, releasing its GPU texture.
|
||||||
|
*
|
||||||
|
* @return Any error that occurs.
|
||||||
|
*/
|
||||||
|
errorret_t uiFrameDispose(void);
|
||||||
@@ -85,5 +85,5 @@ errorret_t uiCropDraw(void) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
errorChain(spriteBatchBuffer(sprites, count, &SHADER_UNLIT, material));
|
errorChain(spriteBatchBuffer(sprites, count, &SHADER_UNLIT, material));
|
||||||
return spriteBatchFlush();
|
errorOk();
|
||||||
}
|
}
|
||||||
@@ -75,7 +75,7 @@ errorret_t uiFullboxDraw(uifullbox_t *fullbox) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
errorChain(spriteBatchBuffer(&sprite, 1, &SHADER_UNLIT, material));
|
errorChain(spriteBatchBuffer(&sprite, 1, &SHADER_UNLIT, material));
|
||||||
return spriteBatchFlush();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
void uiFullboxTransition(
|
void uiFullboxTransition(
|
||||||
@@ -11,7 +11,6 @@
|
|||||||
#include "time/time.h"
|
#include "time/time.h"
|
||||||
#include "display/text/text.h"
|
#include "display/text/text.h"
|
||||||
#include "display/screen/screen.h"
|
#include "display/screen/screen.h"
|
||||||
#include "display/spritebatch/spritebatch.h"
|
|
||||||
#include "uifullbox.h"
|
#include "uifullbox.h"
|
||||||
|
|
||||||
#define UI_LOADING_TEXT "loading"
|
#define UI_LOADING_TEXT "loading"
|
||||||
@@ -20,6 +19,8 @@ uiloading_t UI_LOADING;
|
|||||||
|
|
||||||
errorret_t uiLoadingInit(void) {
|
errorret_t uiLoadingInit(void) {
|
||||||
memoryZero(&UI_LOADING, sizeof(uiloading_t));
|
memoryZero(&UI_LOADING, sizeof(uiloading_t));
|
||||||
|
uiLabelInit(&UI_LOADING.textLabel, &FONT_DEFAULT);
|
||||||
|
uiLabelSetText(&UI_LOADING.textLabel, UI_LOADING_TEXT);
|
||||||
eventInit(
|
eventInit(
|
||||||
&UI_LOADING.onTransitionEnd,
|
&UI_LOADING.onTransitionEnd,
|
||||||
UI_LOADING.onTransitionEndCallbacks,
|
UI_LOADING.onTransitionEndCallbacks,
|
||||||
@@ -53,7 +54,7 @@ errorret_t uiLoadingDraw(void) {
|
|||||||
if(alpha <= 0.0f) errorOk();
|
if(alpha <= 0.0f) errorOk();
|
||||||
|
|
||||||
int32_t textW, textH;
|
int32_t textW, textH;
|
||||||
textMeasure(UI_LOADING_TEXT, &FONT_DEFAULT, &textW, &textH);
|
uiLabelGetSize(&UI_LOADING.textLabel, &textW, &textH);
|
||||||
|
|
||||||
float_t x = (float_t)(SCREEN.scanX + SCREEN.scanWidth) -
|
float_t x = (float_t)(SCREEN.scanX + SCREEN.scanWidth) -
|
||||||
(float_t)textW - UI_LOADING_MARGIN;
|
(float_t)textW - UI_LOADING_MARGIN;
|
||||||
@@ -62,9 +63,10 @@ errorret_t uiLoadingDraw(void) {
|
|||||||
|
|
||||||
color_t color = COLOR_WHITE;
|
color_t color = COLOR_WHITE;
|
||||||
color.a = (uint8_t)(alpha * 255.0f);
|
color.a = (uint8_t)(alpha * 255.0f);
|
||||||
|
uiLabelSetColor(&UI_LOADING.textLabel, color);
|
||||||
|
|
||||||
errorChain(textDraw(x, y, UI_LOADING_TEXT, color, &FONT_DEFAULT));
|
errorChain(uiLabelDraw(&UI_LOADING.textLabel, x, y));
|
||||||
return spriteBatchFlush();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
static void uiLoadingTransition(
|
static void uiLoadingTransition(
|
||||||
@@ -8,6 +8,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include "error/error.h"
|
#include "error/error.h"
|
||||||
#include "event/event.h"
|
#include "event/event.h"
|
||||||
|
#include "ui/widget/uilabel.h"
|
||||||
|
|
||||||
#define UI_LOADING_FADE_DURATION 0.5f
|
#define UI_LOADING_FADE_DURATION 0.5f
|
||||||
#define UI_LOADING_MARGIN 8.0f
|
#define UI_LOADING_MARGIN 8.0f
|
||||||
@@ -20,6 +21,7 @@ typedef struct {
|
|||||||
eventcallback_t onTransitionEndCallbacks[4];
|
eventcallback_t onTransitionEndCallbacks[4];
|
||||||
void *onTransitionEndUsers[4];
|
void *onTransitionEndUsers[4];
|
||||||
event_t onTransitionEnd;
|
event_t onTransitionEnd;
|
||||||
|
uilabel_t textLabel;
|
||||||
} uiloading_t;
|
} uiloading_t;
|
||||||
|
|
||||||
extern uiloading_t UI_LOADING;
|
extern uiloading_t UI_LOADING;
|
||||||
+1
-1
@@ -52,5 +52,5 @@ errorret_t uiTransitionFadeDraw(const uitransitiondata_t *data) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
errorChain(spriteBatchBuffer(&sprite, 1, &SHADER_UNLIT, material));
|
errorChain(spriteBatchBuffer(&sprite, 1, &SHADER_UNLIT, material));
|
||||||
return spriteBatchFlush();
|
errorOk();
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "ui.h"
|
||||||
|
#include "util/memory.h"
|
||||||
|
#include "assert/assert.h"
|
||||||
|
#include "display/spritebatch/spritebatch.h"
|
||||||
|
#include "display/screen/screen.h"
|
||||||
|
#include "ui/uielement.h"
|
||||||
|
#include "ui/focus/uifocus.h"
|
||||||
|
|
||||||
|
ui_t UI;
|
||||||
|
|
||||||
|
errorret_t uiInit(void) {
|
||||||
|
memoryZero(&UI, sizeof(ui_t));
|
||||||
|
uiFocusInit();
|
||||||
|
uiElementsSort();
|
||||||
|
|
||||||
|
uielement_t *element = &UI_ELEMENTS[0];
|
||||||
|
while(!uiElementIsNull(element)) {
|
||||||
|
errorChain(uiElementInit(element));
|
||||||
|
element++;
|
||||||
|
}
|
||||||
|
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t uiUpdate(void) {
|
||||||
|
uiFocusUpdate();
|
||||||
|
|
||||||
|
uielement_t *element = &UI_ELEMENTS[0];
|
||||||
|
while(!uiElementIsNull(element)) {
|
||||||
|
errorChain(uiElementUpdate(element));
|
||||||
|
element++;
|
||||||
|
}
|
||||||
|
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t uiRender(void) {
|
||||||
|
const uielement_t *element = &UI_ELEMENTS[0];
|
||||||
|
while(!uiElementIsNull(element)) {
|
||||||
|
errorChain(uiElementDraw(element));
|
||||||
|
element++;
|
||||||
|
}
|
||||||
|
|
||||||
|
errorChain(spriteBatchFlush());
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t uiDispose(void) {
|
||||||
|
uielement_t *element = &UI_ELEMENTS[0];
|
||||||
|
while(!uiElementIsNull(element)) {
|
||||||
|
errorChain(uiElementDispose(element));
|
||||||
|
element++;
|
||||||
|
}
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include "error/error.h"
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
void *nothing;
|
||||||
|
} ui_t;
|
||||||
|
|
||||||
|
extern ui_t UI;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the UI system.
|
||||||
|
*/
|
||||||
|
errorret_t uiInit(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the UI system.
|
||||||
|
*
|
||||||
|
* @return Any error that occurs.
|
||||||
|
*/
|
||||||
|
errorret_t uiUpdate(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the UI system.
|
||||||
|
*
|
||||||
|
* @return Any error that occurs.
|
||||||
|
*/
|
||||||
|
errorret_t uiRender(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disposes of the UI system.
|
||||||
|
*
|
||||||
|
* @return Any error that occurs.
|
||||||
|
*/
|
||||||
|
errorret_t uiDispose(void);
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "uielement.h"
|
||||||
|
#include "assert/assert.h"
|
||||||
|
#include "util/sort.h"
|
||||||
|
#include "ui/frame/uiframe.h"
|
||||||
|
#include "ui/debug/uifps.h"
|
||||||
|
#include "engine/engine.h"
|
||||||
|
#include "ui/overlay/uifullbox.h"
|
||||||
|
#include "ui/overlay/uiloading.h"
|
||||||
|
#include "ui/overlay/uicrop.h"
|
||||||
|
#include "ui/transition/uitransition.h"
|
||||||
|
#include "ui/debug/uiconsole.h"
|
||||||
|
#include "ui/frame/uiconfirm.h"
|
||||||
|
|
||||||
|
// Priming pass: X does nothing here, so this only exists to process any
|
||||||
|
// #include directives nested in uielementlist.h/ui/uielementgame.h at
|
||||||
|
// file scope (each such header's own #pragma once makes it a no-op on
|
||||||
|
// the real pass below, which happens inside UI_ELEMENTS[]'s braces,
|
||||||
|
// where a raw #include of a declaration would be invalid).
|
||||||
|
#define X(initFn, updateFn, drawFn, disposeFn, order) // do nothing
|
||||||
|
#include "uielementlist.h"
|
||||||
|
#undef X
|
||||||
|
|
||||||
|
uielement_t UI_ELEMENTS[] = {
|
||||||
|
#define X(initFn, updateFn, drawFn, disposeFn, elementOrder) \
|
||||||
|
{ \
|
||||||
|
.init = initFn, .update = updateFn, .draw = drawFn, \
|
||||||
|
.dispose = disposeFn, .order = elementOrder \
|
||||||
|
},
|
||||||
|
#include "uielementlist.h"
|
||||||
|
#undef X
|
||||||
|
|
||||||
|
{ 0 } // Null terminator
|
||||||
|
};
|
||||||
|
|
||||||
|
bool_t uiElementIsNull(const uielement_t *element) {
|
||||||
|
return element->init == NULL &&
|
||||||
|
element->update == NULL &&
|
||||||
|
element->draw == NULL &&
|
||||||
|
element->dispose == NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
int_t uiElementCompareOrder(const void *a, const void *b) {
|
||||||
|
const uielement_t *elementA = (const uielement_t *)a;
|
||||||
|
const uielement_t *elementB = (const uielement_t *)b;
|
||||||
|
if(elementA->order < elementB->order) return -1;
|
||||||
|
if(elementA->order > elementB->order) return 1;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void uiElementsSort(void) {
|
||||||
|
// The trailing null terminator is always the last static entry -- sort
|
||||||
|
// everything before it, and leave it in place.
|
||||||
|
const size_t count = (sizeof(UI_ELEMENTS) / sizeof(UI_ELEMENTS[0])) - 1;
|
||||||
|
sortBubble(UI_ELEMENTS, count, sizeof(uielement_t), uiElementCompareOrder);
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t uiElementInit(uielement_t *element) {
|
||||||
|
assertNotNull(element, "element must not be NULL");
|
||||||
|
if(element->init != NULL) errorChain(element->init());
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t uiElementUpdate(uielement_t *element) {
|
||||||
|
assertNotNull(element, "element must not be NULL");
|
||||||
|
if(element->update != NULL) errorChain(element->update());
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t uiElementDraw(const uielement_t *element) {
|
||||||
|
assertNotNull(element, "element must not be NULL");
|
||||||
|
if(element->draw != NULL) errorChain(element->draw());
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t uiElementDispose(uielement_t *element) {
|
||||||
|
assertNotNull(element, "element must not be NULL");
|
||||||
|
if(element->dispose != NULL) errorChain(element->dispose());
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include "error/error.h"
|
||||||
|
|
||||||
|
// Built-in order tiers. Lower values update/render first; ties preserve
|
||||||
|
// their relative UI_ELEMENTS declaration order (uiElementsSort uses a
|
||||||
|
// stable sort). Game-specific elements can use any int32_t value -- these
|
||||||
|
// are just the ones the engine itself relies on.
|
||||||
|
#define UI_ELEMENT_ORDER_DEFAULT 0
|
||||||
|
#define UI_ELEMENT_ORDER_DEBUG 1000
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
errorret_t (*init)();
|
||||||
|
errorret_t (*update)();
|
||||||
|
errorret_t (*draw)();
|
||||||
|
errorret_t (*dispose)();
|
||||||
|
int32_t order;
|
||||||
|
} uielement_t;
|
||||||
|
|
||||||
|
extern uielement_t UI_ELEMENTS[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true when all four callbacks on the element are NULL,
|
||||||
|
* which marks the end of the UI_ELEMENTS array.
|
||||||
|
*
|
||||||
|
* @param element The element to test.
|
||||||
|
* @returns True if the element is the null terminator.
|
||||||
|
*/
|
||||||
|
bool_t uiElementIsNull(const uielement_t *element);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compares two elements by their .order field, ascending. Matches
|
||||||
|
* sortcompare_t, for use with the project's sort utilities.
|
||||||
|
*
|
||||||
|
* @param a First uielement_t to compare.
|
||||||
|
* @param b Second uielement_t to compare.
|
||||||
|
* @return Negative if a < b, zero if a == b, positive if a > b.
|
||||||
|
*/
|
||||||
|
int_t uiElementCompareOrder(const void *a, const void *b);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stably sorts UI_ELEMENTS in place by .order, ascending. The trailing
|
||||||
|
* null terminator is never moved. Called once by uiInit -- element order
|
||||||
|
* is static after that, so there's no need to re-sort every frame.
|
||||||
|
*/
|
||||||
|
void uiElementsSort(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes a UI element, invoking its init callback if set.
|
||||||
|
*
|
||||||
|
* @param element The element to initialize.
|
||||||
|
* @return Any error that occurs.
|
||||||
|
*/
|
||||||
|
errorret_t uiElementInit(uielement_t *element);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates a UI element, calling its update callback if set.
|
||||||
|
*
|
||||||
|
* @param element The element to update.
|
||||||
|
* @return Any error that occurs.
|
||||||
|
*/
|
||||||
|
errorret_t uiElementUpdate(uielement_t *element);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draws a UI element, calling its draw callback if set.
|
||||||
|
*
|
||||||
|
* @param element The element to render.
|
||||||
|
* @return Any error that occurs.
|
||||||
|
*/
|
||||||
|
errorret_t uiElementDraw(const uielement_t *element);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disposes of a UI element, invoking its dispose callback if set.
|
||||||
|
*
|
||||||
|
* @param element The element to dispose.
|
||||||
|
* @return Any error that occurs.
|
||||||
|
*/
|
||||||
|
errorret_t uiElementDispose(uielement_t *element);
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
// X(init, update, draw, dispose, order) -- pass NULL for any callback the
|
||||||
|
// element doesn't need. order controls update/draw sequence (lower first,
|
||||||
|
// see UI_ELEMENT_ORDER_* in uielement.h); ties preserve declaration order.
|
||||||
|
// See uielement.c for how this expands.
|
||||||
|
|
||||||
|
X(uiFrameInit, NULL, NULL, uiFrameDispose, UI_ELEMENT_ORDER_DEFAULT)
|
||||||
|
|
||||||
|
// Fullbox under: above scene, below system UI.
|
||||||
|
X(
|
||||||
|
uiFullboxUnderInit, uiFullboxUnderUpdate, uiFullboxUnderDraw, NULL,
|
||||||
|
UI_ELEMENT_ORDER_DEFAULT
|
||||||
|
)
|
||||||
|
|
||||||
|
// Text stuffs
|
||||||
|
X(uiConfirmInit, NULL, uiConfirmDraw, uiConfirmDispose, UI_ELEMENT_ORDER_DEFAULT)
|
||||||
|
|
||||||
|
X(
|
||||||
|
uiTransitionInit, uiTransitionUpdate, uiTransitionDraw, NULL,
|
||||||
|
UI_ELEMENT_ORDER_DEFAULT
|
||||||
|
)
|
||||||
|
|
||||||
|
// Fullbox over: above absolutely everything (except debug).
|
||||||
|
X(
|
||||||
|
uiFullboxOverInit, uiFullboxOverUpdate, uiFullboxOverDraw, NULL,
|
||||||
|
UI_ELEMENT_ORDER_DEFAULT
|
||||||
|
)
|
||||||
|
|
||||||
|
X(
|
||||||
|
uiLoadingInit, uiLoadingUpdate, uiLoadingDraw, NULL,
|
||||||
|
UI_ELEMENT_ORDER_DEFAULT
|
||||||
|
)
|
||||||
|
|
||||||
|
X(uiCropInit, NULL, uiCropDraw, NULL, UI_ELEMENT_ORDER_DEFAULT)
|
||||||
|
|
||||||
|
// Debug items -- always last.
|
||||||
|
X(NULL, NULL, uiConsoleDraw, uiConsoleDispose, UI_ELEMENT_ORDER_DEBUG)
|
||||||
|
X(uiFPSInit, NULL, uiFPSDraw, NULL, UI_ELEMENT_ORDER_DEBUG)
|
||||||
|
|
||||||
|
// Game-specific UI elements (duskrpg)
|
||||||
|
#include "ui/uielementgame.h"
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# Copyright (c) 2026 Dominic Masters
|
||||||
|
#
|
||||||
|
# This software is released under the MIT License.
|
||||||
|
# https://opensource.org/licenses/MIT
|
||||||
|
|
||||||
|
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||||
|
PUBLIC
|
||||||
|
uibutton.c
|
||||||
|
uicheckbox.c
|
||||||
|
uitab.c
|
||||||
|
uislider.c
|
||||||
|
uidropdown.c
|
||||||
|
uiscrolling.c
|
||||||
|
uimenu.c
|
||||||
|
uilabel.c
|
||||||
|
uiwidgetlabel.c
|
||||||
|
)
|
||||||
@@ -7,15 +7,15 @@
|
|||||||
|
|
||||||
#include "uibutton.h"
|
#include "uibutton.h"
|
||||||
#include "assert/assert.h"
|
#include "assert/assert.h"
|
||||||
#include "util/memory.h"
|
|
||||||
#include "display/text/text.h"
|
#include "display/text/text.h"
|
||||||
#include "display/color.h"
|
#include "display/color.h"
|
||||||
|
|
||||||
void uiButtonInit(uibutton_t *button, const char_t *label) {
|
void uiButtonInit(uibutton_t *button, const char_t *label) {
|
||||||
assertNotNull(button, "Button cannot be NULL");
|
assertNotNull(button, "Button cannot be NULL");
|
||||||
assertNotNull(label, "Label cannot be NULL");
|
assertNotNull(label, "Label cannot be NULL");
|
||||||
memoryZero(button, sizeof(uibutton_t));
|
uiWidgetLabelInit(&button->label, &FONT_DEFAULT);
|
||||||
button->label = label;
|
uiWidgetLabelSetText(&button->label, label);
|
||||||
|
button->highlighted = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool_t uiButtonIsHighlighted(const uibutton_t *button) {
|
bool_t uiButtonIsHighlighted(const uibutton_t *button) {
|
||||||
@@ -26,6 +26,9 @@ bool_t uiButtonIsHighlighted(const uibutton_t *button) {
|
|||||||
void uiButtonSetHighlighted(uibutton_t *button, const bool_t highlighted) {
|
void uiButtonSetHighlighted(uibutton_t *button, const bool_t highlighted) {
|
||||||
assertNotNull(button, "Button cannot be NULL");
|
assertNotNull(button, "Button cannot be NULL");
|
||||||
button->highlighted = highlighted;
|
button->highlighted = highlighted;
|
||||||
|
uiWidgetLabelSetColor(
|
||||||
|
&button->label, highlighted ? COLOR_RED : COLOR_WHITE
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
errorret_t uiButtonDraw(
|
errorret_t uiButtonDraw(
|
||||||
@@ -34,10 +37,6 @@ errorret_t uiButtonDraw(
|
|||||||
const float_t y
|
const float_t y
|
||||||
) {
|
) {
|
||||||
assertNotNull(button, "Button cannot be NULL");
|
assertNotNull(button, "Button cannot be NULL");
|
||||||
errorChain(textDraw(
|
errorChain(uiWidgetLabelDraw(&button->label, x, y));
|
||||||
x, y, button->label,
|
|
||||||
button->highlighted ? COLOR_RED : COLOR_WHITE,
|
|
||||||
&FONT_DEFAULT
|
|
||||||
));
|
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
@@ -7,9 +7,10 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
#include "error/error.h"
|
#include "error/error.h"
|
||||||
|
#include "ui/widget/uiwidgetlabel.h"
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
const char_t *label;
|
uiwidgetlabel_t label;
|
||||||
bool_t highlighted;
|
bool_t highlighted;
|
||||||
} uibutton_t;
|
} uibutton_t;
|
||||||
|
|
||||||
@@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
#include "uicheckbox.h"
|
#include "uicheckbox.h"
|
||||||
#include "util/memory.h"
|
#include "util/memory.h"
|
||||||
|
#include "util/string.h"
|
||||||
#include "display/text/text.h"
|
#include "display/text/text.h"
|
||||||
#include "display/color.h"
|
#include "display/color.h"
|
||||||
|
|
||||||
@@ -15,7 +16,9 @@ void uiCheckboxInit(
|
|||||||
const char_t *label
|
const char_t *label
|
||||||
) {
|
) {
|
||||||
memoryZero(checkbox, sizeof(uicheckbox_t));
|
memoryZero(checkbox, sizeof(uicheckbox_t));
|
||||||
checkbox->label = label;
|
checkbox->rawLabel = label;
|
||||||
|
uiWidgetLabelInit(&checkbox->label, &FONT_DEFAULT);
|
||||||
|
uiCheckboxRebuildLabel(checkbox);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool_t uiCheckboxIsChecked(const uicheckbox_t *checkbox) {
|
bool_t uiCheckboxIsChecked(const uicheckbox_t *checkbox) {
|
||||||
@@ -24,6 +27,7 @@ bool_t uiCheckboxIsChecked(const uicheckbox_t *checkbox) {
|
|||||||
|
|
||||||
void uiCheckboxSetChecked(uicheckbox_t *checkbox, const bool_t checked) {
|
void uiCheckboxSetChecked(uicheckbox_t *checkbox, const bool_t checked) {
|
||||||
checkbox->checked = checked;
|
checkbox->checked = checked;
|
||||||
|
uiCheckboxRebuildLabel(checkbox);
|
||||||
}
|
}
|
||||||
|
|
||||||
void uiCheckboxToggle(uicheckbox_t *checkbox) {
|
void uiCheckboxToggle(uicheckbox_t *checkbox) {
|
||||||
@@ -36,6 +40,9 @@ bool_t uiCheckboxIsHighlighted(const uicheckbox_t *checkbox) {
|
|||||||
|
|
||||||
void uiCheckboxSetHighlighted(uicheckbox_t *checkbox, const bool_t highlighted) {
|
void uiCheckboxSetHighlighted(uicheckbox_t *checkbox, const bool_t highlighted) {
|
||||||
checkbox->highlighted = highlighted;
|
checkbox->highlighted = highlighted;
|
||||||
|
uiWidgetLabelSetColor(
|
||||||
|
&checkbox->label, highlighted ? COLOR_RED : COLOR_WHITE
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
errorret_t uiCheckboxDraw(
|
errorret_t uiCheckboxDraw(
|
||||||
@@ -43,15 +50,15 @@ errorret_t uiCheckboxDraw(
|
|||||||
const float_t x,
|
const float_t x,
|
||||||
const float_t y
|
const float_t y
|
||||||
) {
|
) {
|
||||||
color_t color = checkbox->highlighted ? COLOR_RED : COLOR_WHITE;
|
errorChain(uiWidgetLabelDraw(&checkbox->label, x, y));
|
||||||
const char_t *mark = checkbox->checked ? "Y " : "N ";
|
|
||||||
errorChain(textDraw(x, y, mark, color, &FONT_DEFAULT));
|
|
||||||
|
|
||||||
int32_t markW, markH;
|
|
||||||
textMeasure(mark, &FONT_DEFAULT, &markW, &markH);
|
|
||||||
errorChain(textDraw(
|
|
||||||
x + (float_t)markW, y, checkbox->label, color, &FONT_DEFAULT
|
|
||||||
));
|
|
||||||
|
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void uiCheckboxRebuildLabel(uicheckbox_t *checkbox) {
|
||||||
|
char_t combined[UI_WIDGET_LABEL_TEXT_MAX];
|
||||||
|
stringFormat(
|
||||||
|
combined, UI_WIDGET_LABEL_TEXT_MAX - 1, "%s %s",
|
||||||
|
checkbox->checked ? "Y" : "N", checkbox->rawLabel
|
||||||
|
);
|
||||||
|
uiWidgetLabelSetText(&checkbox->label, combined);
|
||||||
|
}
|
||||||
@@ -7,9 +7,11 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
#include "error/error.h"
|
#include "error/error.h"
|
||||||
|
#include "ui/widget/uiwidgetlabel.h"
|
||||||
|
|
||||||
typedef struct uicheckbox_s {
|
typedef struct uicheckbox_s {
|
||||||
const char_t *label;
|
const char_t *rawLabel;
|
||||||
|
uiwidgetlabel_t label;
|
||||||
bool_t checked;
|
bool_t checked;
|
||||||
bool_t highlighted;
|
bool_t highlighted;
|
||||||
} uicheckbox_t;
|
} uicheckbox_t;
|
||||||
@@ -77,3 +79,12 @@ errorret_t uiCheckboxDraw(
|
|||||||
const float_t x,
|
const float_t x,
|
||||||
const float_t y
|
const float_t y
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rebuilds the checkbox's cached label ("Y "/"N " mark plus rawLabel)
|
||||||
|
* from its current checked state. Called internally whenever checked
|
||||||
|
* changes.
|
||||||
|
*
|
||||||
|
* @param checkbox The checkbox to update.
|
||||||
|
*/
|
||||||
|
void uiCheckboxRebuildLabel(uicheckbox_t *checkbox);
|
||||||
@@ -25,10 +25,13 @@ void uiDropdownInit(
|
|||||||
assertTrue(optionCount > 0, "Dropdown must have at least one option");
|
assertTrue(optionCount > 0, "Dropdown must have at least one option");
|
||||||
|
|
||||||
memoryZero(dropdown, sizeof(uidropdown_t));
|
memoryZero(dropdown, sizeof(uidropdown_t));
|
||||||
dropdown->label = label;
|
uiWidgetLabelInit(&dropdown->label, &FONT_DEFAULT);
|
||||||
|
uiWidgetLabelSetText(&dropdown->label, label);
|
||||||
|
uiWidgetLabelInit(&dropdown->value, &FONT_DEFAULT);
|
||||||
dropdown->options = options;
|
dropdown->options = options;
|
||||||
dropdown->optionCount = optionCount;
|
dropdown->optionCount = optionCount;
|
||||||
dropdown->selectedIndex = selectedIndex < optionCount ? selectedIndex : 0;
|
dropdown->selectedIndex = selectedIndex < optionCount ? selectedIndex : 0;
|
||||||
|
uiDropdownRebuildValue(dropdown);
|
||||||
}
|
}
|
||||||
|
|
||||||
uint8_t uiDropdownGetSelectedIndex(const uidropdown_t *dropdown) {
|
uint8_t uiDropdownGetSelectedIndex(const uidropdown_t *dropdown) {
|
||||||
@@ -45,18 +48,21 @@ void uiDropdownSetSelectedIndex(uidropdown_t *dropdown, const uint8_t index) {
|
|||||||
assertNotNull(dropdown, "Dropdown cannot be NULL");
|
assertNotNull(dropdown, "Dropdown cannot be NULL");
|
||||||
assertTrue(index < dropdown->optionCount, "Dropdown index out of range");
|
assertTrue(index < dropdown->optionCount, "Dropdown index out of range");
|
||||||
dropdown->selectedIndex = index;
|
dropdown->selectedIndex = index;
|
||||||
|
uiDropdownRebuildValue(dropdown);
|
||||||
}
|
}
|
||||||
|
|
||||||
void uiDropdownStepNext(uidropdown_t *dropdown) {
|
void uiDropdownStepNext(uidropdown_t *dropdown) {
|
||||||
assertNotNull(dropdown, "Dropdown cannot be NULL");
|
assertNotNull(dropdown, "Dropdown cannot be NULL");
|
||||||
dropdown->selectedIndex =
|
dropdown->selectedIndex =
|
||||||
(dropdown->selectedIndex + 1) % dropdown->optionCount;
|
(dropdown->selectedIndex + 1) % dropdown->optionCount;
|
||||||
|
uiDropdownRebuildValue(dropdown);
|
||||||
}
|
}
|
||||||
|
|
||||||
void uiDropdownStepPrev(uidropdown_t *dropdown) {
|
void uiDropdownStepPrev(uidropdown_t *dropdown) {
|
||||||
assertNotNull(dropdown, "Dropdown cannot be NULL");
|
assertNotNull(dropdown, "Dropdown cannot be NULL");
|
||||||
dropdown->selectedIndex = dropdown->selectedIndex == 0 ?
|
dropdown->selectedIndex = dropdown->selectedIndex == 0 ?
|
||||||
dropdown->optionCount - 1 : dropdown->selectedIndex - 1;
|
dropdown->optionCount - 1 : dropdown->selectedIndex - 1;
|
||||||
|
uiDropdownRebuildValue(dropdown);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool_t uiDropdownIsHighlighted(const uidropdown_t *dropdown) {
|
bool_t uiDropdownIsHighlighted(const uidropdown_t *dropdown) {
|
||||||
@@ -70,6 +76,9 @@ void uiDropdownSetHighlighted(
|
|||||||
) {
|
) {
|
||||||
assertNotNull(dropdown, "Dropdown cannot be NULL");
|
assertNotNull(dropdown, "Dropdown cannot be NULL");
|
||||||
dropdown->highlighted = highlighted;
|
dropdown->highlighted = highlighted;
|
||||||
|
color_t color = highlighted ? COLOR_RED : COLOR_WHITE;
|
||||||
|
uiWidgetLabelSetColor(&dropdown->label, color);
|
||||||
|
uiWidgetLabelSetColor(&dropdown->value, color);
|
||||||
}
|
}
|
||||||
|
|
||||||
errorret_t uiDropdownDraw(
|
errorret_t uiDropdownDraw(
|
||||||
@@ -79,22 +88,23 @@ errorret_t uiDropdownDraw(
|
|||||||
) {
|
) {
|
||||||
assertNotNull(dropdown, "Dropdown cannot be NULL");
|
assertNotNull(dropdown, "Dropdown cannot be NULL");
|
||||||
|
|
||||||
color_t color = dropdown->highlighted ? COLOR_RED : COLOR_WHITE;
|
errorChain(uiWidgetLabelDraw(&dropdown->label, x, y));
|
||||||
|
|
||||||
errorChain(textDraw(x, y, dropdown->label, color, &FONT_DEFAULT));
|
|
||||||
|
|
||||||
int32_t labelW, labelH;
|
int32_t labelW, labelH;
|
||||||
textMeasure(dropdown->label, &FONT_DEFAULT, &labelW, &labelH);
|
uiWidgetLabelGetSize(&dropdown->label, &labelW, &labelH);
|
||||||
|
|
||||||
char_t valueText[UI_DROPDOWN_VALUE_TEXT_MAX];
|
errorChain(uiWidgetLabelDraw(
|
||||||
stringFormat(
|
&dropdown->value, x + (float_t)labelW + UI_DROPDOWN_GAP, y
|
||||||
valueText, UI_DROPDOWN_VALUE_TEXT_MAX - 1, "< %s >",
|
|
||||||
uiDropdownGetSelectedOption(dropdown)
|
|
||||||
);
|
|
||||||
|
|
||||||
errorChain(textDraw(
|
|
||||||
x + (float_t)labelW + UI_DROPDOWN_GAP, y, valueText, color, &FONT_DEFAULT
|
|
||||||
));
|
));
|
||||||
|
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void uiDropdownRebuildValue(uidropdown_t *dropdown) {
|
||||||
|
char_t valueText[UI_WIDGET_LABEL_TEXT_MAX];
|
||||||
|
stringFormat(
|
||||||
|
valueText, UI_WIDGET_LABEL_TEXT_MAX - 1, "< %s >",
|
||||||
|
uiDropdownGetSelectedOption(dropdown)
|
||||||
|
);
|
||||||
|
uiWidgetLabelSetText(&dropdown->value, valueText);
|
||||||
|
}
|
||||||
@@ -7,12 +7,13 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
#include "error/error.h"
|
#include "error/error.h"
|
||||||
|
#include "ui/widget/uiwidgetlabel.h"
|
||||||
|
|
||||||
#define UI_DROPDOWN_GAP 4.0f
|
#define UI_DROPDOWN_GAP 4.0f
|
||||||
#define UI_DROPDOWN_VALUE_TEXT_MAX 64
|
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
const char_t *label;
|
uiwidgetlabel_t label;
|
||||||
|
uiwidgetlabel_t value;
|
||||||
const char_t *const *options;
|
const char_t *const *options;
|
||||||
uint8_t optionCount;
|
uint8_t optionCount;
|
||||||
uint8_t selectedIndex;
|
uint8_t selectedIndex;
|
||||||
@@ -111,3 +112,12 @@ errorret_t uiDropdownDraw(
|
|||||||
const float_t x,
|
const float_t x,
|
||||||
const float_t y
|
const float_t y
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rebuilds the dropdown's cached value label ("< option >") from its
|
||||||
|
* current selectedIndex. Called internally whenever selectedIndex
|
||||||
|
* changes.
|
||||||
|
*
|
||||||
|
* @param dropdown The dropdown to update.
|
||||||
|
*/
|
||||||
|
void uiDropdownRebuildValue(uidropdown_t *dropdown);
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "uilabel.h"
|
||||||
|
#include "assert/assert.h"
|
||||||
|
#include "util/memory.h"
|
||||||
|
#include "util/string.h"
|
||||||
|
#include "display/text/text.h"
|
||||||
|
|
||||||
|
void uiLabelInit(uilabel_t *label, font_t *font) {
|
||||||
|
assertNotNull(label, "Label cannot be NULL");
|
||||||
|
assertNotNull(font, "Font cannot be NULL");
|
||||||
|
|
||||||
|
memoryZero(label, sizeof(uilabel_t));
|
||||||
|
label->font = font;
|
||||||
|
label->color = COLOR_WHITE;
|
||||||
|
}
|
||||||
|
|
||||||
|
void uiLabelSetText(uilabel_t *label, const char_t *text) {
|
||||||
|
assertNotNull(label, "Label cannot be NULL");
|
||||||
|
assertNotNull(text, "Text cannot be NULL");
|
||||||
|
assertStrLenMax(text, UI_LABEL_TEXT_MAX, "Label text too long");
|
||||||
|
|
||||||
|
stringCopy(label->text, text, UI_LABEL_TEXT_MAX);
|
||||||
|
label->spriteCount = textBuildSpriteCache(
|
||||||
|
label->text, label->font, label->sprites, UI_LABEL_SPRITE_COUNT_MAX,
|
||||||
|
&label->width, &label->height
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void uiLabelSetColor(uilabel_t *label, const color_t color) {
|
||||||
|
assertNotNull(label, "Label cannot be NULL");
|
||||||
|
label->color = color;
|
||||||
|
}
|
||||||
|
|
||||||
|
void uiLabelGetSize(
|
||||||
|
const uilabel_t *label,
|
||||||
|
int32_t *outWidth,
|
||||||
|
int32_t *outHeight
|
||||||
|
) {
|
||||||
|
assertNotNull(label, "Label cannot be NULL");
|
||||||
|
assertNotNull(outWidth, "Output width cannot be NULL");
|
||||||
|
assertNotNull(outHeight, "Output height cannot be NULL");
|
||||||
|
*outWidth = label->width;
|
||||||
|
*outHeight = label->height;
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t uiLabelDraw(
|
||||||
|
const uilabel_t *label,
|
||||||
|
const float_t x,
|
||||||
|
const float_t y
|
||||||
|
) {
|
||||||
|
assertNotNull(label, "Label cannot be NULL");
|
||||||
|
if(label->spriteCount == 0) errorOk();
|
||||||
|
|
||||||
|
// Cached sprites are relative to (0,0); textDrawSpriteCache translates
|
||||||
|
// into the requested screen position here instead of in
|
||||||
|
// uiLabelSetText, so a label can be repositioned every frame without
|
||||||
|
// rebuilding the (much more expensive) glyph/UV cache.
|
||||||
|
spritebatchsprite_t scratch[UI_LABEL_SPRITE_COUNT_MAX];
|
||||||
|
errorChain(textDrawSpriteCache(
|
||||||
|
label->sprites, label->spriteCount, scratch, x, y, label->color,
|
||||||
|
label->font->texture
|
||||||
|
));
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include "error/error.h"
|
||||||
|
#include "display/text/font.h"
|
||||||
|
#include "display/spritebatch/spritebatchsprite.h"
|
||||||
|
#include "display/color.h"
|
||||||
|
|
||||||
|
#define UI_LABEL_TEXT_MAX 256
|
||||||
|
#define UI_LABEL_SPRITE_COUNT_MAX UI_LABEL_TEXT_MAX
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
char_t text[UI_LABEL_TEXT_MAX];
|
||||||
|
color_t color;
|
||||||
|
font_t *font;
|
||||||
|
|
||||||
|
spritebatchsprite_t sprites[UI_LABEL_SPRITE_COUNT_MAX];
|
||||||
|
uint32_t spriteCount;
|
||||||
|
int32_t width;
|
||||||
|
int32_t height;
|
||||||
|
} uilabel_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes a label, defaulting to white text and no text set.
|
||||||
|
*
|
||||||
|
* @param label The label to initialize.
|
||||||
|
* @param font Font to use for rendering. Must outlive the label.
|
||||||
|
*/
|
||||||
|
void uiLabelInit(uilabel_t *label, font_t *font);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the label's text, rebuilding its cached sprites (glyph lookup, UV,
|
||||||
|
* and layout) immediately. This is the only way the sprite cache gets
|
||||||
|
* rebuilt -- uiLabelDraw never recomputes it, so call this whenever the
|
||||||
|
* text changes rather than once up front and expecting it to stay in sync.
|
||||||
|
*
|
||||||
|
* @param label The label to update.
|
||||||
|
* @param text Null-terminated string to display. Must be shorter than
|
||||||
|
* UI_LABEL_TEXT_MAX.
|
||||||
|
*/
|
||||||
|
void uiLabelSetText(uilabel_t *label, const char_t *text);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the label's tint color. Cheap -- doesn't touch the sprite cache,
|
||||||
|
* since color is applied via the draw material, not baked into sprites.
|
||||||
|
*
|
||||||
|
* @param label The label to update.
|
||||||
|
* @param color The new tint color.
|
||||||
|
*/
|
||||||
|
void uiLabelSetColor(uilabel_t *label, const color_t color);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the measured size (in pixels) of the label's current text, cached
|
||||||
|
* from the last uiLabelSetText call.
|
||||||
|
*
|
||||||
|
* @param label The label to query.
|
||||||
|
* @param outWidth Pointer to store the width.
|
||||||
|
* @param outHeight Pointer to store the height.
|
||||||
|
*/
|
||||||
|
void uiLabelGetSize(
|
||||||
|
const uilabel_t *label,
|
||||||
|
int32_t *outWidth,
|
||||||
|
int32_t *outHeight
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draws the label's cached sprites at the given screen position in a
|
||||||
|
* single batched buffer call. Does not recompute glyph layout -- call
|
||||||
|
* uiLabelSetText first whenever the text changes.
|
||||||
|
*
|
||||||
|
* @param label The label to draw.
|
||||||
|
* @param x Screen x position.
|
||||||
|
* @param y Screen y position.
|
||||||
|
* @return Any error that occurs.
|
||||||
|
*/
|
||||||
|
errorret_t uiLabelDraw(
|
||||||
|
const uilabel_t *label,
|
||||||
|
const float_t x,
|
||||||
|
const float_t y
|
||||||
|
);
|
||||||
@@ -10,6 +10,7 @@
|
|||||||
#include "util/memory.h"
|
#include "util/memory.h"
|
||||||
#include "display/text/text.h"
|
#include "display/text/text.h"
|
||||||
#include "display/color.h"
|
#include "display/color.h"
|
||||||
|
#include "ui/widget/uiwidgetlabel.h"
|
||||||
#include "ui/widget/uibutton.h"
|
#include "ui/widget/uibutton.h"
|
||||||
#include "ui/widget/uitab.h"
|
#include "ui/widget/uitab.h"
|
||||||
#include "ui/widget/uislider.h"
|
#include "ui/widget/uislider.h"
|
||||||
@@ -102,12 +103,8 @@ errorret_t uiMenuDraw(
|
|||||||
|
|
||||||
if(item->type == UI_MENU_WIDGET_TYPE_LABEL) {
|
if(item->type == UI_MENU_WIDGET_TYPE_LABEL) {
|
||||||
if(col > 0) { row++; col = 0; }
|
if(col > 0) { row++; col = 0; }
|
||||||
errorChain(textDraw(
|
errorChain(uiWidgetLabelDraw(
|
||||||
x,
|
&item->label, x, y + (float_t)row * rowHeight
|
||||||
y + (float_t)row * rowHeight,
|
|
||||||
item->label,
|
|
||||||
COLOR_WHITE,
|
|
||||||
&FONT_DEFAULT
|
|
||||||
));
|
));
|
||||||
row++;
|
row++;
|
||||||
continue;
|
continue;
|
||||||
@@ -274,6 +271,13 @@ uint8_t uiMenuFocusSlotToIndex(const uimenu_t *menu, const uint8_t slot) {
|
|||||||
return 0xFF;
|
return 0xFF;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void uiMenuLabelInit(uimenuitem_t *item, const char_t *text) {
|
||||||
|
assertNotNull(item, "Item cannot be NULL");
|
||||||
|
assertNotNull(text, "Text cannot be NULL");
|
||||||
|
uiWidgetLabelInit(&item->label, &FONT_DEFAULT);
|
||||||
|
uiWidgetLabelSetText(&item->label, text);
|
||||||
|
}
|
||||||
|
|
||||||
void uiMenuItemSetHighlighted(uimenuitem_t *item, const bool_t highlighted) {
|
void uiMenuItemSetHighlighted(uimenuitem_t *item, const bool_t highlighted) {
|
||||||
if(item->type == UI_MENU_WIDGET_TYPE_CHECKBOX) {
|
if(item->type == UI_MENU_WIDGET_TYPE_CHECKBOX) {
|
||||||
uiCheckboxSetHighlighted(&item->checkbox, highlighted);
|
uiCheckboxSetHighlighted(&item->checkbox, highlighted);
|
||||||
@@ -8,14 +8,13 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include "error/error.h"
|
#include "error/error.h"
|
||||||
#include "ui/focus/uifocus.h"
|
#include "ui/focus/uifocus.h"
|
||||||
|
#include "ui/widget/uiwidgetlabel.h"
|
||||||
#include "ui/widget/uibutton.h"
|
#include "ui/widget/uibutton.h"
|
||||||
#include "ui/widget/uicheckbox.h"
|
#include "ui/widget/uicheckbox.h"
|
||||||
#include "ui/widget/uitab.h"
|
#include "ui/widget/uitab.h"
|
||||||
#include "ui/widget/uislider.h"
|
#include "ui/widget/uislider.h"
|
||||||
#include "ui/widget/uidropdown.h"
|
#include "ui/widget/uidropdown.h"
|
||||||
|
|
||||||
#define UI_MENU_LABEL_MAX UI_CHECKBOX_LABEL_MAX
|
|
||||||
|
|
||||||
typedef struct uimenu_s uimenu_t;
|
typedef struct uimenu_s uimenu_t;
|
||||||
|
|
||||||
typedef enum {
|
typedef enum {
|
||||||
@@ -32,7 +31,7 @@ typedef enum {
|
|||||||
typedef struct {
|
typedef struct {
|
||||||
uimenuwidgettype_t type;
|
uimenuwidgettype_t type;
|
||||||
union {
|
union {
|
||||||
char_t *label;
|
uiwidgetlabel_t label;
|
||||||
uicheckbox_t checkbox;
|
uicheckbox_t checkbox;
|
||||||
uibutton_t button;
|
uibutton_t button;
|
||||||
uitab_t tab;
|
uitab_t tab;
|
||||||
@@ -178,6 +177,15 @@ uint8_t uiMenuFocusSlotToIndex(const uimenu_t *menu, const uint8_t slot);
|
|||||||
*/
|
*/
|
||||||
void uiMenuItemSetHighlighted(uimenuitem_t *item, const bool_t highlighted);
|
void uiMenuItemSetHighlighted(uimenuitem_t *item, const bool_t highlighted);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes a menu item as a standalone label, caching its text.
|
||||||
|
* Used internally by the MENU_LABEL helper macro.
|
||||||
|
*
|
||||||
|
* @param item The item to initialize.
|
||||||
|
* @param text Display text.
|
||||||
|
*/
|
||||||
|
void uiMenuLabelInit(uimenuitem_t *item, const char_t *text);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal focus callback - forwards selection to the menu's selected handler.
|
* Internal focus callback - forwards selection to the menu's selected handler.
|
||||||
*
|
*
|
||||||
@@ -227,7 +235,7 @@ bool_t uiMenuFocusDirection(
|
|||||||
#define MENU_LABEL(text) \
|
#define MENU_LABEL(text) \
|
||||||
assertTrue(menuIndex < menuCapacity, "Menu item count exceeds capacity"); \
|
assertTrue(menuIndex < menuCapacity, "Menu item count exceeds capacity"); \
|
||||||
menu->items[menuIndex].type = UI_MENU_WIDGET_TYPE_LABEL; \
|
menu->items[menuIndex].type = UI_MENU_WIDGET_TYPE_LABEL; \
|
||||||
menu->items[menuIndex].label = text; \
|
uiMenuLabelInit(&menu->items[menuIndex], text); \
|
||||||
++menuIndex
|
++menuIndex
|
||||||
|
|
||||||
#define MENU_SPACER() \
|
#define MENU_SPACER() \
|
||||||
@@ -30,12 +30,16 @@ void uiSliderInitFloat(
|
|||||||
assertTrue(step > 0.0f, "Slider step must be greater than zero");
|
assertTrue(step > 0.0f, "Slider step must be greater than zero");
|
||||||
|
|
||||||
memoryZero(slider, sizeof(uislider_t));
|
memoryZero(slider, sizeof(uislider_t));
|
||||||
slider->label = label;
|
uiWidgetLabelInit(&slider->label, &FONT_DEFAULT);
|
||||||
|
uiWidgetLabelSetText(&slider->label, label);
|
||||||
|
uiWidgetLabelInit(&slider->valueLabel, &FONT_DEFAULT);
|
||||||
slider->type = UI_SLIDER_TYPE_FLOAT;
|
slider->type = UI_SLIDER_TYPE_FLOAT;
|
||||||
slider->min.f = min;
|
slider->min.f = min;
|
||||||
slider->max.f = max;
|
slider->max.f = max;
|
||||||
slider->step.f = step;
|
slider->step.f = step;
|
||||||
slider->value.f = mathClamp(value, min, max);
|
slider->value.f = mathClamp(value, min, max);
|
||||||
|
uiSliderRebuildValueLabel(slider);
|
||||||
|
uiSliderRebuildGeometry(slider);
|
||||||
}
|
}
|
||||||
|
|
||||||
void uiSliderInitInt(
|
void uiSliderInitInt(
|
||||||
@@ -52,12 +56,16 @@ void uiSliderInitInt(
|
|||||||
assertTrue(step > 0, "Slider step must be greater than zero");
|
assertTrue(step > 0, "Slider step must be greater than zero");
|
||||||
|
|
||||||
memoryZero(slider, sizeof(uislider_t));
|
memoryZero(slider, sizeof(uislider_t));
|
||||||
slider->label = label;
|
uiWidgetLabelInit(&slider->label, &FONT_DEFAULT);
|
||||||
|
uiWidgetLabelSetText(&slider->label, label);
|
||||||
|
uiWidgetLabelInit(&slider->valueLabel, &FONT_DEFAULT);
|
||||||
slider->type = UI_SLIDER_TYPE_INT;
|
slider->type = UI_SLIDER_TYPE_INT;
|
||||||
slider->min.i = min;
|
slider->min.i = min;
|
||||||
slider->max.i = max;
|
slider->max.i = max;
|
||||||
slider->step.i = step;
|
slider->step.i = step;
|
||||||
slider->value.i = mathClamp(value, min, max);
|
slider->value.i = mathClamp(value, min, max);
|
||||||
|
uiSliderRebuildValueLabel(slider);
|
||||||
|
uiSliderRebuildGeometry(slider);
|
||||||
}
|
}
|
||||||
|
|
||||||
float_t uiSliderGetFloat(const uislider_t *slider) {
|
float_t uiSliderGetFloat(const uislider_t *slider) {
|
||||||
@@ -80,6 +88,8 @@ void uiSliderSetFloat(uislider_t *slider, const float_t value) {
|
|||||||
slider->type == UI_SLIDER_TYPE_FLOAT, "Slider is not a float slider"
|
slider->type == UI_SLIDER_TYPE_FLOAT, "Slider is not a float slider"
|
||||||
);
|
);
|
||||||
slider->value.f = mathClamp(value, slider->min.f, slider->max.f);
|
slider->value.f = mathClamp(value, slider->min.f, slider->max.f);
|
||||||
|
uiSliderRebuildValueLabel(slider);
|
||||||
|
uiSliderRebuildGeometry(slider);
|
||||||
}
|
}
|
||||||
|
|
||||||
void uiSliderSetInt(uislider_t *slider, const int32_t value) {
|
void uiSliderSetInt(uislider_t *slider, const int32_t value) {
|
||||||
@@ -88,6 +98,8 @@ void uiSliderSetInt(uislider_t *slider, const int32_t value) {
|
|||||||
slider->type == UI_SLIDER_TYPE_INT, "Slider is not an int slider"
|
slider->type == UI_SLIDER_TYPE_INT, "Slider is not an int slider"
|
||||||
);
|
);
|
||||||
slider->value.i = mathClamp(value, slider->min.i, slider->max.i);
|
slider->value.i = mathClamp(value, slider->min.i, slider->max.i);
|
||||||
|
uiSliderRebuildValueLabel(slider);
|
||||||
|
uiSliderRebuildGeometry(slider);
|
||||||
}
|
}
|
||||||
|
|
||||||
void uiSliderStepUp(uislider_t *slider) {
|
void uiSliderStepUp(uislider_t *slider) {
|
||||||
@@ -138,6 +150,9 @@ bool_t uiSliderIsHighlighted(const uislider_t *slider) {
|
|||||||
void uiSliderSetHighlighted(uislider_t *slider, const bool_t highlighted) {
|
void uiSliderSetHighlighted(uislider_t *slider, const bool_t highlighted) {
|
||||||
assertNotNull(slider, "Slider cannot be NULL");
|
assertNotNull(slider, "Slider cannot be NULL");
|
||||||
slider->highlighted = highlighted;
|
slider->highlighted = highlighted;
|
||||||
|
color_t color = highlighted ? COLOR_RED : COLOR_WHITE;
|
||||||
|
uiWidgetLabelSetColor(&slider->label, color);
|
||||||
|
uiWidgetLabelSetColor(&slider->valueLabel, color);
|
||||||
}
|
}
|
||||||
|
|
||||||
errorret_t uiSliderDraw(
|
errorret_t uiSliderDraw(
|
||||||
@@ -147,47 +162,97 @@ errorret_t uiSliderDraw(
|
|||||||
) {
|
) {
|
||||||
assertNotNull(slider, "Slider cannot be NULL");
|
assertNotNull(slider, "Slider cannot be NULL");
|
||||||
|
|
||||||
color_t color = slider->highlighted ? COLOR_RED : COLOR_WHITE;
|
errorChain(uiWidgetLabelDraw(&slider->label, x, y));
|
||||||
|
|
||||||
errorChain(textDraw(x, y, slider->label, color, &FONT_DEFAULT));
|
|
||||||
|
|
||||||
int32_t labelW, labelH;
|
|
||||||
textMeasure(slider->label, &FONT_DEFAULT, &labelW, &labelH);
|
|
||||||
|
|
||||||
float_t trackX = x + (float_t)labelW + UI_SLIDER_GAP;
|
|
||||||
float_t trackY = y + ((float_t)labelH - UI_SLIDER_TRACK_HEIGHT) * 0.5f;
|
|
||||||
|
|
||||||
spritebatchsprite_t trackSprite = {
|
|
||||||
.min = { trackX, trackY, 0.0f },
|
|
||||||
.max = { trackX + UI_SLIDER_TRACK_WIDTH, trackY + UI_SLIDER_TRACK_HEIGHT, 0.0f },
|
|
||||||
.uvMin = { 0.0f, 0.0f },
|
|
||||||
.uvMax = { 1.0f, 1.0f }
|
|
||||||
};
|
|
||||||
shadermaterial_t trackMaterial = {
|
shadermaterial_t trackMaterial = {
|
||||||
.unlit = {
|
.unlit = {
|
||||||
.color = COLOR_DARK_GRAY,
|
.color = COLOR_DARK_GRAY,
|
||||||
.texture = &TEXTURE_WHITE
|
.texture = &TEXTURE_WHITE
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
errorChain(spriteBatchBuffer(&trackSprite, 1, &SHADER_UNLIT, trackMaterial));
|
spritebatchsprite_t track = spriteBatchSpriteTranslate(
|
||||||
errorChain(spriteBatchFlush());
|
&slider->cachedTrack, x, y
|
||||||
|
);
|
||||||
|
errorChain(spriteBatchBuffer(&track, 1, &SHADER_UNLIT, trackMaterial));
|
||||||
|
|
||||||
|
if(slider->cachedFillVisible) {
|
||||||
|
shadermaterial_t fillMaterial = {
|
||||||
|
.unlit = {
|
||||||
|
.color = slider->highlighted ? COLOR_RED : COLOR_WHITE,
|
||||||
|
.texture = &TEXTURE_WHITE
|
||||||
|
}
|
||||||
|
};
|
||||||
|
spritebatchsprite_t fill = spriteBatchSpriteTranslate(
|
||||||
|
&slider->cachedFill, x, y
|
||||||
|
);
|
||||||
|
errorChain(spriteBatchBuffer(&fill, 1, &SHADER_UNLIT, fillMaterial));
|
||||||
|
}
|
||||||
|
|
||||||
|
if(slider->cachedMarkerCount > 0) {
|
||||||
|
spritebatchsprite_t markers[UI_SLIDER_STEP_MARKERS_MAX];
|
||||||
|
for(int32_t i = 0; i < slider->cachedMarkerCount; i++) {
|
||||||
|
markers[i] = spriteBatchSpriteTranslate(&slider->cachedMarkers[i], x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
shadermaterial_t markerMaterial = {
|
||||||
|
.unlit = {
|
||||||
|
.color = COLOR_WHITE,
|
||||||
|
.texture = &TEXTURE_WHITE
|
||||||
|
}
|
||||||
|
};
|
||||||
|
errorChain(spriteBatchBuffer(
|
||||||
|
markers, slider->cachedMarkerCount, &SHADER_UNLIT, markerMaterial
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
errorChain(uiWidgetLabelDraw(
|
||||||
|
&slider->valueLabel,
|
||||||
|
x + slider->cachedTrack.max[0] + UI_SLIDER_GAP,
|
||||||
|
y
|
||||||
|
));
|
||||||
|
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
void uiSliderRebuildValueLabel(uislider_t *slider) {
|
||||||
|
char_t valueText[UI_WIDGET_LABEL_TEXT_MAX];
|
||||||
|
if(slider->type == UI_SLIDER_TYPE_INT) {
|
||||||
|
stringFormat(
|
||||||
|
valueText, UI_WIDGET_LABEL_TEXT_MAX - 1, "%d", slider->value.i
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
stringFormat(
|
||||||
|
valueText, UI_WIDGET_LABEL_TEXT_MAX - 1, "%.2f", slider->value.f
|
||||||
|
);
|
||||||
|
}
|
||||||
|
uiWidgetLabelSetText(&slider->valueLabel, valueText);
|
||||||
|
}
|
||||||
|
|
||||||
|
void uiSliderRebuildGeometry(uislider_t *slider) {
|
||||||
|
int32_t labelW, labelH;
|
||||||
|
uiWidgetLabelGetSize(&slider->label, &labelW, &labelH);
|
||||||
|
|
||||||
|
float_t trackX = (float_t)labelW + UI_SLIDER_GAP;
|
||||||
|
float_t trackY = ((float_t)labelH - UI_SLIDER_TRACK_HEIGHT) * 0.5f;
|
||||||
|
|
||||||
|
slider->cachedTrack = (spritebatchsprite_t){
|
||||||
|
.min = { trackX, trackY, 0.0f },
|
||||||
|
.max = {
|
||||||
|
trackX + UI_SLIDER_TRACK_WIDTH, trackY + UI_SLIDER_TRACK_HEIGHT, 0.0f
|
||||||
|
},
|
||||||
|
.uvMin = { 0.0f, 0.0f },
|
||||||
|
.uvMax = { 1.0f, 1.0f }
|
||||||
|
};
|
||||||
|
|
||||||
float_t fillWidth = UI_SLIDER_TRACK_WIDTH * uiSliderGetRatio(slider);
|
float_t fillWidth = UI_SLIDER_TRACK_WIDTH * uiSliderGetRatio(slider);
|
||||||
if(fillWidth > 0.0f) {
|
slider->cachedFillVisible = fillWidth > 0.0f;
|
||||||
spritebatchsprite_t fillSprite = {
|
if(slider->cachedFillVisible) {
|
||||||
|
slider->cachedFill = (spritebatchsprite_t){
|
||||||
.min = { trackX, trackY, 0.0f },
|
.min = { trackX, trackY, 0.0f },
|
||||||
.max = { trackX + fillWidth, trackY + UI_SLIDER_TRACK_HEIGHT, 0.0f },
|
.max = { trackX + fillWidth, trackY + UI_SLIDER_TRACK_HEIGHT, 0.0f },
|
||||||
.uvMin = { 0.0f, 0.0f },
|
.uvMin = { 0.0f, 0.0f },
|
||||||
.uvMax = { 1.0f, 1.0f }
|
.uvMax = { 1.0f, 1.0f }
|
||||||
};
|
};
|
||||||
shadermaterial_t fillMaterial = {
|
|
||||||
.unlit = {
|
|
||||||
.color = color,
|
|
||||||
.texture = &TEXTURE_WHITE
|
|
||||||
}
|
|
||||||
};
|
|
||||||
errorChain(spriteBatchBuffer(&fillSprite, 1, &SHADER_UNLIT, fillMaterial));
|
|
||||||
errorChain(spriteBatchFlush());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int32_t stepCount = uiSliderGetStepCount(slider);
|
int32_t stepCount = uiSliderGetStepCount(slider);
|
||||||
@@ -195,7 +260,7 @@ errorret_t uiSliderDraw(
|
|||||||
for(int32_t i = 0; i <= stepCount; i++) {
|
for(int32_t i = 0; i <= stepCount; i++) {
|
||||||
float_t markerX = trackX +
|
float_t markerX = trackX +
|
||||||
UI_SLIDER_TRACK_WIDTH * (float_t)i / (float_t)stepCount;
|
UI_SLIDER_TRACK_WIDTH * (float_t)i / (float_t)stepCount;
|
||||||
spritebatchsprite_t markerSprite = {
|
slider->cachedMarkers[i] = (spritebatchsprite_t){
|
||||||
.min = {
|
.min = {
|
||||||
markerX - UI_SLIDER_STEP_MARKER_WIDTH * 0.5f,
|
markerX - UI_SLIDER_STEP_MARKER_WIDTH * 0.5f,
|
||||||
trackY - UI_SLIDER_STEP_MARKER_OVERHANG,
|
trackY - UI_SLIDER_STEP_MARKER_OVERHANG,
|
||||||
@@ -209,34 +274,9 @@ errorret_t uiSliderDraw(
|
|||||||
.uvMin = { 0.0f, 0.0f },
|
.uvMin = { 0.0f, 0.0f },
|
||||||
.uvMax = { 1.0f, 1.0f }
|
.uvMax = { 1.0f, 1.0f }
|
||||||
};
|
};
|
||||||
shadermaterial_t markerMaterial = {
|
|
||||||
.unlit = {
|
|
||||||
.color = COLOR_WHITE,
|
|
||||||
.texture = &TEXTURE_WHITE
|
|
||||||
}
|
}
|
||||||
};
|
slider->cachedMarkerCount = stepCount + 1;
|
||||||
errorChain(
|
|
||||||
spriteBatchBuffer(&markerSprite, 1, &SHADER_UNLIT, markerMaterial)
|
|
||||||
);
|
|
||||||
errorChain(spriteBatchFlush());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
char_t valueText[UI_SLIDER_VALUE_TEXT_MAX];
|
|
||||||
if(slider->type == UI_SLIDER_TYPE_INT) {
|
|
||||||
stringFormat(
|
|
||||||
valueText, UI_SLIDER_VALUE_TEXT_MAX - 1, "%d", slider->value.i
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
stringFormat(
|
slider->cachedMarkerCount = 0;
|
||||||
valueText, UI_SLIDER_VALUE_TEXT_MAX - 1, "%.2f", slider->value.f
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
errorChain(textDraw(
|
|
||||||
trackX + UI_SLIDER_TRACK_WIDTH + UI_SLIDER_GAP, y,
|
|
||||||
valueText, color, &FONT_DEFAULT
|
|
||||||
));
|
|
||||||
|
|
||||||
errorOk();
|
|
||||||
}
|
}
|
||||||
@@ -7,13 +7,14 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
#include "error/error.h"
|
#include "error/error.h"
|
||||||
|
#include "ui/widget/uiwidgetlabel.h"
|
||||||
|
#include "display/spritebatch/spritebatchsprite.h"
|
||||||
|
|
||||||
#define UI_SLIDER_TRACK_WIDTH 80.0f
|
#define UI_SLIDER_TRACK_WIDTH 80.0f
|
||||||
#define UI_SLIDER_TRACK_HEIGHT 4.0f
|
#define UI_SLIDER_TRACK_HEIGHT 4.0f
|
||||||
#define UI_SLIDER_STEP_MARKER_WIDTH 2.0f
|
#define UI_SLIDER_STEP_MARKER_WIDTH 2.0f
|
||||||
#define UI_SLIDER_STEP_MARKER_OVERHANG 1.0f
|
#define UI_SLIDER_STEP_MARKER_OVERHANG 1.0f
|
||||||
#define UI_SLIDER_GAP 4.0f
|
#define UI_SLIDER_GAP 4.0f
|
||||||
#define UI_SLIDER_VALUE_TEXT_MAX 16
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Number of discrete steps below which an int slider renders individual
|
* Number of discrete steps below which an int slider renders individual
|
||||||
@@ -34,13 +35,24 @@ typedef union {
|
|||||||
} uislidervalue_t;
|
} uislidervalue_t;
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
const char_t *label;
|
uiwidgetlabel_t label;
|
||||||
|
uiwidgetlabel_t valueLabel;
|
||||||
uislidertype_t type;
|
uislidertype_t type;
|
||||||
uislidervalue_t value;
|
uislidervalue_t value;
|
||||||
uislidervalue_t min;
|
uislidervalue_t min;
|
||||||
uislidervalue_t max;
|
uislidervalue_t max;
|
||||||
uislidervalue_t step;
|
uislidervalue_t step;
|
||||||
bool_t highlighted;
|
bool_t highlighted;
|
||||||
|
|
||||||
|
// Track/fill/marker geometry, cached relative to origin (0,0) and only
|
||||||
|
// rebuilt when the value or step configuration changes (see
|
||||||
|
// uiSliderRebuildGeometry) -- uiSliderDraw just translates these into
|
||||||
|
// position instead of re-deriving them every frame.
|
||||||
|
spritebatchsprite_t cachedTrack;
|
||||||
|
spritebatchsprite_t cachedFill;
|
||||||
|
bool_t cachedFillVisible;
|
||||||
|
spritebatchsprite_t cachedMarkers[UI_SLIDER_STEP_MARKERS_MAX];
|
||||||
|
int32_t cachedMarkerCount;
|
||||||
} uislider_t;
|
} uislider_t;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -183,3 +195,21 @@ errorret_t uiSliderDraw(
|
|||||||
const float_t x,
|
const float_t x,
|
||||||
const float_t y
|
const float_t y
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rebuilds the slider's cached value label from its current value.
|
||||||
|
* Called internally whenever value changes.
|
||||||
|
*
|
||||||
|
* @param slider The slider to update.
|
||||||
|
*/
|
||||||
|
void uiSliderRebuildValueLabel(uislider_t *slider);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rebuilds the slider's cached track/fill/marker geometry (relative to
|
||||||
|
* origin (0,0)) from its current label size, value, and step
|
||||||
|
* configuration. Called internally on init and whenever the value
|
||||||
|
* changes -- uiSliderDraw never recomputes this itself.
|
||||||
|
*
|
||||||
|
* @param slider The slider to update.
|
||||||
|
*/
|
||||||
|
void uiSliderRebuildGeometry(uislider_t *slider);
|
||||||
@@ -7,7 +7,6 @@
|
|||||||
|
|
||||||
#include "uitab.h"
|
#include "uitab.h"
|
||||||
#include "assert/assert.h"
|
#include "assert/assert.h"
|
||||||
#include "util/memory.h"
|
|
||||||
#include "display/text/text.h"
|
#include "display/text/text.h"
|
||||||
#include "display/color.h"
|
#include "display/color.h"
|
||||||
#include "display/spritebatch/spritebatch.h"
|
#include "display/spritebatch/spritebatch.h"
|
||||||
@@ -17,8 +16,10 @@
|
|||||||
void uiTabInit(uitab_t *tab, const char_t *label) {
|
void uiTabInit(uitab_t *tab, const char_t *label) {
|
||||||
assertNotNull(tab, "Tab cannot be NULL");
|
assertNotNull(tab, "Tab cannot be NULL");
|
||||||
assertNotNull(label, "Label cannot be NULL");
|
assertNotNull(label, "Label cannot be NULL");
|
||||||
memoryZero(tab, sizeof(uitab_t));
|
uiWidgetLabelInit(&tab->label, &FONT_DEFAULT);
|
||||||
tab->label = label;
|
uiWidgetLabelSetText(&tab->label, label);
|
||||||
|
tab->active = false;
|
||||||
|
uiTabRebuildBackground(tab);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool_t uiTabIsActive(const uitab_t *tab) {
|
bool_t uiTabIsActive(const uitab_t *tab) {
|
||||||
@@ -38,15 +39,9 @@ errorret_t uiTabDraw(
|
|||||||
) {
|
) {
|
||||||
assertNotNull(tab, "Tab cannot be NULL");
|
assertNotNull(tab, "Tab cannot be NULL");
|
||||||
|
|
||||||
int32_t labelW, labelH;
|
spritebatchsprite_t sprite = spriteBatchSpriteTranslate(
|
||||||
textMeasure(tab->label, &FONT_DEFAULT, &labelW, &labelH);
|
&tab->cachedBackground, x, y
|
||||||
|
);
|
||||||
spritebatchsprite_t sprite = {
|
|
||||||
.min = { x, y, 0.0f },
|
|
||||||
.max = { x + (float_t)labelW, y + (float_t)labelH, 0.0f },
|
|
||||||
.uvMin = { 0.0f, 0.0f },
|
|
||||||
.uvMax = { 1.0f, 1.0f }
|
|
||||||
};
|
|
||||||
shadermaterial_t material = {
|
shadermaterial_t material = {
|
||||||
.unlit = {
|
.unlit = {
|
||||||
.color = tab->active ? COLOR_GREEN : COLOR_RED,
|
.color = tab->active ? COLOR_GREEN : COLOR_RED,
|
||||||
@@ -54,9 +49,21 @@ errorret_t uiTabDraw(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
errorChain(spriteBatchBuffer(&sprite, 1, &SHADER_UNLIT, material));
|
errorChain(spriteBatchBuffer(&sprite, 1, &SHADER_UNLIT, material));
|
||||||
errorChain(spriteBatchFlush());
|
|
||||||
|
|
||||||
errorChain(textDraw(x, y, tab->label, COLOR_WHITE, &FONT_DEFAULT));
|
errorChain(uiWidgetLabelDraw(&tab->label, x, y));
|
||||||
|
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void uiTabRebuildBackground(uitab_t *tab) {
|
||||||
|
assertNotNull(tab, "Tab cannot be NULL");
|
||||||
|
|
||||||
|
int32_t labelW, labelH;
|
||||||
|
uiWidgetLabelGetSize(&tab->label, &labelW, &labelH);
|
||||||
|
tab->cachedBackground = (spritebatchsprite_t){
|
||||||
|
.min = { 0.0f, 0.0f, 0.0f },
|
||||||
|
.max = { (float_t)labelW, (float_t)labelH, 0.0f },
|
||||||
|
.uvMin = { 0.0f, 0.0f },
|
||||||
|
.uvMax = { 1.0f, 1.0f }
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -7,10 +7,17 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
#include "error/error.h"
|
#include "error/error.h"
|
||||||
|
#include "ui/widget/uiwidgetlabel.h"
|
||||||
|
#include "display/spritebatch/spritebatchsprite.h"
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
const char_t *label;
|
uiwidgetlabel_t label;
|
||||||
bool_t active;
|
bool_t active;
|
||||||
|
|
||||||
|
// Background quad, cached relative to origin (0,0) from the label's
|
||||||
|
// size at init -- uiTabDraw only translates this into position, since
|
||||||
|
// active/inactive only changes the material color, not the geometry.
|
||||||
|
spritebatchsprite_t cachedBackground;
|
||||||
} uitab_t;
|
} uitab_t;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -51,3 +58,13 @@ errorret_t uiTabDraw(
|
|||||||
const float_t x,
|
const float_t x,
|
||||||
const float_t y
|
const float_t y
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rebuilds the tab's cached background quad (relative to origin (0,0))
|
||||||
|
* from its current label size. Called internally by uiTabInit -- the
|
||||||
|
* background's geometry never changes afterwards, since the tab has no
|
||||||
|
* API to change its label text.
|
||||||
|
*
|
||||||
|
* @param tab The tab to update.
|
||||||
|
*/
|
||||||
|
void uiTabRebuildBackground(uitab_t *tab);
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "uiwidgetlabel.h"
|
||||||
|
#include "assert/assert.h"
|
||||||
|
#include "util/memory.h"
|
||||||
|
#include "util/string.h"
|
||||||
|
#include "display/text/text.h"
|
||||||
|
|
||||||
|
void uiWidgetLabelInit(uiwidgetlabel_t *label, font_t *font) {
|
||||||
|
assertNotNull(label, "Label cannot be NULL");
|
||||||
|
assertNotNull(font, "Font cannot be NULL");
|
||||||
|
|
||||||
|
memoryZero(label, sizeof(uiwidgetlabel_t));
|
||||||
|
label->font = font;
|
||||||
|
label->color = COLOR_WHITE;
|
||||||
|
}
|
||||||
|
|
||||||
|
void uiWidgetLabelSetText(uiwidgetlabel_t *label, const char_t *text) {
|
||||||
|
assertNotNull(label, "Label cannot be NULL");
|
||||||
|
assertNotNull(text, "Text cannot be NULL");
|
||||||
|
assertStrLenMax(text, UI_WIDGET_LABEL_TEXT_MAX, "Label text too long");
|
||||||
|
|
||||||
|
stringCopy(label->text, text, UI_WIDGET_LABEL_TEXT_MAX);
|
||||||
|
label->spriteCount = textBuildSpriteCache(
|
||||||
|
label->text, label->font, label->sprites,
|
||||||
|
UI_WIDGET_LABEL_SPRITE_COUNT_MAX, &label->width, &label->height
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void uiWidgetLabelSetColor(uiwidgetlabel_t *label, const color_t color) {
|
||||||
|
assertNotNull(label, "Label cannot be NULL");
|
||||||
|
label->color = color;
|
||||||
|
}
|
||||||
|
|
||||||
|
void uiWidgetLabelGetSize(
|
||||||
|
const uiwidgetlabel_t *label,
|
||||||
|
int32_t *outWidth,
|
||||||
|
int32_t *outHeight
|
||||||
|
) {
|
||||||
|
assertNotNull(label, "Label cannot be NULL");
|
||||||
|
assertNotNull(outWidth, "Output width cannot be NULL");
|
||||||
|
assertNotNull(outHeight, "Output height cannot be NULL");
|
||||||
|
*outWidth = label->width;
|
||||||
|
*outHeight = label->height;
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t uiWidgetLabelDraw(
|
||||||
|
const uiwidgetlabel_t *label,
|
||||||
|
const float_t x,
|
||||||
|
const float_t y
|
||||||
|
) {
|
||||||
|
assertNotNull(label, "Label cannot be NULL");
|
||||||
|
if(label->spriteCount == 0) errorOk();
|
||||||
|
|
||||||
|
spritebatchsprite_t scratch[UI_WIDGET_LABEL_SPRITE_COUNT_MAX];
|
||||||
|
errorChain(textDrawSpriteCache(
|
||||||
|
label->sprites, label->spriteCount, scratch, x, y, label->color,
|
||||||
|
label->font->texture
|
||||||
|
));
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include "error/error.h"
|
||||||
|
#include "display/text/font.h"
|
||||||
|
#include "display/spritebatch/spritebatchsprite.h"
|
||||||
|
#include "display/color.h"
|
||||||
|
|
||||||
|
// A cached, cheap-to-redraw label sized for embedding directly inside
|
||||||
|
// small widgets (buttons, checkboxes, sliders, ...) rather than
|
||||||
|
// referencing text by pointer and re-deriving glyph geometry every
|
||||||
|
// frame. Capacity is intentionally much smaller than uilabel_t's -- it
|
||||||
|
// gets embedded by value in every widget instance (and inside
|
||||||
|
// uimenuitem_t's union, which is often arrayed), so keeping it small
|
||||||
|
// matters on memory-constrained targets. See uilabel_t for the
|
||||||
|
// general-purpose, larger-capacity equivalent.
|
||||||
|
#define UI_WIDGET_LABEL_TEXT_MAX 64
|
||||||
|
#define UI_WIDGET_LABEL_SPRITE_COUNT_MAX UI_WIDGET_LABEL_TEXT_MAX
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
char_t text[UI_WIDGET_LABEL_TEXT_MAX];
|
||||||
|
color_t color;
|
||||||
|
font_t *font;
|
||||||
|
|
||||||
|
spritebatchsprite_t sprites[UI_WIDGET_LABEL_SPRITE_COUNT_MAX];
|
||||||
|
uint32_t spriteCount;
|
||||||
|
int32_t width;
|
||||||
|
int32_t height;
|
||||||
|
} uiwidgetlabel_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes a widget label, defaulting to white text and no text set.
|
||||||
|
*
|
||||||
|
* @param label The label to initialize.
|
||||||
|
* @param font Font to use for rendering. Must outlive the label.
|
||||||
|
*/
|
||||||
|
void uiWidgetLabelInit(uiwidgetlabel_t *label, font_t *font);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the label's text, rebuilding its cached sprites immediately.
|
||||||
|
* Call this whenever the displayed text changes -- uiWidgetLabelDraw
|
||||||
|
* never recomputes it.
|
||||||
|
*
|
||||||
|
* @param label The label to update.
|
||||||
|
* @param text Null-terminated string to display. Must be shorter than
|
||||||
|
* UI_WIDGET_LABEL_TEXT_MAX.
|
||||||
|
*/
|
||||||
|
void uiWidgetLabelSetText(uiwidgetlabel_t *label, const char_t *text);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the label's tint color. Cheap -- doesn't touch the sprite cache.
|
||||||
|
*
|
||||||
|
* @param label The label to update.
|
||||||
|
* @param color The new tint color.
|
||||||
|
*/
|
||||||
|
void uiWidgetLabelSetColor(uiwidgetlabel_t *label, const color_t color);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the measured size (in pixels) of the label's current text, cached
|
||||||
|
* from the last uiWidgetLabelSetText call.
|
||||||
|
*
|
||||||
|
* @param label The label to query.
|
||||||
|
* @param outWidth Pointer to store the width.
|
||||||
|
* @param outHeight Pointer to store the height.
|
||||||
|
*/
|
||||||
|
void uiWidgetLabelGetSize(
|
||||||
|
const uiwidgetlabel_t *label,
|
||||||
|
int32_t *outWidth,
|
||||||
|
int32_t *outHeight
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draws the label's cached sprites at the given screen position in a
|
||||||
|
* single batched buffer call. Does not recompute glyph layout.
|
||||||
|
*
|
||||||
|
* @param label The label to draw.
|
||||||
|
* @param x Screen x position.
|
||||||
|
* @param y Screen y position.
|
||||||
|
* @return Any error that occurs.
|
||||||
|
*/
|
||||||
|
errorret_t uiWidgetLabelDraw(
|
||||||
|
const uiwidgetlabel_t *label,
|
||||||
|
const float_t x,
|
||||||
|
const float_t y
|
||||||
|
);
|
||||||
+2
-2
@@ -3,8 +3,8 @@
|
|||||||
# This software is released under the MIT License.
|
# This software is released under the MIT License.
|
||||||
# https://opensource.org/licenses/MIT
|
# https://opensource.org/licenses/MIT
|
||||||
|
|
||||||
# Sources
|
|
||||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||||
PUBLIC
|
PUBLIC
|
||||||
assetchunkloader.c
|
uitextbox.c
|
||||||
|
uitextboxmain.c
|
||||||
)
|
)
|
||||||
@@ -33,6 +33,7 @@ void uiTextboxInit(
|
|||||||
box->maxLength = maxLength;
|
box->maxLength = maxLength;
|
||||||
box->lines = lines;
|
box->lines = lines;
|
||||||
box->linesMax = linesMax;
|
box->linesMax = linesMax;
|
||||||
|
box->glyphsBuiltForPage = -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
void uiTextboxSetText(uitextbox_t *box, const char_t *text) {
|
void uiTextboxSetText(uitextbox_t *box, const char_t *text) {
|
||||||
@@ -43,6 +44,7 @@ void uiTextboxSetText(uitextbox_t *box, const char_t *text) {
|
|||||||
box->scroll = 0;
|
box->scroll = 0;
|
||||||
box->layoutWidth = 0.0f;
|
box->layoutWidth = 0.0f;
|
||||||
box->layoutHeight = 0.0f;
|
box->layoutHeight = 0.0f;
|
||||||
|
box->glyphsBuiltForPage = -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
void uiTextboxBuildLayout(
|
void uiTextboxBuildLayout(
|
||||||
@@ -52,6 +54,7 @@ void uiTextboxBuildLayout(
|
|||||||
) {
|
) {
|
||||||
assertNotNull(box, "Textbox cannot be NULL");
|
assertNotNull(box, "Textbox cannot be NULL");
|
||||||
|
|
||||||
|
box->glyphsBuiltForPage = -1;
|
||||||
box->layoutWidth = width;
|
box->layoutWidth = width;
|
||||||
box->layoutHeight = height;
|
box->layoutHeight = height;
|
||||||
box->lineCount = 0;
|
box->lineCount = 0;
|
||||||
@@ -169,13 +172,30 @@ errorret_t uiTextboxDraw(
|
|||||||
uiTextboxBuildLayout(box, contentW, contentH);
|
uiTextboxBuildLayout(box, contentW, contentH);
|
||||||
}
|
}
|
||||||
|
|
||||||
errorChain(uiFrameDraw(x, y, width, height));
|
errorChain(uiFrameDrawCached(&box->frameCache, x, y, width, height));
|
||||||
errorChain(spriteBatchFlush());
|
|
||||||
|
|
||||||
if(box->lineCount == 0 || box->text[0] == '\0') errorOk();
|
if(box->lineCount == 0 || box->text[0] == '\0') errorOk();
|
||||||
|
|
||||||
float_t fontW = (float_t)FONT_DEFAULT.tileset->tileWidth;
|
if(box->glyphsBuiltForPage != box->currentPage) {
|
||||||
float_t fontH = (float_t)FONT_DEFAULT.tileset->tileHeight;
|
uiTextboxBuildPageGlyphs(box);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(box->glyphCount == 0) errorOk();
|
||||||
|
|
||||||
|
int32_t visibleCount = 0;
|
||||||
|
while(
|
||||||
|
visibleCount < box->glyphCount &&
|
||||||
|
box->glyphs[visibleCount].revealAt <= box->scroll
|
||||||
|
) visibleCount++;
|
||||||
|
|
||||||
|
if(visibleCount == 0) errorOk();
|
||||||
|
|
||||||
|
spritebatchsprite_t scratch[UI_TEXTBOX_PAGE_GLYPHS_MAX];
|
||||||
|
for(int32_t i = 0; i < visibleCount; i++) {
|
||||||
|
scratch[i] = spriteBatchSpriteTranslate(
|
||||||
|
&box->glyphs[i].sprite, contentX, contentY
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
shadermaterial_t material = {
|
shadermaterial_t material = {
|
||||||
.unlit = {
|
.unlit = {
|
||||||
@@ -183,35 +203,47 @@ errorret_t uiTextboxDraw(
|
|||||||
.texture = FONT_DEFAULT.texture
|
.texture = FONT_DEFAULT.texture
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
errorChain(spriteBatchBuffer(scratch, visibleCount, &SHADER_UNLIT, material));
|
||||||
|
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
void uiTextboxBuildPageGlyphs(uitextbox_t *box) {
|
||||||
|
assertNotNull(box, "Textbox cannot be NULL");
|
||||||
|
|
||||||
|
box->glyphCount = 0;
|
||||||
|
|
||||||
int32_t pageFirst = box->currentPage * box->linesPerPage;
|
int32_t pageFirst = box->currentPage * box->linesPerPage;
|
||||||
int32_t pageLast = pageFirst + box->linesPerPage;
|
int32_t pageLast = pageFirst + box->linesPerPage;
|
||||||
if(pageLast > box->lineCount) pageLast = box->lineCount;
|
if(pageLast > box->lineCount) pageLast = box->lineCount;
|
||||||
|
|
||||||
int32_t charsLeft = box->scroll;
|
float_t fontW = (float_t)FONT_DEFAULT.tileset->tileWidth;
|
||||||
|
float_t fontH = (float_t)FONT_DEFAULT.tileset->tileHeight;
|
||||||
|
|
||||||
for(int32_t li = pageFirst; li < pageLast && charsLeft > 0; li++) {
|
int32_t consumed = 0;
|
||||||
|
for(int32_t li = pageFirst; li < pageLast; li++) {
|
||||||
uitextboxline_t *line = &box->lines[li];
|
uitextboxline_t *line = &box->lines[li];
|
||||||
int32_t visible = line->count < charsLeft ? line->count : charsLeft;
|
float_t lineY =
|
||||||
float_t lineY = contentY +
|
|
||||||
(float_t)(li - pageFirst) * (fontH + UI_TEXTBOX_LINE_SPACING);
|
(float_t)(li - pageFirst) * (fontH + UI_TEXTBOX_LINE_SPACING);
|
||||||
|
|
||||||
for(int32_t ci = 0; ci < visible; ci++) {
|
for(int32_t ci = 0; ci < line->count; ci++) {
|
||||||
|
consumed++;
|
||||||
char_t c = box->text[line->start + ci];
|
char_t c = box->text[line->start + ci];
|
||||||
if(c == ' ') continue;
|
if(c == ' ') continue;
|
||||||
spritebatchsprite_t sprite = textGetSprite(
|
|
||||||
(vec2){ contentX + (float_t)ci * fontW, lineY },
|
assertTrue(
|
||||||
c,
|
box->glyphCount < UI_TEXTBOX_PAGE_GLYPHS_MAX,
|
||||||
&FONT_DEFAULT
|
"Textbox page produces too many glyphs"
|
||||||
);
|
);
|
||||||
errorChain(spriteBatchBuffer(&sprite, 1, &SHADER_UNLIT, material));
|
uitextboxglyph_t *glyph = &box->glyphs[box->glyphCount++];
|
||||||
|
glyph->sprite = textGetSprite(
|
||||||
|
(vec2){ (float_t)ci * fontW, lineY }, c, &FONT_DEFAULT
|
||||||
|
);
|
||||||
|
glyph->revealAt = consumed;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
charsLeft -= visible;
|
box->glyphsBuiltForPage = box->currentPage;
|
||||||
}
|
|
||||||
|
|
||||||
errorChain(spriteBatchFlush());
|
|
||||||
errorOk();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int32_t uiTextboxGetPageCharCount(const uitextbox_t *box) {
|
int32_t uiTextboxGetPageCharCount(const uitextbox_t *box) {
|
||||||
@@ -7,16 +7,34 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
#include "error/error.h"
|
#include "error/error.h"
|
||||||
|
#include "ui/frame/uiframe.h"
|
||||||
|
#include "display/spritebatch/spritebatchsprite.h"
|
||||||
|
|
||||||
#define UI_TEXTBOX_LINES_PER_PAGE_MAX 4
|
#define UI_TEXTBOX_LINES_PER_PAGE_MAX 4
|
||||||
#define UI_TEXTBOX_SCROLL_CHARS_PER_TICK 1
|
#define UI_TEXTBOX_SCROLL_CHARS_PER_TICK 1
|
||||||
#define UI_TEXTBOX_LINE_SPACING 0.0f
|
#define UI_TEXTBOX_LINE_SPACING 0.0f
|
||||||
|
|
||||||
|
// Fixed capacity for a page's cached glyph sprites (see uitextboxglyph_t).
|
||||||
|
// Sized generously above UI_TEXTBOX_LINES_PER_PAGE_MAX worth of glyphs at
|
||||||
|
// typical textbox widths; uiTextboxBuildPageGlyphs asserts if a page
|
||||||
|
// somehow produces more than this.
|
||||||
|
#define UI_TEXTBOX_PAGE_GLYPHS_MAX 512
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
int32_t start;
|
int32_t start;
|
||||||
int32_t count;
|
int32_t count;
|
||||||
} uitextboxline_t;
|
} uitextboxline_t;
|
||||||
|
|
||||||
|
// A single visible glyph's cached sprite (relative to the textbox's
|
||||||
|
// content origin, i.e. (0,0)) plus the scroll value at/after which it
|
||||||
|
// becomes visible -- lets uiTextboxDraw turn the typewriter scroll into
|
||||||
|
// a simple prefix-count instead of recomputing glyph geometry every
|
||||||
|
// frame.
|
||||||
|
typedef struct {
|
||||||
|
spritebatchsprite_t sprite;
|
||||||
|
int32_t revealAt;
|
||||||
|
} uitextboxglyph_t;
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
char_t *text;
|
char_t *text;
|
||||||
uint32_t maxLength;
|
uint32_t maxLength;
|
||||||
@@ -34,6 +52,15 @@ typedef struct {
|
|||||||
|
|
||||||
int32_t currentPage;
|
int32_t currentPage;
|
||||||
int32_t scroll;
|
int32_t scroll;
|
||||||
|
|
||||||
|
// Cached glyph sprites for the current page (see uitextboxglyph_t),
|
||||||
|
// rebuilt only when currentPage no longer matches
|
||||||
|
// glyphsBuiltForPage -- not every frame/scroll tick.
|
||||||
|
uitextboxglyph_t glyphs[UI_TEXTBOX_PAGE_GLYPHS_MAX];
|
||||||
|
int32_t glyphCount;
|
||||||
|
int32_t glyphsBuiltForPage;
|
||||||
|
|
||||||
|
uiframecache_t frameCache;
|
||||||
} uitextbox_t;
|
} uitextbox_t;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -136,3 +163,13 @@ bool_t uiTextboxHasNextPage(const uitextbox_t *box);
|
|||||||
* @param box The textbox to advance.
|
* @param box The textbox to advance.
|
||||||
*/
|
*/
|
||||||
void uiTextboxNextPage(uitextbox_t *box);
|
void uiTextboxNextPage(uitextbox_t *box);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rebuilds the cached glyph sprites (see uitextboxglyph_t) for the
|
||||||
|
* current page from its line layout. Called automatically by
|
||||||
|
* uiTextboxDraw whenever currentPage no longer matches the page the
|
||||||
|
* cache was last built for.
|
||||||
|
*
|
||||||
|
* @param box The textbox to rebuild.
|
||||||
|
*/
|
||||||
|
void uiTextboxBuildPageGlyphs(uitextbox_t *box);
|
||||||
@@ -79,7 +79,6 @@ errorret_t uiTextboxMainDraw(void) {
|
|||||||
&FONT_DEFAULT
|
&FONT_DEFAULT
|
||||||
);
|
);
|
||||||
errorChain(spriteBatchBuffer(&caret, 1, &SHADER_UNLIT, material));
|
errorChain(spriteBatchBuffer(&caret, 1, &SHADER_UNLIT, material));
|
||||||
errorChain(spriteBatchFlush());
|
|
||||||
|
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
+1
-1
@@ -6,7 +6,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
#include "ui/rpg/textbox/uitextbox.h"
|
#include "uitextbox.h"
|
||||||
#include "ui/focus/uifocusitem.h"
|
#include "ui/focus/uifocusitem.h"
|
||||||
|
|
||||||
#define UI_TEXTBOX_MAIN_LINES 4
|
#define UI_TEXTBOX_MAIN_LINES 4
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "uitestlabel.h"
|
||||||
|
#include "ui/widget/uilabel.h"
|
||||||
|
#include "display/text/text.h"
|
||||||
|
#include "display/screen/screen.h"
|
||||||
|
#include "engine/engine.h"
|
||||||
|
|
||||||
|
uilabel_t UI_TEST_LABEL;
|
||||||
|
|
||||||
|
errorret_t uiTestLabelInit() {
|
||||||
|
uiLabelInit(&UI_TEST_LABEL, &FONT_DEFAULT);
|
||||||
|
uiLabelSetText(&UI_TEST_LABEL, "duskrpg");
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t uiTestLabelDraw() {
|
||||||
|
int32_t versionWidth, versionHeight;
|
||||||
|
textMeasure(ENGINE.version, &FONT_DEFAULT, &versionWidth, &versionHeight);
|
||||||
|
|
||||||
|
errorChain(uiLabelDraw(
|
||||||
|
&UI_TEST_LABEL,
|
||||||
|
(float_t)(SCREEN.scanX + SCREEN.scanWidth - UI_TEST_LABEL.width),
|
||||||
|
(float_t)(
|
||||||
|
SCREEN.scanY + SCREEN.scanHeight - versionHeight -
|
||||||
|
UI_TEST_LABEL.height
|
||||||
|
)
|
||||||
|
));
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include "error/error.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the test label, a smoke test for the duskrpg-side UI
|
||||||
|
* element extension point (uielementgame.h).
|
||||||
|
*
|
||||||
|
* @return Any error that occurs.
|
||||||
|
*/
|
||||||
|
errorret_t uiTestLabelInit();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draws the test label above the engine's debug version number.
|
||||||
|
*
|
||||||
|
* @return Any error that occurs.
|
||||||
|
*/
|
||||||
|
errorret_t uiTestLabelDraw();
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# Copyright (c) 2026 Dominic Masters
|
||||||
|
#
|
||||||
|
# This software is released under the MIT License.
|
||||||
|
# https://opensource.org/licenses/MIT
|
||||||
|
|
||||||
|
include(dusktest)
|
||||||
|
|
||||||
|
# Tests
|
||||||
|
dusktest(test_uiframe.c)
|
||||||
|
dusktest(test_uislider.c)
|
||||||
|
dusktest(test_uitab.c)
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "dusktest.h"
|
||||||
|
#include "ui/frame/uiframe.h"
|
||||||
|
|
||||||
|
static void test_uiFrameBuildSprites_layout(void **state) {
|
||||||
|
UI_FRAME.tileset = (tileset_t){
|
||||||
|
.tileWidth = 1,
|
||||||
|
.tileHeight = 1,
|
||||||
|
.tileCount = 9,
|
||||||
|
.columns = 3,
|
||||||
|
.rows = 3,
|
||||||
|
.uv = { 0.25f, 0.25f }
|
||||||
|
};
|
||||||
|
|
||||||
|
const float_t x = 100.0f;
|
||||||
|
const float_t y = 50.0f;
|
||||||
|
const float_t width = 40.0f;
|
||||||
|
const float_t height = 30.0f;
|
||||||
|
const float_t tileW = (float_t)UI_FRAME_BORDER_WIDTH;
|
||||||
|
const float_t tileH = (float_t)UI_FRAME_BORDER_HEIGHT;
|
||||||
|
|
||||||
|
spritebatchsprite_t sprites[9];
|
||||||
|
uiFrameBuildSprites(sprites, x, y, width, height);
|
||||||
|
|
||||||
|
// Top-left corner sits exactly at the rect's origin.
|
||||||
|
assert_float_equal(sprites[0].min[0], x, 0.0001f);
|
||||||
|
assert_float_equal(sprites[0].min[1], y, 0.0001f);
|
||||||
|
assert_float_equal(sprites[0].max[0], x + tileW, 0.0001f);
|
||||||
|
assert_float_equal(sprites[0].max[1], y + tileH, 0.0001f);
|
||||||
|
assert_float_equal(sprites[0].uvMin[0], 0.0f, 0.0001f);
|
||||||
|
assert_float_equal(sprites[0].uvMin[1], 0.0f, 0.0001f);
|
||||||
|
assert_float_equal(sprites[0].uvMax[0], 0.25f, 0.0001f);
|
||||||
|
assert_float_equal(sprites[0].uvMax[1], 0.25f, 0.0001f);
|
||||||
|
|
||||||
|
// Top-middle edge stretches to fill the width minus both corners.
|
||||||
|
assert_float_equal(sprites[1].min[0], x + tileW, 0.0001f);
|
||||||
|
assert_float_equal(sprites[1].max[0], x + width - tileW, 0.0001f);
|
||||||
|
assert_float_equal(sprites[1].max[1], y + tileH, 0.0001f);
|
||||||
|
|
||||||
|
// Center tile fills the remaining interior on both axes.
|
||||||
|
assert_float_equal(sprites[4].min[0], x + tileW, 0.0001f);
|
||||||
|
assert_float_equal(sprites[4].min[1], y + tileH, 0.0001f);
|
||||||
|
assert_float_equal(sprites[4].max[0], x + width - tileW, 0.0001f);
|
||||||
|
assert_float_equal(sprites[4].max[1], y + height - tileH, 0.0001f);
|
||||||
|
|
||||||
|
// Bottom-right corner sits exactly at the rect's far corner, sampling
|
||||||
|
// tileset column 2, row 2.
|
||||||
|
assert_float_equal(sprites[8].min[0], x + width - tileW, 0.0001f);
|
||||||
|
assert_float_equal(sprites[8].min[1], y + height - tileH, 0.0001f);
|
||||||
|
assert_float_equal(sprites[8].max[0], x + width, 0.0001f);
|
||||||
|
assert_float_equal(sprites[8].max[1], y + height, 0.0001f);
|
||||||
|
assert_float_equal(sprites[8].uvMin[0], 0.5f, 0.0001f);
|
||||||
|
assert_float_equal(sprites[8].uvMin[1], 0.5f, 0.0001f);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char **argv) {
|
||||||
|
const struct CMUnitTest tests[] = {
|
||||||
|
cmocka_unit_test(test_uiFrameBuildSprites_layout),
|
||||||
|
};
|
||||||
|
|
||||||
|
return cmocka_run_group_tests(tests, NULL, NULL);
|
||||||
|
}
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "dusktest.h"
|
||||||
|
#include "ui/widget/uislider.h"
|
||||||
|
#include "util/memory.h"
|
||||||
|
|
||||||
|
// uiSliderInitFloat/InitInt require a real font (FONT_DEFAULT) to build
|
||||||
|
// the label's glyph cache, which needs a live GL context this test binary
|
||||||
|
// doesn't have -- see the label size caching contract for why this is
|
||||||
|
// safe to bypass here (uiSliderRebuildGeometry only reads slider->label's
|
||||||
|
// already-measured width/height, it never touches the font itself).
|
||||||
|
static void sliderSetupFloat(
|
||||||
|
uislider_t *slider,
|
||||||
|
const float_t value,
|
||||||
|
const float_t min,
|
||||||
|
const float_t max,
|
||||||
|
const float_t step
|
||||||
|
) {
|
||||||
|
memoryZero(slider, sizeof(uislider_t));
|
||||||
|
slider->label.width = 20;
|
||||||
|
slider->label.height = 10;
|
||||||
|
slider->type = UI_SLIDER_TYPE_FLOAT;
|
||||||
|
slider->min.f = min;
|
||||||
|
slider->max.f = max;
|
||||||
|
slider->step.f = step;
|
||||||
|
slider->value.f = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void sliderSetupInt(
|
||||||
|
uislider_t *slider,
|
||||||
|
const int32_t value,
|
||||||
|
const int32_t min,
|
||||||
|
const int32_t max,
|
||||||
|
const int32_t step
|
||||||
|
) {
|
||||||
|
memoryZero(slider, sizeof(uislider_t));
|
||||||
|
slider->label.width = 20;
|
||||||
|
slider->label.height = 10;
|
||||||
|
slider->type = UI_SLIDER_TYPE_INT;
|
||||||
|
slider->min.i = min;
|
||||||
|
slider->max.i = max;
|
||||||
|
slider->step.i = step;
|
||||||
|
slider->value.i = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_uiSliderRebuildGeometry_trackPosition(void **state) {
|
||||||
|
uislider_t slider;
|
||||||
|
sliderSetupFloat(&slider, 0.0f, 0.0f, 1.0f, 0.1f);
|
||||||
|
uiSliderRebuildGeometry(&slider);
|
||||||
|
|
||||||
|
const float_t expectedX = (float_t)slider.label.width + UI_SLIDER_GAP;
|
||||||
|
const float_t expectedY =
|
||||||
|
((float_t)slider.label.height - UI_SLIDER_TRACK_HEIGHT) * 0.5f;
|
||||||
|
|
||||||
|
assert_float_equal(slider.cachedTrack.min[0], expectedX, 0.0001f);
|
||||||
|
assert_float_equal(slider.cachedTrack.min[1], expectedY, 0.0001f);
|
||||||
|
assert_float_equal(
|
||||||
|
slider.cachedTrack.max[0], expectedX + UI_SLIDER_TRACK_WIDTH, 0.0001f
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_uiSliderRebuildGeometry_fillVisibility(void **state) {
|
||||||
|
uislider_t slider;
|
||||||
|
|
||||||
|
// At the minimum value, the ratio is 0 -- no fill should be visible.
|
||||||
|
sliderSetupFloat(&slider, 0.0f, 0.0f, 10.0f, 1.0f);
|
||||||
|
uiSliderRebuildGeometry(&slider);
|
||||||
|
assert_false(slider.cachedFillVisible);
|
||||||
|
|
||||||
|
// Halfway to max -- fill should cover half the track.
|
||||||
|
sliderSetupFloat(&slider, 5.0f, 0.0f, 10.0f, 1.0f);
|
||||||
|
uiSliderRebuildGeometry(&slider);
|
||||||
|
assert_true(slider.cachedFillVisible);
|
||||||
|
assert_float_equal(
|
||||||
|
slider.cachedFill.max[0] - slider.cachedFill.min[0],
|
||||||
|
UI_SLIDER_TRACK_WIDTH * 0.5f,
|
||||||
|
0.0001f
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_uiSliderRebuildGeometry_stepMarkers(void **state) {
|
||||||
|
uislider_t slider;
|
||||||
|
|
||||||
|
// 5 steps between 0 and 10 -- under the marker cap, so markers render.
|
||||||
|
sliderSetupInt(&slider, 0, 0, 10, 2);
|
||||||
|
uiSliderRebuildGeometry(&slider);
|
||||||
|
assert_int_equal(slider.cachedMarkerCount, 6);
|
||||||
|
assert_float_equal(
|
||||||
|
slider.cachedMarkers[0].min[0], slider.cachedTrack.min[0] -
|
||||||
|
UI_SLIDER_STEP_MARKER_WIDTH * 0.5f,
|
||||||
|
0.0001f
|
||||||
|
);
|
||||||
|
|
||||||
|
// 100 steps -- over the marker cap, so it falls back to a smooth fill
|
||||||
|
// with no markers.
|
||||||
|
sliderSetupInt(&slider, 0, 0, 100, 1);
|
||||||
|
uiSliderRebuildGeometry(&slider);
|
||||||
|
assert_int_equal(slider.cachedMarkerCount, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char **argv) {
|
||||||
|
const struct CMUnitTest tests[] = {
|
||||||
|
cmocka_unit_test(test_uiSliderRebuildGeometry_trackPosition),
|
||||||
|
cmocka_unit_test(test_uiSliderRebuildGeometry_fillVisibility),
|
||||||
|
cmocka_unit_test(test_uiSliderRebuildGeometry_stepMarkers),
|
||||||
|
};
|
||||||
|
|
||||||
|
return cmocka_run_group_tests(tests, NULL, NULL);
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "dusktest.h"
|
||||||
|
#include "ui/widget/uitab.h"
|
||||||
|
#include "util/memory.h"
|
||||||
|
|
||||||
|
// uiTabInit requires a real font (FONT_DEFAULT) to build the label's
|
||||||
|
// glyph cache, which needs a live GL context this test binary doesn't
|
||||||
|
// have. uiTabRebuildBackground only reads the label's already-measured
|
||||||
|
// width/height, so it's safe to set those directly and exercise it here.
|
||||||
|
static void test_uiTabRebuildBackground_matchesLabelSize(void **state) {
|
||||||
|
uitab_t tab;
|
||||||
|
memoryZero(&tab, sizeof(uitab_t));
|
||||||
|
tab.label.width = 42;
|
||||||
|
tab.label.height = 12;
|
||||||
|
|
||||||
|
uiTabRebuildBackground(&tab);
|
||||||
|
|
||||||
|
assert_float_equal(tab.cachedBackground.min[0], 0.0f, 0.0001f);
|
||||||
|
assert_float_equal(tab.cachedBackground.min[1], 0.0f, 0.0001f);
|
||||||
|
assert_float_equal(tab.cachedBackground.max[0], 42.0f, 0.0001f);
|
||||||
|
assert_float_equal(tab.cachedBackground.max[1], 12.0f, 0.0001f);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char **argv) {
|
||||||
|
const struct CMUnitTest tests[] = {
|
||||||
|
cmocka_unit_test(test_uiTabRebuildBackground_matchesLabelSize),
|
||||||
|
};
|
||||||
|
|
||||||
|
return cmocka_run_group_tests(tests, NULL, NULL);
|
||||||
|
}
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user