Compare commits
100 Commits
alpha-0.0.4
...
ac2
| 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 | |||
| ca02ee0352 | |||
| fbaa54145e | |||
| 28754ffbf2 | |||
| 470c0eba7a | |||
| 7098dcec43 | |||
| 07137f57af | |||
| 8b7491a3d3 | |||
| 8cfa8ddfeb | |||
| ef284a15a1 | |||
| 3723921573 | |||
| 195399635e | |||
| 46e2a924d3 | |||
| b693ea4102 | |||
| a73f55beb0 | |||
| 0bd2491ab7 | |||
| 860c797c9c | |||
| 38c5080f9f | |||
| fae191d8fe | |||
| d83a953e2d | |||
| 2dcf0d0f0d | |||
| 6dee37d8c1 | |||
| 3bad03afb3 | |||
| 6a6d8448f7 | |||
| 189babd2cf | |||
| 988a0f2294 | |||
| 85bf455731 | |||
| 589e4224f3 | |||
| bd7fa154b4 | |||
| a292f1992b | |||
| 88ddf429b7 | |||
| 7a1f6662df | |||
| afc68bccc6 | |||
| b4cdc4a64f | |||
| 3ad5afb81c | |||
| bed3f20118 | |||
| de67315178 | |||
| 8d5c0c7cad | |||
| a8271e01bd | |||
| 900b3f8558 | |||
| fdc4e056f9 | |||
| 7b98e40ccf | |||
| 01d89cf22c | |||
| 503a3c799a | |||
| 0b21388844 | |||
| 117bdf0c00 | |||
| 172dc5d37b |
@@ -4,36 +4,6 @@ on:
|
||||
tags:
|
||||
- '*'
|
||||
jobs:
|
||||
run-tests:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y \
|
||||
build-essential \
|
||||
cmake \
|
||||
python3 \
|
||||
python3-pip \
|
||||
python3-polib \
|
||||
python3-pil \
|
||||
libsdl2-dev \
|
||||
libgl1-mesa-dev \
|
||||
libzip-dev \
|
||||
python3-dotenv \
|
||||
python3-pyqt5 \
|
||||
python3-opengl \
|
||||
xz-utils \
|
||||
liblzma-dev \
|
||||
libbz2-dev \
|
||||
zlib1g-dev \
|
||||
git \
|
||||
libssl-dev
|
||||
- name: Run tests
|
||||
run: ./scripts/test-linux.sh
|
||||
|
||||
build-linux:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
name: Test Dusk
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
jobs:
|
||||
run-tests:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y \
|
||||
build-essential \
|
||||
cmake \
|
||||
python3 \
|
||||
python3-pip \
|
||||
python3-polib \
|
||||
python3-pil \
|
||||
libsdl2-dev \
|
||||
libgl1-mesa-dev \
|
||||
libzip-dev \
|
||||
python3-dotenv \
|
||||
python3-pyqt5 \
|
||||
python3-opengl \
|
||||
xz-utils \
|
||||
liblzma-dev \
|
||||
libbz2-dev \
|
||||
zlib1g-dev \
|
||||
git \
|
||||
libssl-dev
|
||||
- name: Run tests
|
||||
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)
|
||||
|
||||
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_AUTHOR "YourWishes" CACHE STRING "Game author / coder")
|
||||
@@ -90,6 +91,12 @@ target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME}
|
||||
DUSK_VERSION="${DUSK_VERSION}"
|
||||
)
|
||||
|
||||
if(DUSK_NETWORKING)
|
||||
target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
|
||||
DUSK_NETWORKING
|
||||
)
|
||||
endif()
|
||||
|
||||
# Toolchains
|
||||
include(cmake/targets/${DUSK_TARGET_SYSTEM}.cmake)
|
||||
|
||||
@@ -116,7 +123,11 @@ if(DUSK_BUILD_TESTS)
|
||||
endif()
|
||||
|
||||
# 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(
|
||||
OUTPUT "${DUSK_ASSETS_ZIP}"
|
||||
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
|
||||
uiconsole.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 "time/time.h"
|
||||
#include "display/spritebatch/spritebatch.h"
|
||||
#include "util/string.h"
|
||||
#include "display/text/text.h"
|
||||
#include "display/screen/screen.h"
|
||||
#include "engine/engine.h"
|
||||
|
||||
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() {
|
||||
char_t fpsText[32];
|
||||
|
||||
@@ -50,23 +58,21 @@ errorret_t uiFPSDraw() {
|
||||
textColor = COLOR_RED;
|
||||
}
|
||||
|
||||
errorChain(textDraw(
|
||||
(float_t)SCREEN.scanX,
|
||||
(float_t)SCREEN.scanY,
|
||||
fpsText, textColor,
|
||||
&FONT_DEFAULT
|
||||
uiLabelSetColor(&UIFPS.fpsLabel, textColor);
|
||||
if(stringCompare(fpsText, UIFPS.fpsLabel.text) != 0) {
|
||||
uiLabelSetText(&UIFPS.fpsLabel, fpsText);
|
||||
}
|
||||
errorChain(uiLabelDraw(
|
||||
&UIFPS.fpsLabel, (float_t)SCREEN.scanX, (float_t)SCREEN.scanY
|
||||
));
|
||||
errorChain(spriteBatchFlush());
|
||||
|
||||
int32_t versionWidth, versionHeight;
|
||||
textMeasure(ENGINE.version, &FONT_DEFAULT, &versionWidth, &versionHeight);
|
||||
errorChain(textDraw(
|
||||
uiLabelGetSize(&UIFPS.versionLabel, &versionWidth, &versionHeight);
|
||||
errorChain(uiLabelDraw(
|
||||
&UIFPS.versionLabel,
|
||||
(float_t)(SCREEN.scanX + SCREEN.scanWidth - versionWidth),
|
||||
(float_t)(SCREEN.scanY + SCREEN.scanHeight - versionHeight),
|
||||
ENGINE.version,
|
||||
color(255, 255, 255, 128),
|
||||
&FONT_DEFAULT
|
||||
(float_t)(SCREEN.scanY + SCREEN.scanHeight - versionHeight)
|
||||
));
|
||||
|
||||
return spriteBatchFlush();
|
||||
errorOk();
|
||||
}
|
||||
@@ -8,17 +8,27 @@
|
||||
#pragma once
|
||||
#include "error/error.h"
|
||||
#include "time/timeepoch.h"
|
||||
#include "ui/widget/uilabel.h"
|
||||
|
||||
typedef struct {
|
||||
dusktimeepoch_t lastTick;
|
||||
float_t fpsAverage;
|
||||
uilabel_t fpsLabel;
|
||||
uilabel_t versionLabel;
|
||||
} uifps_t;
|
||||
|
||||
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).
|
||||
*
|
||||
*
|
||||
* @return Any error that occurs.
|
||||
*/
|
||||
errorret_t uiFPSDraw();
|
||||
@@ -12,11 +12,11 @@
|
||||
#include "time/time.h"
|
||||
|
||||
const uifocusdirmap_t UI_FOCUS_DIR_MAP[] = {
|
||||
{ INPUT_ACTION_UP, UI_FOCUS_DIRECTION_UP, 0, -1 },
|
||||
{ INPUT_ACTION_DOWN, UI_FOCUS_DIRECTION_DOWN, 0, 1 },
|
||||
{ INPUT_ACTION_LEFT, UI_FOCUS_DIRECTION_LEFT, -1, 0 },
|
||||
{ INPUT_ACTION_RIGHT, UI_FOCUS_DIRECTION_RIGHT, 1, 0 },
|
||||
{ INPUT_ACTION_NULL, UI_FOCUS_DIRECTION_NONE, 0, 0 },
|
||||
{ INPUT_BIND_UP, UI_FOCUS_DIRECTION_UP, 0, -1 },
|
||||
{ INPUT_BIND_DOWN, UI_FOCUS_DIRECTION_DOWN, 0, 1 },
|
||||
{ INPUT_BIND_LEFT, UI_FOCUS_DIRECTION_LEFT, -1, 0 },
|
||||
{ INPUT_BIND_RIGHT, UI_FOCUS_DIRECTION_RIGHT, 1, 0 },
|
||||
{ INPUT_BIND_NULL, UI_FOCUS_DIRECTION_NONE, 0, 0 },
|
||||
};
|
||||
|
||||
uifocus_t UI_FOCUS;
|
||||
@@ -31,6 +31,7 @@ uifocusitem_t * uiFocusPush(
|
||||
uifocusitemcallback_t selected,
|
||||
uifocusitemcallback_t changed,
|
||||
uifocusitemcallback_t closed,
|
||||
uifocusitemdirectioncallback_t direction,
|
||||
void *user
|
||||
) {
|
||||
assertTrue(
|
||||
@@ -47,6 +48,7 @@ uifocusitem_t * uiFocusPush(
|
||||
item->selected = selected;
|
||||
item->changed = changed;
|
||||
item->closed = closed;
|
||||
item->direction = direction;
|
||||
item->user = user;
|
||||
UI_FOCUS.count++;
|
||||
UI_FOCUS.pushedThisTick = true;
|
||||
@@ -106,7 +108,7 @@ void uiFocusMoveDirection(
|
||||
) {
|
||||
for(
|
||||
const uifocusdirmap_t *m = UI_FOCUS_DIR_MAP;
|
||||
m->action != INPUT_ACTION_NULL;
|
||||
m->action != INPUT_BIND_NULL;
|
||||
m++
|
||||
) {
|
||||
if(m->direction != dir) continue;
|
||||
@@ -118,6 +120,14 @@ void uiFocusMoveDirection(
|
||||
}
|
||||
}
|
||||
|
||||
void uiFocusHandleDirection(
|
||||
uifocusitem_t *item,
|
||||
const uifocusdirection_t dir
|
||||
) {
|
||||
if(item->direction != NULL && item->direction(item, dir)) return;
|
||||
uiFocusMoveDirection(item, dir);
|
||||
}
|
||||
|
||||
void uiFocusUpdate(void) {
|
||||
if(UI_FOCUS.count == 0) return;
|
||||
|
||||
@@ -132,32 +142,32 @@ void uiFocusUpdate(void) {
|
||||
|
||||
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);
|
||||
return;
|
||||
}
|
||||
|
||||
if(inputPressed(INPUT_ACTION_CANCEL)) {
|
||||
if(inputPressed(INPUT_BIND_CANCEL)) {
|
||||
uiFocusPop();
|
||||
return;
|
||||
}
|
||||
|
||||
for(
|
||||
const uifocusdirmap_t *m = UI_FOCUS_DIR_MAP;
|
||||
m->action != INPUT_ACTION_NULL;
|
||||
m->action != INPUT_BIND_NULL;
|
||||
m++
|
||||
) {
|
||||
if(!inputPressed(m->action)) continue;
|
||||
UI_FOCUS.direction = m->direction;
|
||||
UI_FOCUS.timeHeld = 0.0f;
|
||||
uiFocusMoveDirection(item, m->direction);
|
||||
uiFocusHandleDirection(item, m->direction);
|
||||
return;
|
||||
}
|
||||
|
||||
bool_t held = false;
|
||||
for(
|
||||
const uifocusdirmap_t *m = UI_FOCUS_DIR_MAP;
|
||||
m->action != INPUT_ACTION_NULL;
|
||||
m->action != INPUT_BIND_NULL;
|
||||
m++
|
||||
) {
|
||||
if(m->direction != UI_FOCUS.direction) continue;
|
||||
@@ -176,5 +186,5 @@ void uiFocusUpdate(void) {
|
||||
if(UI_FOCUS.timeHeld < UI_FOCUS_HOLD_DELAY) return;
|
||||
if(UI_FOCUS.timeHeld < UI_FOCUS_HOLD_DELAY + UI_FOCUS_HOLD_REPEAT) return;
|
||||
UI_FOCUS.timeHeld = UI_FOCUS_HOLD_DELAY;
|
||||
uiFocusMoveDirection(item, UI_FOCUS.direction);
|
||||
uiFocusHandleDirection(item, UI_FOCUS.direction);
|
||||
}
|
||||
@@ -22,16 +22,8 @@
|
||||
*/
|
||||
#define UI_FOCUS_HOLD_REPEAT 0.1f
|
||||
|
||||
typedef enum {
|
||||
UI_FOCUS_DIRECTION_NONE,
|
||||
UI_FOCUS_DIRECTION_UP,
|
||||
UI_FOCUS_DIRECTION_DOWN,
|
||||
UI_FOCUS_DIRECTION_LEFT,
|
||||
UI_FOCUS_DIRECTION_RIGHT
|
||||
} uifocusdirection_t;
|
||||
|
||||
typedef struct {
|
||||
inputaction_t action;
|
||||
inputbind_t action;
|
||||
uifocusdirection_t direction;
|
||||
int8_t dx;
|
||||
int8_t dy;
|
||||
@@ -39,7 +31,7 @@ typedef struct {
|
||||
|
||||
/**
|
||||
* 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[];
|
||||
|
||||
@@ -72,6 +64,8 @@ void uiFocusInit(void);
|
||||
* @param selected Called when the user selects the focused cell.
|
||||
* @param changed Called when the focused cell position changes.
|
||||
* @param closed Called when this focus item is popped.
|
||||
* @param direction Called on a direction press/hold before the default
|
||||
* cell-to-cell movement is applied; may be NULL.
|
||||
* @param user Arbitrary pointer stored on the item before changed fires.
|
||||
* @returns Pointer to the newly pushed focus item.
|
||||
*/
|
||||
@@ -81,6 +75,7 @@ uifocusitem_t * uiFocusPush(
|
||||
uifocusitemcallback_t selected,
|
||||
uifocusitemcallback_t changed,
|
||||
uifocusitemcallback_t closed,
|
||||
uifocusitemdirectioncallback_t direction,
|
||||
void *user
|
||||
);
|
||||
|
||||
@@ -118,6 +113,19 @@ void uiFocusMoveDirection(
|
||||
const uifocusdirection_t dir
|
||||
);
|
||||
|
||||
/**
|
||||
* Handles a direction press/hold for the given item: gives the item's
|
||||
* direction callback (if any) first refusal, falling back to the
|
||||
* default cell-to-cell movement if it's unset or returns false.
|
||||
*
|
||||
* @param item The focus item to move.
|
||||
* @param dir Direction that was pressed or held.
|
||||
*/
|
||||
void uiFocusHandleDirection(
|
||||
uifocusitem_t *item,
|
||||
const uifocusdirection_t dir
|
||||
);
|
||||
|
||||
/**
|
||||
* Updates the focus system. Handles first-press movement and
|
||||
* held-direction repeating. Called once per game tick.
|
||||
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "dusk.h"
|
||||
|
||||
typedef struct uifocusitem_s uifocusitem_t;
|
||||
|
||||
typedef enum {
|
||||
UI_FOCUS_DIRECTION_NONE,
|
||||
UI_FOCUS_DIRECTION_UP,
|
||||
UI_FOCUS_DIRECTION_DOWN,
|
||||
UI_FOCUS_DIRECTION_LEFT,
|
||||
UI_FOCUS_DIRECTION_RIGHT
|
||||
} uifocusdirection_t;
|
||||
|
||||
/**
|
||||
* Callback invoked when a focus item's selected cell changes.
|
||||
*
|
||||
* @param item The focus item that changed.
|
||||
*/
|
||||
typedef bool_t (*uifocusitemcallback_t)(const uifocusitem_t *item);
|
||||
|
||||
/**
|
||||
* Callback invoked when a direction is pressed or held-repeated while
|
||||
* this item is focused, before the default cell-to-cell movement is
|
||||
* applied.
|
||||
*
|
||||
* @param item The focus item that is currently focused.
|
||||
* @param direction The direction that was pressed.
|
||||
* @returns True if the direction was fully handled and the default
|
||||
* grid movement should be skipped; false to fall through to it.
|
||||
*/
|
||||
typedef bool_t (*uifocusitemdirectioncallback_t)(
|
||||
const uifocusitem_t *item,
|
||||
const uifocusdirection_t direction
|
||||
);
|
||||
|
||||
/**
|
||||
* A single entry on the UI focus stack. Tracks the focused cell
|
||||
* within a grid of cols x rows, and the current x/y position.
|
||||
*/
|
||||
struct uifocusitem_s {
|
||||
uint8_t cols;
|
||||
uint8_t rows;
|
||||
uint8_t x;
|
||||
uint8_t y;
|
||||
uifocusitemcallback_t selected;
|
||||
uifocusitemcallback_t changed;
|
||||
uifocusitemcallback_t closed;
|
||||
uifocusitemdirectioncallback_t direction;
|
||||
void *user;
|
||||
};
|
||||
@@ -6,5 +6,7 @@
|
||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
PUBLIC
|
||||
uiframe.c
|
||||
uisettings.c
|
||||
uiconfirm.c
|
||||
)
|
||||
|
||||
add_subdirectory(settings)
|
||||
@@ -0,0 +1,13 @@
|
||||
# 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
|
||||
uisettings.c
|
||||
uisettingsgeneral.c
|
||||
uisettingsinput.c
|
||||
uisettingsdisplay.c
|
||||
uisettingsaudio.c
|
||||
)
|
||||
@@ -0,0 +1,248 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "uisettings.h"
|
||||
#include "uisettingsgeneral.h"
|
||||
#include "uisettingsinput.h"
|
||||
#include "uisettingsdisplay.h"
|
||||
#include "uisettingsaudio.h"
|
||||
#include "ui/frame/uiframe.h"
|
||||
#include "ui/frame/uiconfirm.h"
|
||||
#include "util/memory.h"
|
||||
#include "display/text/text.h"
|
||||
#include "display/screen/screen.h"
|
||||
#include "assert/assert.h"
|
||||
#include "locale/localemanager.h"
|
||||
#include "asset/loader/locale/assetlocaleloader.h"
|
||||
|
||||
uisettings_t UI_SETTINGS;
|
||||
|
||||
uisettingstabdef_t UI_SETTINGS_TAB_DEFS[UI_SETTINGS_TAB_COUNT] = {
|
||||
{
|
||||
.localeId = "ui.settings.tabs.general",
|
||||
.init = uiSettingsGeneralInit,
|
||||
.menu = (uimenu_t *)&UI_SETTINGS.data,
|
||||
.load = uiSettingsGeneralLoad,
|
||||
.apply = uiSettingsGeneralApply,
|
||||
.hasChanges = uiSettingsGeneralHasChanges
|
||||
},
|
||||
|
||||
{
|
||||
.localeId = "ui.settings.tabs.input",
|
||||
.init = uiSettingsInputInit,
|
||||
.menu = (uimenu_t *)&UI_SETTINGS.data,
|
||||
.load = uiSettingsInputLoad,
|
||||
.apply = uiSettingsInputApply,
|
||||
.hasChanges = uiSettingsInputHasChanges
|
||||
},
|
||||
|
||||
{
|
||||
.localeId = "ui.settings.tabs.display",
|
||||
.init = uiSettingsDisplayInit,
|
||||
.menu = (uimenu_t *)&UI_SETTINGS.data,
|
||||
.load = uiSettingsDisplayLoad,
|
||||
.apply = uiSettingsDisplayApply,
|
||||
.hasChanges = uiSettingsDisplayHasChanges
|
||||
},
|
||||
|
||||
{
|
||||
.localeId = "ui.settings.tabs.audio",
|
||||
.init = uiSettingsAudioInit,
|
||||
.menu = (uimenu_t *)&UI_SETTINGS.data,
|
||||
.load = uiSettingsAudioLoad,
|
||||
.apply = uiSettingsAudioApply,
|
||||
.hasChanges = uiSettingsAudioHasChanges
|
||||
}
|
||||
};
|
||||
|
||||
static uisettingstabdef_t UI_SETTINGS_TOP_DEF = {
|
||||
.menu = &UI_SETTINGS.tabsMenu,
|
||||
.load = uiSettingsLoad,
|
||||
.hasChanges = uiSettingsHasChanges
|
||||
};
|
||||
|
||||
static uisettingstabdef_t *UI_SETTINGS_PENDING_DISCARD = NULL;
|
||||
static uisettingstabdef_t *UI_SETTINGS_ACTIVE_TAB = NULL;
|
||||
|
||||
errorret_t uiSettingsActivateTab(uisettingstabdef_t *def) {
|
||||
if(def == UI_SETTINGS_ACTIVE_TAB) errorOk();
|
||||
|
||||
errorChain(def->init(&UI_SETTINGS.data));
|
||||
def->menu->user = def;
|
||||
def->load();
|
||||
UI_SETTINGS_ACTIVE_TAB = def;
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
void uiSettingsTabChanged(
|
||||
const uimenu_t *menu,
|
||||
const uint8_t index,
|
||||
const uimenuitem_t *item
|
||||
) {
|
||||
if(index >= UI_SETTINGS_TAB_COUNT) return;
|
||||
errorCatch(uiSettingsActivateTab(&UI_SETTINGS_TAB_DEFS[index]));
|
||||
}
|
||||
|
||||
void uiSettingsTabSelected(
|
||||
const uimenu_t *menu,
|
||||
const uint8_t index,
|
||||
const uimenuitem_t *item
|
||||
) {
|
||||
if(index >= UI_SETTINGS_TAB_COUNT) return;
|
||||
|
||||
uisettingstabdef_t *def = &UI_SETTINGS_TAB_DEFS[index];
|
||||
errorCatch(uiSettingsActivateTab(def));
|
||||
uiMenuOpen(def->menu);
|
||||
}
|
||||
|
||||
errorret_t uiSettingsInit(void) {
|
||||
memoryZero(&UI_SETTINGS, sizeof(uisettings_t));
|
||||
UI_SETTINGS_ACTIVE_TAB = NULL;
|
||||
|
||||
for(uint8_t i = 0; i < UI_SETTINGS_TAB_COUNT; i++) {
|
||||
errorChain(assetLocaleGetString(
|
||||
&LOCALE.entry->data.locale,
|
||||
UI_SETTINGS_TAB_DEFS[i].localeId,
|
||||
0,
|
||||
UI_SETTINGS.tabLabels[i],
|
||||
UI_SETTINGS_TAB_LABEL_MAX
|
||||
));
|
||||
}
|
||||
|
||||
MENU_BEGIN(
|
||||
&UI_SETTINGS.tabsMenu, UI_SETTINGS.tabs, uiSettingsTabSelected,
|
||||
uiSettingsDiscardMenuClosed, uiSettingsTabChanged
|
||||
);
|
||||
for(uint8_t i = 0; i < UI_SETTINGS_TAB_COUNT; i++) {
|
||||
MENU_TAB(UI_SETTINGS.tabLabels[i]);
|
||||
}
|
||||
MENU_END(UI_SETTINGS.tabs, menuIndex);
|
||||
|
||||
UI_SETTINGS.tabsMenu.user = &UI_SETTINGS_TOP_DEF;
|
||||
|
||||
errorChain(assetLocaleGetString(
|
||||
&LOCALE.entry->data.locale,
|
||||
"ui.settings.apply",
|
||||
0,
|
||||
UI_SETTINGS.applyLabel,
|
||||
UI_SETTINGS_APPLY_LABEL_MAX
|
||||
));
|
||||
|
||||
errorChain(uiSettingsActivateTab(&UI_SETTINGS_TAB_DEFS[0]));
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t uiSettingsDraw(void) {
|
||||
if(!uiMenuIsActive(&UI_SETTINGS.tabsMenu)) errorOk();
|
||||
|
||||
const float_t width = SCREEN.width;
|
||||
const float_t height = SCREEN.height;
|
||||
const float_t x = (float_t)SCREEN.scanX +
|
||||
((float_t)SCREEN.scanWidth - width) * 0.5f;
|
||||
const float_t y = (float_t)SCREEN.scanY +
|
||||
((float_t)SCREEN.scanHeight - height) * 0.5f;
|
||||
|
||||
errorChain(uiFrameDrawCached(&UI_SETTINGS.frameCache, x, y, width, height));
|
||||
|
||||
const float_t contentX = x + UI_FRAME_START_X;
|
||||
const float_t contentY = y + UI_FRAME_START_Y;
|
||||
const float_t contentWidth = width - (UI_FRAME_START_X * 2);
|
||||
const float_t contentHeight = height - (UI_FRAME_START_Y * 2);
|
||||
|
||||
errorChain(uiMenuDraw(
|
||||
&UI_SETTINGS.tabsMenu,
|
||||
contentX,
|
||||
contentY,
|
||||
contentWidth,
|
||||
contentHeight
|
||||
));
|
||||
|
||||
const float_t tabsRowHeight = (float_t)FONT_DEFAULT.tileset->tileHeight;
|
||||
const float_t pageY = contentY + tabsRowHeight + UI_FRAME_PADDING_Y;
|
||||
const float_t pageHeight = contentHeight - tabsRowHeight - UI_FRAME_PADDING_Y;
|
||||
|
||||
if(UI_SETTINGS_ACTIVE_TAB != NULL) {
|
||||
errorChain(uiMenuDraw(
|
||||
UI_SETTINGS_ACTIVE_TAB->menu, contentX, pageY, contentWidth, pageHeight
|
||||
));
|
||||
}
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t uiSettingsUpdate(void) {
|
||||
if(!UI_SETTINGS_PENDING_DISCARD) errorOk();
|
||||
|
||||
uisettingstabdef_t *def = UI_SETTINGS_PENDING_DISCARD;
|
||||
UI_SETTINGS_PENDING_DISCARD = NULL;
|
||||
|
||||
return uiSettingsDiscardUpdate(def);
|
||||
}
|
||||
|
||||
bool_t uiSettingsIsOpen(void) {
|
||||
return uiMenuIsActive(&UI_SETTINGS.tabsMenu);
|
||||
}
|
||||
|
||||
void uiSettingsOpen() {
|
||||
uiSettingsLoad();
|
||||
uiMenuOpen(&UI_SETTINGS.tabsMenu);
|
||||
}
|
||||
|
||||
void uiSettingsClose() {
|
||||
uiMenuClose(&UI_SETTINGS.tabsMenu);
|
||||
}
|
||||
|
||||
errorret_t uiSettingsDispose(void) {
|
||||
errorOk();
|
||||
}
|
||||
|
||||
void uiSettingsLoad(void) {
|
||||
if(UI_SETTINGS_ACTIVE_TAB != NULL) UI_SETTINGS_ACTIVE_TAB->load();
|
||||
}
|
||||
|
||||
void uiSettingsApply(void) {
|
||||
if(UI_SETTINGS_ACTIVE_TAB != NULL) UI_SETTINGS_ACTIVE_TAB->apply();
|
||||
}
|
||||
|
||||
bool_t uiSettingsHasChanges(void) {
|
||||
return UI_SETTINGS_ACTIVE_TAB != NULL && UI_SETTINGS_ACTIVE_TAB->hasChanges();
|
||||
}
|
||||
|
||||
void uiSettingsDiscardMenuClosed(const uimenu_t *menu) {
|
||||
uisettingstabdef_t *def = (uisettingstabdef_t *)menu->user;
|
||||
if(def->hasChanges()) UI_SETTINGS_PENDING_DISCARD = def;
|
||||
}
|
||||
|
||||
errorret_t uiSettingsDiscardUpdate(uisettingstabdef_t *def) {
|
||||
#define UI_SETTINGS_DISCARD_TEXT_MAX 128
|
||||
char_t text[UI_SETTINGS_DISCARD_TEXT_MAX];
|
||||
errorChain(assetLocaleGetString(
|
||||
&LOCALE.entry->data.locale,
|
||||
"ui.confirm.discard_changes",
|
||||
0,
|
||||
text,
|
||||
UI_SETTINGS_DISCARD_TEXT_MAX
|
||||
));
|
||||
#undef UI_SETTINGS_DISCARD_TEXT_MAX
|
||||
|
||||
uiConfirmOpen(text, uiSettingsDiscardConfirmResult, def);
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
void uiSettingsDiscardConfirmResult(const bool_t result, void *user) {
|
||||
uisettingstabdef_t *def = (uisettingstabdef_t *)user;
|
||||
|
||||
if(!result) {
|
||||
uiMenuOpen(def->menu);
|
||||
return;
|
||||
}
|
||||
|
||||
def->load();
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
/**
|
||||
* 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 "ui/widget/uimenu.h"
|
||||
#include "ui/frame/uiframe.h"
|
||||
#include "uisettingsdata.h"
|
||||
|
||||
#define UI_SETTINGS_TAB_COUNT 4
|
||||
#define UI_SETTINGS_TAB_LABEL_MAX 32
|
||||
#define UI_SETTINGS_APPLY_LABEL_MAX 32
|
||||
|
||||
typedef struct {
|
||||
const char_t *localeId;
|
||||
errorret_t (*init)(uisettingsdata_t *data);
|
||||
uimenu_t *menu;
|
||||
void (*load)(void);
|
||||
void (*apply)(void);
|
||||
bool_t (*hasChanges)(void);
|
||||
} uisettingstabdef_t;
|
||||
|
||||
typedef struct {
|
||||
uimenu_t tabsMenu;
|
||||
uimenuitem_t tabs[UI_SETTINGS_TAB_COUNT];
|
||||
char_t tabLabels[UI_SETTINGS_TAB_COUNT][UI_SETTINGS_TAB_LABEL_MAX];
|
||||
char_t applyLabel[UI_SETTINGS_APPLY_LABEL_MAX];
|
||||
uisettingsdata_t data;
|
||||
uiframecache_t frameCache;
|
||||
} uisettings_t;
|
||||
|
||||
extern uisettings_t UI_SETTINGS;
|
||||
extern uisettingstabdef_t UI_SETTINGS_TAB_DEFS[UI_SETTINGS_TAB_COUNT];
|
||||
|
||||
/**
|
||||
* Menu closed callback that flags a pending discard-changes
|
||||
* confirmation if the menu's settings page/panel reports unsaved
|
||||
* changes. Pass this directly as a settings page menu's closed
|
||||
* callback. The pending def is picked up by uiSettingsUpdate on the
|
||||
* next tick.
|
||||
*
|
||||
* @param menu The menu that was just closed.
|
||||
*/
|
||||
void uiSettingsDiscardMenuClosed(const uimenu_t *menu);
|
||||
|
||||
/**
|
||||
* Shows the discard-changes confirmation dialog for def. Called once
|
||||
* by uiSettingsUpdate when uiSettingsDiscardMenuClosed has flagged a
|
||||
* pending def.
|
||||
*
|
||||
* @param def The tab/panel def to show the confirmation for.
|
||||
* @return Any error that occurs.
|
||||
*/
|
||||
errorret_t uiSettingsDiscardUpdate(uisettingstabdef_t *def);
|
||||
|
||||
/**
|
||||
* Callback passed to uiConfirmOpen by uiSettingsDiscardUpdate. Reopens
|
||||
* the menu if the user chose to keep editing, otherwise reloads the
|
||||
* widgets back to the actual engine values.
|
||||
*
|
||||
* @param result True if the user confirmed discarding changes, false
|
||||
* to keep editing.
|
||||
* @param user The uisettingstabdef_t* this confirmation belongs to.
|
||||
*/
|
||||
void uiSettingsDiscardConfirmResult(const bool_t result, void *user);
|
||||
|
||||
/**
|
||||
* Initializes the settings panel and all of its category pages.
|
||||
*
|
||||
* @return Any error that occurs.
|
||||
*/
|
||||
errorret_t uiSettingsInit(void);
|
||||
|
||||
/**
|
||||
* Draws the settings panel. No-op when not visible.
|
||||
*
|
||||
* @return Any error that occurs.
|
||||
*/
|
||||
errorret_t uiSettingsDraw(void);
|
||||
|
||||
/**
|
||||
* Updates the settings panel. Shows a discard-changes confirmation if
|
||||
* the panel was just closed with unsaved changes.
|
||||
*
|
||||
* @return Any error that occurs.
|
||||
*/
|
||||
errorret_t uiSettingsUpdate(void);
|
||||
|
||||
/**
|
||||
* Returns true when the settings panel is currently open.
|
||||
*
|
||||
* @returns True if open.
|
||||
*/
|
||||
bool_t uiSettingsIsOpen(void);
|
||||
|
||||
/**
|
||||
* Opens the settings panel. No-op when already open.
|
||||
*/
|
||||
void uiSettingsOpen();
|
||||
|
||||
/**
|
||||
* Closes the settings panel. No-op when already closed.
|
||||
*/
|
||||
void uiSettingsClose();
|
||||
|
||||
/**
|
||||
* Disposes of the settings panel.
|
||||
*
|
||||
* @return Any error that occurs.
|
||||
*/
|
||||
errorret_t uiSettingsDispose(void);
|
||||
|
||||
/**
|
||||
* Loads every category page's widgets from the current engine settings.
|
||||
* Called automatically by uiSettingsOpen.
|
||||
*/
|
||||
void uiSettingsLoad(void);
|
||||
|
||||
/**
|
||||
* Applies every category page's widget values to the engine settings.
|
||||
* Called automatically when a page's own Apply button is pressed.
|
||||
*/
|
||||
void uiSettingsApply(void);
|
||||
|
||||
/**
|
||||
* Returns whether any category page has a widget value that differs
|
||||
* from what was loaded.
|
||||
*
|
||||
* @returns True if there are unsaved changes.
|
||||
*/
|
||||
bool_t uiSettingsHasChanges(void);
|
||||
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "uisettingsaudio.h"
|
||||
#include "uisettings.h"
|
||||
#include "assert/assert.h"
|
||||
#include "util/memory.h"
|
||||
|
||||
void uiSettingsAudioSelected(
|
||||
const uimenu_t *menu,
|
||||
const uint8_t index,
|
||||
const uimenuitem_t *item
|
||||
) {
|
||||
if(index != UI_SETTINGS_AUDIO_INDEX_APPLY) return;
|
||||
uiSettingsAudioApply();
|
||||
}
|
||||
|
||||
errorret_t uiSettingsAudioInit(uisettingsdata_t *data) {
|
||||
uisettingsaudio_t *audio = &data->audio;
|
||||
memoryZero(audio, sizeof(uisettingsaudio_t));
|
||||
|
||||
MENU_BEGIN(
|
||||
&audio->menu, audio->items,
|
||||
uiSettingsAudioSelected, uiSettingsDiscardMenuClosed, NULL
|
||||
);
|
||||
MENU_LABEL("No audio settings yet");
|
||||
MENU_BUTTON(UI_SETTINGS.applyLabel);
|
||||
|
||||
MENU_END(audio->items, 1);
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
void uiSettingsAudioLoad(void) {
|
||||
}
|
||||
|
||||
void uiSettingsAudioApply(void) {
|
||||
uiMenuClose(&UI_SETTINGS.data.audio.menu);
|
||||
}
|
||||
|
||||
bool_t uiSettingsAudioHasChanges(void) {
|
||||
return false;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* 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 "uisettingsdata.h"
|
||||
|
||||
/**
|
||||
* Initializes the audio settings page into the shared settings data
|
||||
* union.
|
||||
*
|
||||
* @param data The shared settings data union to initialize into.
|
||||
* @return Any error that occurs.
|
||||
*/
|
||||
errorret_t uiSettingsAudioInit(uisettingsdata_t *data);
|
||||
|
||||
/**
|
||||
* Loads the page's widgets from the current engine audio settings.
|
||||
*/
|
||||
void uiSettingsAudioLoad(void);
|
||||
|
||||
/**
|
||||
* Applies the page's widget values to the engine audio settings.
|
||||
*/
|
||||
void uiSettingsAudioApply(void);
|
||||
|
||||
/**
|
||||
* Returns whether any widget's value differs from what was loaded.
|
||||
*
|
||||
* @returns True if there are unsaved changes.
|
||||
*/
|
||||
bool_t uiSettingsAudioHasChanges(void);
|
||||
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* 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 "ui/widget/uimenu.h"
|
||||
|
||||
#define UI_SETTINGS_GENERAL_ITEM_COUNT 3
|
||||
#define UI_SETTINGS_GENERAL_INDEX_LANGUAGE 0
|
||||
#define UI_SETTINGS_GENERAL_INDEX_LANGUAGE_DETAIL 1
|
||||
#define UI_SETTINGS_GENERAL_INDEX_APPLY 2
|
||||
#define UI_SETTINGS_GENERAL_LOCALE_COUNT 3
|
||||
#define UI_SETTINGS_GENERAL_LABEL_MAX 32
|
||||
#define UI_SETTINGS_GENERAL_INFO_MAX 64
|
||||
|
||||
typedef struct {
|
||||
uimenu_t menu;
|
||||
uimenuitem_t items[UI_SETTINGS_GENERAL_ITEM_COUNT];
|
||||
const char_t *localeNames[UI_SETTINGS_GENERAL_LOCALE_COUNT];
|
||||
char_t languageLabel[UI_SETTINGS_GENERAL_LABEL_MAX];
|
||||
char_t labelInfo[UI_SETTINGS_GENERAL_INFO_MAX];
|
||||
} uisettingsgeneral_t;
|
||||
|
||||
#define UI_SETTINGS_INPUT_ITEM_COUNT 2
|
||||
#define UI_SETTINGS_INPUT_LABEL_MAX 32
|
||||
#define UI_SETTINGS_INPUT_INDEX_DEADZONE 0
|
||||
#define UI_SETTINGS_INPUT_INDEX_APPLY 1
|
||||
|
||||
typedef struct {
|
||||
uimenu_t menu;
|
||||
uimenuitem_t items[UI_SETTINGS_INPUT_ITEM_COUNT];
|
||||
char_t deadzoneLabel[UI_SETTINGS_INPUT_LABEL_MAX];
|
||||
} uisettingsinput_t;
|
||||
|
||||
#define UI_SETTINGS_DISPLAY_ITEM_COUNT 2
|
||||
#define UI_SETTINGS_DISPLAY_INDEX_APPLY 1
|
||||
|
||||
typedef struct {
|
||||
uimenu_t menu;
|
||||
uimenuitem_t items[UI_SETTINGS_DISPLAY_ITEM_COUNT];
|
||||
} uisettingsdisplay_t;
|
||||
|
||||
#define UI_SETTINGS_AUDIO_ITEM_COUNT 2
|
||||
#define UI_SETTINGS_AUDIO_INDEX_APPLY 1
|
||||
|
||||
typedef struct {
|
||||
uimenu_t menu;
|
||||
uimenuitem_t items[UI_SETTINGS_AUDIO_ITEM_COUNT];
|
||||
} uisettingsaudio_t;
|
||||
|
||||
/**
|
||||
* Shared, overlapping storage for every settings tab's page data. Only
|
||||
* one member is ever live at a time -- the active tab's Init writes
|
||||
* into it, overwriting whatever the previously active tab left there.
|
||||
* This trades simultaneous access to every tab for a memory footprint
|
||||
* of only the single largest page.
|
||||
*/
|
||||
typedef union {
|
||||
uisettingsgeneral_t general;
|
||||
uisettingsinput_t input;
|
||||
uisettingsdisplay_t display;
|
||||
uisettingsaudio_t audio;
|
||||
} uisettingsdata_t;
|
||||
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "uisettingsdisplay.h"
|
||||
#include "uisettings.h"
|
||||
#include "assert/assert.h"
|
||||
#include "util/memory.h"
|
||||
|
||||
void uiSettingsDisplaySelected(
|
||||
const uimenu_t *menu,
|
||||
const uint8_t index,
|
||||
const uimenuitem_t *item
|
||||
) {
|
||||
if(index != UI_SETTINGS_DISPLAY_INDEX_APPLY) return;
|
||||
uiSettingsDisplayApply();
|
||||
}
|
||||
|
||||
errorret_t uiSettingsDisplayInit(uisettingsdata_t *data) {
|
||||
uisettingsdisplay_t *display = &data->display;
|
||||
memoryZero(display, sizeof(uisettingsdisplay_t));
|
||||
|
||||
MENU_BEGIN(
|
||||
&display->menu, display->items,
|
||||
uiSettingsDisplaySelected, uiSettingsDiscardMenuClosed, NULL
|
||||
);
|
||||
MENU_LABEL("No display settings yet");
|
||||
MENU_BUTTON(UI_SETTINGS.applyLabel);
|
||||
|
||||
MENU_END(display->items, 1);
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
void uiSettingsDisplayLoad(void) {
|
||||
}
|
||||
|
||||
void uiSettingsDisplayApply(void) {
|
||||
uiMenuClose(&UI_SETTINGS.data.display.menu);
|
||||
}
|
||||
|
||||
bool_t uiSettingsDisplayHasChanges(void) {
|
||||
return false;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* 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 "uisettingsdata.h"
|
||||
|
||||
/**
|
||||
* Initializes the display settings page into the shared settings data
|
||||
* union.
|
||||
*
|
||||
* @param data The shared settings data union to initialize into.
|
||||
* @return Any error that occurs.
|
||||
*/
|
||||
errorret_t uiSettingsDisplayInit(uisettingsdata_t *data);
|
||||
|
||||
/**
|
||||
* Loads the page's widgets from the current engine display settings.
|
||||
*/
|
||||
void uiSettingsDisplayLoad(void);
|
||||
|
||||
/**
|
||||
* Applies the page's widget values to the engine display settings.
|
||||
*/
|
||||
void uiSettingsDisplayApply(void);
|
||||
|
||||
/**
|
||||
* Returns whether any widget's value differs from what was loaded.
|
||||
*
|
||||
* @returns True if there are unsaved changes.
|
||||
*/
|
||||
bool_t uiSettingsDisplayHasChanges(void);
|
||||
@@ -0,0 +1,117 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "uisettingsgeneral.h"
|
||||
#include "uisettings.h"
|
||||
#include "assert/assert.h"
|
||||
#include "util/memory.h"
|
||||
#include "util/string.h"
|
||||
#include "locale/localemanager.h"
|
||||
#include "locale/localeinfo.h"
|
||||
#include "asset/loader/locale/assetlocaleloader.h"
|
||||
|
||||
static const localeinfo_t *const UI_SETTINGS_GENERAL_LOCALES[
|
||||
UI_SETTINGS_GENERAL_LOCALE_COUNT
|
||||
] = {
|
||||
&LOCALE_EN_US,
|
||||
&LOCALE_JP_JP,
|
||||
&LOCALE_ES_MX
|
||||
};
|
||||
|
||||
void uiSettingsGeneralSelected(
|
||||
const uimenu_t *menu,
|
||||
const uint8_t index,
|
||||
const uimenuitem_t *item
|
||||
) {
|
||||
switch(index) {
|
||||
case UI_SETTINGS_GENERAL_INDEX_APPLY:
|
||||
uiSettingsGeneralApply();
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
errorret_t uiSettingsGeneralInit(uisettingsdata_t *data) {
|
||||
uisettingsgeneral_t *general = &data->general;
|
||||
memoryZero(general, sizeof(uisettingsgeneral_t));
|
||||
|
||||
for(uint8_t i = 0; i < UI_SETTINGS_GENERAL_LOCALE_COUNT; i++) {
|
||||
general->localeNames[i] = UI_SETTINGS_GENERAL_LOCALES[i]->name;
|
||||
}
|
||||
|
||||
errorChain(assetLocaleGetString(
|
||||
&LOCALE.entry->data.locale,
|
||||
"ui.settings.general.language",
|
||||
0,
|
||||
general->languageLabel,
|
||||
UI_SETTINGS_GENERAL_LABEL_MAX
|
||||
));
|
||||
errorChain(assetLocaleGetString(
|
||||
&LOCALE.entry->data.locale,
|
||||
"ui.settings.general.language_detail",
|
||||
0,
|
||||
general->labelInfo,
|
||||
UI_SETTINGS_GENERAL_INFO_MAX
|
||||
));
|
||||
|
||||
MENU_BEGIN(
|
||||
&general->menu, general->items,
|
||||
uiSettingsGeneralSelected, uiSettingsDiscardMenuClosed, NULL
|
||||
);
|
||||
|
||||
MENU_DROPDOWN(
|
||||
general->languageLabel, general->localeNames,
|
||||
UI_SETTINGS_GENERAL_LOCALE_COUNT, 0
|
||||
);
|
||||
MENU_LABEL(general->labelInfo);
|
||||
|
||||
MENU_BUTTON(UI_SETTINGS.applyLabel);
|
||||
MENU_END(general->items, 1);
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
void uiSettingsGeneralLoad(void) {
|
||||
uisettingsgeneral_t *general = &UI_SETTINGS.data.general;
|
||||
|
||||
uint8_t index = 0;
|
||||
for(uint8_t i = 0; i < UI_SETTINGS_GENERAL_LOCALE_COUNT; i++) {
|
||||
if(stringCompare(LOCALE.locale->file, UI_SETTINGS_GENERAL_LOCALES[i]->file) != 0) {
|
||||
continue;
|
||||
}
|
||||
index = i;
|
||||
break;
|
||||
}
|
||||
|
||||
uiDropdownSetSelectedIndex(
|
||||
&general->items[UI_SETTINGS_GENERAL_INDEX_LANGUAGE].dropdown, index
|
||||
);
|
||||
}
|
||||
|
||||
void uiSettingsGeneralApply(void) {
|
||||
uisettingsgeneral_t *general = &UI_SETTINGS.data.general;
|
||||
|
||||
uint8_t index = uiDropdownGetSelectedIndex(
|
||||
&general->items[UI_SETTINGS_GENERAL_INDEX_LANGUAGE].dropdown
|
||||
);
|
||||
errorCatch(localeManagerSetLocale(UI_SETTINGS_GENERAL_LOCALES[index]));
|
||||
|
||||
uiMenuClose(&general->menu);
|
||||
}
|
||||
|
||||
bool_t uiSettingsGeneralHasChanges(void) {
|
||||
uisettingsgeneral_t *general = &UI_SETTINGS.data.general;
|
||||
|
||||
uint8_t index = uiDropdownGetSelectedIndex(
|
||||
&general->items[UI_SETTINGS_GENERAL_INDEX_LANGUAGE].dropdown
|
||||
);
|
||||
return stringCompare(
|
||||
LOCALE.locale->file, UI_SETTINGS_GENERAL_LOCALES[index]->file
|
||||
) != 0;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* 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 "uisettingsdata.h"
|
||||
|
||||
/**
|
||||
* Initializes the general settings page into the shared settings data
|
||||
* union.
|
||||
*
|
||||
* @param data The shared settings data union to initialize into.
|
||||
* @return Any error that occurs.
|
||||
*/
|
||||
errorret_t uiSettingsGeneralInit(uisettingsdata_t *data);
|
||||
|
||||
/**
|
||||
* Loads the page's widgets from the current engine general settings.
|
||||
*/
|
||||
void uiSettingsGeneralLoad(void);
|
||||
|
||||
/**
|
||||
* Applies the page's widget values to the engine general settings.
|
||||
*/
|
||||
void uiSettingsGeneralApply(void);
|
||||
|
||||
/**
|
||||
* Returns whether any widget's value differs from what was loaded.
|
||||
*
|
||||
* @returns True if there are unsaved changes.
|
||||
*/
|
||||
bool_t uiSettingsGeneralHasChanges(void);
|
||||
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "uisettingsinput.h"
|
||||
#include "uisettings.h"
|
||||
#include "assert/assert.h"
|
||||
#include "util/memory.h"
|
||||
#include "locale/localemanager.h"
|
||||
#include "asset/loader/locale/assetlocaleloader.h"
|
||||
#include "input/input.h"
|
||||
#include "save/savesettings.h"
|
||||
|
||||
void uiSettingsInputSelected(
|
||||
const uimenu_t *menu,
|
||||
const uint8_t index,
|
||||
const uimenuitem_t *item
|
||||
) {
|
||||
if(index != UI_SETTINGS_INPUT_INDEX_APPLY) return;
|
||||
uiSettingsInputApply();
|
||||
}
|
||||
|
||||
errorret_t uiSettingsInputInit(uisettingsdata_t *data) {
|
||||
uisettingsinput_t *input = &data->input;
|
||||
memoryZero(input, sizeof(uisettingsinput_t));
|
||||
|
||||
MENU_BEGIN(
|
||||
&input->menu, input->items,
|
||||
uiSettingsInputSelected, uiSettingsDiscardMenuClosed, NULL
|
||||
);
|
||||
#ifdef DUSK_INPUT_GAMEPAD
|
||||
errorChain(assetLocaleGetString(
|
||||
&LOCALE.entry->data.locale,
|
||||
"ui.settings.input.deadzone",
|
||||
0,
|
||||
input->deadzoneLabel,
|
||||
UI_SETTINGS_INPUT_LABEL_MAX
|
||||
));
|
||||
MENU_SLIDER_FLOAT(
|
||||
input->deadzoneLabel, INPUT_DEADZONE_DEFAULT, 0.0f, 1.0f, 0.05f
|
||||
);
|
||||
#else
|
||||
MENU_LABEL("No input settings yet");
|
||||
#endif
|
||||
|
||||
MENU_BUTTON(UI_SETTINGS.applyLabel);
|
||||
MENU_END(input->items, 1);
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
void uiSettingsInputLoad(void) {
|
||||
#ifdef DUSK_INPUT_GAMEPAD
|
||||
uiSliderSetFloat(
|
||||
&UI_SETTINGS.data.input.items[UI_SETTINGS_INPUT_INDEX_DEADZONE].slider,
|
||||
INPUT.deadzone
|
||||
);
|
||||
#endif
|
||||
}
|
||||
|
||||
void uiSettingsInputApply(void) {
|
||||
#ifdef DUSK_INPUT_GAMEPAD
|
||||
INPUT.deadzone = uiSliderGetFloat(
|
||||
&UI_SETTINGS.data.input.items[UI_SETTINGS_INPUT_INDEX_DEADZONE].slider
|
||||
);
|
||||
#endif
|
||||
errorCatch(errorPrint(saveSettingsWrite()));
|
||||
uiMenuClose(&UI_SETTINGS.data.input.menu);
|
||||
}
|
||||
|
||||
bool_t uiSettingsInputHasChanges(void) {
|
||||
#ifdef DUSK_INPUT_GAMEPAD
|
||||
if(uiSliderGetFloat(
|
||||
&UI_SETTINGS.data.input.items[UI_SETTINGS_INPUT_INDEX_DEADZONE].slider
|
||||
) != INPUT.deadzone) return true;
|
||||
#endif
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* 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 "uisettingsdata.h"
|
||||
|
||||
/**
|
||||
* Initializes the input settings page into the shared settings data
|
||||
* union.
|
||||
*
|
||||
* @param data The shared settings data union to initialize into.
|
||||
* @return Any error that occurs.
|
||||
*/
|
||||
errorret_t uiSettingsInputInit(uisettingsdata_t *data);
|
||||
|
||||
/**
|
||||
* Loads the page's widgets from the current engine input settings.
|
||||
*/
|
||||
void uiSettingsInputLoad(void);
|
||||
|
||||
/**
|
||||
* Applies the page's widget values to the engine input settings.
|
||||
*/
|
||||
void uiSettingsInputApply(void);
|
||||
|
||||
/**
|
||||
* Returns whether any widget's value differs from what was loaded.
|
||||
*
|
||||
* @returns True if there are unsaved changes.
|
||||
*/
|
||||
bool_t uiSettingsInputHasChanges(void);
|
||||
@@ -0,0 +1,126 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "uiconfirm.h"
|
||||
#include "uiframe.h"
|
||||
#include "assert/assert.h"
|
||||
#include "util/memory.h"
|
||||
#include "util/math.h"
|
||||
#include "display/screen/screen.h"
|
||||
#include "display/text/text.h"
|
||||
#include "display/color.h"
|
||||
#include "display/spritebatch/spritebatch.h"
|
||||
#include "display/texture/texture.h"
|
||||
#include "display/shader/shaderunlit.h"
|
||||
|
||||
#define UI_CONFIRM_BACKDROP_COLOR color4b(0, 0, 0, 160)
|
||||
|
||||
uiconfirm_t UI_CONFIRM;
|
||||
|
||||
void uiConfirmSelected(
|
||||
const uimenu_t *menu,
|
||||
const uint8_t index,
|
||||
const uimenuitem_t *item
|
||||
) {
|
||||
UI_CONFIRM.result = index == UI_CONFIRM_INDEX_CONFIRM;
|
||||
uiConfirmClose();
|
||||
}
|
||||
|
||||
void uiConfirmClosed(const uimenu_t *menu) {
|
||||
if(UI_CONFIRM.callback != NULL) {
|
||||
UI_CONFIRM.callback(UI_CONFIRM.result, UI_CONFIRM.user);
|
||||
}
|
||||
}
|
||||
|
||||
errorret_t uiConfirmInit(void) {
|
||||
memoryZero(&UI_CONFIRM, sizeof(uiconfirm_t));
|
||||
uiLabelInit(&UI_CONFIRM.textLabel, &FONT_DEFAULT);
|
||||
|
||||
MENU_BEGIN(
|
||||
&UI_CONFIRM.menu, UI_CONFIRM.items, uiConfirmSelected, uiConfirmClosed, NULL
|
||||
);
|
||||
MENU_BUTTON("Confirm");
|
||||
MENU_BUTTON("Cancel");
|
||||
|
||||
MENU_END(UI_CONFIRM.items, menuIndex);
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t uiConfirmDraw(void) {
|
||||
if(!uiMenuIsActive(&UI_CONFIRM.menu)) errorOk();
|
||||
|
||||
spritebatchsprite_t backdropSprite = {
|
||||
.min = { 0.0f, 0.0f, 0.0f },
|
||||
.max = { (float_t)SCREEN.width, (float_t)SCREEN.height, 0.0f },
|
||||
.uvMin = { 0.0f, 0.0f },
|
||||
.uvMax = { 1.0f, 1.0f }
|
||||
};
|
||||
shadermaterial_t backdropMaterial = {
|
||||
.unlit = {
|
||||
.color = UI_CONFIRM_BACKDROP_COLOR,
|
||||
.texture = &TEXTURE_WHITE
|
||||
}
|
||||
};
|
||||
errorChain(
|
||||
spriteBatchBuffer(&backdropSprite, 1, &SHADER_UNLIT, backdropMaterial)
|
||||
);
|
||||
|
||||
int32_t textW, textH;
|
||||
uiLabelGetSize(&UI_CONFIRM.textLabel, &textW, &textH);
|
||||
|
||||
float_t rowHeight = (float_t)FONT_DEFAULT.tileset->tileHeight;
|
||||
float_t width = mathMax(
|
||||
(float_t)textW + (UI_FRAME_START_X * 2), UI_CONFIRM_MIN_WIDTH
|
||||
);
|
||||
float_t height = (UI_FRAME_START_Y * 2) + rowHeight + UI_FRAME_PADDING_Y +
|
||||
rowHeight;
|
||||
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;
|
||||
|
||||
errorChain(uiFrameDrawCached(&UI_CONFIRM.frameCache, x, y, width, height));
|
||||
|
||||
float_t contentX = x + UI_FRAME_START_X;
|
||||
float_t contentY = y + UI_FRAME_START_Y;
|
||||
float_t contentWidth = width - (UI_FRAME_START_X * 2);
|
||||
|
||||
errorChain(uiLabelDraw(&UI_CONFIRM.textLabel, contentX, contentY));
|
||||
|
||||
float_t buttonsY = contentY + rowHeight + UI_FRAME_PADDING_Y;
|
||||
errorChain(uiMenuDraw(&UI_CONFIRM.menu, contentX, buttonsY, contentWidth, rowHeight));
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
bool_t uiConfirmIsOpen(void) {
|
||||
return uiMenuIsActive(&UI_CONFIRM.menu);
|
||||
}
|
||||
|
||||
bool_t uiConfirmGetResult(void) {
|
||||
return UI_CONFIRM.result;
|
||||
}
|
||||
|
||||
void uiConfirmOpen(
|
||||
const char_t *question,
|
||||
uiconfirmcallback_t callback,
|
||||
void *user
|
||||
) {
|
||||
assertNotNull(question, "Question cannot be NULL");
|
||||
uiLabelSetText(&UI_CONFIRM.textLabel, question);
|
||||
UI_CONFIRM.callback = callback;
|
||||
UI_CONFIRM.user = user;
|
||||
UI_CONFIRM.result = false;
|
||||
uiMenuOpen(&UI_CONFIRM.menu);
|
||||
}
|
||||
|
||||
void uiConfirmClose(void) {
|
||||
uiMenuClose(&UI_CONFIRM.menu);
|
||||
}
|
||||
|
||||
errorret_t uiConfirmDispose(void) {
|
||||
errorOk();
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
/**
|
||||
* 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 "ui/widget/uimenu.h"
|
||||
#include "ui/widget/uilabel.h"
|
||||
#include "ui/frame/uiframe.h"
|
||||
|
||||
#define UI_CONFIRM_MIN_WIDTH 160.0f
|
||||
#define UI_CONFIRM_INDEX_CONFIRM 0
|
||||
#define UI_CONFIRM_INDEX_CANCEL 1
|
||||
#define UI_CONFIRM_ITEM_COUNT 2
|
||||
|
||||
/**
|
||||
* Callback invoked once a confirm dialog is dismissed.
|
||||
*
|
||||
* @param result True if Confirm was selected, false if Cancel was
|
||||
* selected or the dialog was backed out of.
|
||||
* @param user Arbitrary pointer passed to uiConfirmOpen.
|
||||
*/
|
||||
typedef void (*uiconfirmcallback_t)(const bool_t result, void *user);
|
||||
|
||||
typedef struct {
|
||||
uilabel_t textLabel;
|
||||
uimenu_t menu;
|
||||
uimenuitem_t items[UI_CONFIRM_ITEM_COUNT];
|
||||
uiconfirmcallback_t callback;
|
||||
void *user;
|
||||
bool_t result;
|
||||
uiframecache_t frameCache;
|
||||
} uiconfirm_t;
|
||||
|
||||
extern uiconfirm_t UI_CONFIRM;
|
||||
|
||||
/**
|
||||
* Initializes the confirm dialog.
|
||||
*
|
||||
* @return Any error that occurs.
|
||||
*/
|
||||
errorret_t uiConfirmInit(void);
|
||||
|
||||
/**
|
||||
* Draws the confirm dialog: a semi-transparent black backdrop covering
|
||||
* the whole screen, then its own centered frame with the question text
|
||||
* and Confirm/Cancel buttons. No-op when not open.
|
||||
*
|
||||
* @return Any error that occurs.
|
||||
*/
|
||||
errorret_t uiConfirmDraw(void);
|
||||
|
||||
/**
|
||||
* Returns true when the confirm dialog is currently open.
|
||||
*
|
||||
* @returns True if open.
|
||||
*/
|
||||
bool_t uiConfirmIsOpen(void);
|
||||
|
||||
/**
|
||||
* Returns the result of the most recently dismissed confirm dialog.
|
||||
*
|
||||
* @returns True if Confirm was selected, false otherwise.
|
||||
*/
|
||||
bool_t uiConfirmGetResult(void);
|
||||
|
||||
/**
|
||||
* Opens the confirm dialog with the given question text. callback is
|
||||
* invoked exactly once with the result, whether the dialog was
|
||||
* dismissed by selecting a button or by pressing cancel/back.
|
||||
*
|
||||
* @param question Display text; copied internally, safe to be transient.
|
||||
* @param callback Called with the result once the dialog closes. May be
|
||||
* NULL.
|
||||
* @param user Arbitrary pointer passed through to callback.
|
||||
*/
|
||||
void uiConfirmOpen(
|
||||
const char_t *question,
|
||||
uiconfirmcallback_t callback,
|
||||
void *user
|
||||
);
|
||||
|
||||
/**
|
||||
* Closes the confirm dialog. No-op when already closed.
|
||||
*/
|
||||
void uiConfirmClose(void);
|
||||
|
||||
/**
|
||||
* Disposes of the confirm dialog.
|
||||
*
|
||||
* @return Any error that occurs.
|
||||
*/
|
||||
errorret_t uiConfirmDispose(void);
|
||||
@@ -19,8 +19,8 @@ errorret_t uiFrameInit(void) {
|
||||
memoryZero(&UI_FRAME, sizeof(uiframe_t));
|
||||
|
||||
// Init texture.
|
||||
color_t border = color4b(0, 100, 220, 255);
|
||||
color_t center = color4b(0, 100, 220, 200);
|
||||
color_t border = color4b(0, 50, 255, 255);
|
||||
color_t center = color4b(0, 100, 220, 240);
|
||||
|
||||
for(uint8_t y = 0; y < UI_FRAME_TEXTURE_HEIGHT_POW2; y++) {
|
||||
for(uint8_t x = 0; x < UI_FRAME_TEXTURE_WIDTH_POW2; x++) {
|
||||
@@ -72,6 +72,17 @@ errorret_t uiFrameDraw(
|
||||
}
|
||||
};
|
||||
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 tileH = (float_t)UI_FRAME_BORDER_HEIGHT;
|
||||
|
||||
@@ -112,8 +123,37 @@ errorret_t uiFrameDraw(
|
||||
&UI_FRAME.tileset, 2, 2,
|
||||
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) {
|
||||
@@ -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));
|
||||
return spriteBatchFlush();
|
||||
errorOk();
|
||||
}
|
||||
@@ -75,7 +75,7 @@ errorret_t uiFullboxDraw(uifullbox_t *fullbox) {
|
||||
}
|
||||
};
|
||||
errorChain(spriteBatchBuffer(&sprite, 1, &SHADER_UNLIT, material));
|
||||
return spriteBatchFlush();
|
||||
errorOk();
|
||||
}
|
||||
|
||||
void uiFullboxTransition(
|
||||
@@ -11,7 +11,6 @@
|
||||
#include "time/time.h"
|
||||
#include "display/text/text.h"
|
||||
#include "display/screen/screen.h"
|
||||
#include "display/spritebatch/spritebatch.h"
|
||||
#include "uifullbox.h"
|
||||
|
||||
#define UI_LOADING_TEXT "loading"
|
||||
@@ -20,6 +19,8 @@ uiloading_t UI_LOADING;
|
||||
|
||||
errorret_t uiLoadingInit(void) {
|
||||
memoryZero(&UI_LOADING, sizeof(uiloading_t));
|
||||
uiLabelInit(&UI_LOADING.textLabel, &FONT_DEFAULT);
|
||||
uiLabelSetText(&UI_LOADING.textLabel, UI_LOADING_TEXT);
|
||||
eventInit(
|
||||
&UI_LOADING.onTransitionEnd,
|
||||
UI_LOADING.onTransitionEndCallbacks,
|
||||
@@ -53,7 +54,7 @@ errorret_t uiLoadingDraw(void) {
|
||||
if(alpha <= 0.0f) errorOk();
|
||||
|
||||
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)textW - UI_LOADING_MARGIN;
|
||||
@@ -62,9 +63,10 @@ errorret_t uiLoadingDraw(void) {
|
||||
|
||||
color_t color = COLOR_WHITE;
|
||||
color.a = (uint8_t)(alpha * 255.0f);
|
||||
uiLabelSetColor(&UI_LOADING.textLabel, color);
|
||||
|
||||
errorChain(textDraw(x, y, UI_LOADING_TEXT, color, &FONT_DEFAULT));
|
||||
return spriteBatchFlush();
|
||||
errorChain(uiLabelDraw(&UI_LOADING.textLabel, x, y));
|
||||
errorOk();
|
||||
}
|
||||
|
||||
static void uiLoadingTransition(
|
||||
@@ -8,6 +8,7 @@
|
||||
#pragma once
|
||||
#include "error/error.h"
|
||||
#include "event/event.h"
|
||||
#include "ui/widget/uilabel.h"
|
||||
|
||||
#define UI_LOADING_FADE_DURATION 0.5f
|
||||
#define UI_LOADING_MARGIN 8.0f
|
||||
@@ -20,6 +21,7 @@ typedef struct {
|
||||
eventcallback_t onTransitionEndCallbacks[4];
|
||||
void *onTransitionEndUsers[4];
|
||||
event_t onTransitionEnd;
|
||||
uilabel_t textLabel;
|
||||
} uiloading_t;
|
||||
|
||||
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));
|
||||
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,16 +7,15 @@
|
||||
|
||||
#include "uibutton.h"
|
||||
#include "assert/assert.h"
|
||||
#include "util/memory.h"
|
||||
#include "util/string.h"
|
||||
#include "display/text/text.h"
|
||||
#include "display/color.h"
|
||||
|
||||
void uiButtonInit(uibutton_t *button, const char_t *label) {
|
||||
assertNotNull(button, "Button cannot be NULL");
|
||||
assertNotNull(label, "Label cannot be NULL");
|
||||
memoryZero(button, sizeof(uibutton_t));
|
||||
button->label = label;
|
||||
uiWidgetLabelInit(&button->label, &FONT_DEFAULT);
|
||||
uiWidgetLabelSetText(&button->label, label);
|
||||
button->highlighted = false;
|
||||
}
|
||||
|
||||
bool_t uiButtonIsHighlighted(const uibutton_t *button) {
|
||||
@@ -27,6 +26,9 @@ bool_t uiButtonIsHighlighted(const uibutton_t *button) {
|
||||
void uiButtonSetHighlighted(uibutton_t *button, const bool_t highlighted) {
|
||||
assertNotNull(button, "Button cannot be NULL");
|
||||
button->highlighted = highlighted;
|
||||
uiWidgetLabelSetColor(
|
||||
&button->label, highlighted ? COLOR_RED : COLOR_WHITE
|
||||
);
|
||||
}
|
||||
|
||||
errorret_t uiButtonDraw(
|
||||
@@ -35,10 +37,6 @@ errorret_t uiButtonDraw(
|
||||
const float_t y
|
||||
) {
|
||||
assertNotNull(button, "Button cannot be NULL");
|
||||
errorChain(textDraw(
|
||||
x, y, button->label,
|
||||
button->highlighted ? COLOR_RED : COLOR_WHITE,
|
||||
&FONT_DEFAULT
|
||||
));
|
||||
errorChain(uiWidgetLabelDraw(&button->label, x, y));
|
||||
errorOk();
|
||||
}
|
||||
@@ -7,9 +7,10 @@
|
||||
|
||||
#pragma once
|
||||
#include "error/error.h"
|
||||
#include "ui/widget/uiwidgetlabel.h"
|
||||
|
||||
typedef struct {
|
||||
const char_t *label;
|
||||
uiwidgetlabel_t label;
|
||||
bool_t highlighted;
|
||||
} uibutton_t;
|
||||
|
||||
@@ -16,7 +16,9 @@ void uiCheckboxInit(
|
||||
const char_t *label
|
||||
) {
|
||||
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) {
|
||||
@@ -25,6 +27,7 @@ bool_t uiCheckboxIsChecked(const uicheckbox_t *checkbox) {
|
||||
|
||||
void uiCheckboxSetChecked(uicheckbox_t *checkbox, const bool_t checked) {
|
||||
checkbox->checked = checked;
|
||||
uiCheckboxRebuildLabel(checkbox);
|
||||
}
|
||||
|
||||
void uiCheckboxToggle(uicheckbox_t *checkbox) {
|
||||
@@ -37,6 +40,9 @@ bool_t uiCheckboxIsHighlighted(const uicheckbox_t *checkbox) {
|
||||
|
||||
void uiCheckboxSetHighlighted(uicheckbox_t *checkbox, const bool_t highlighted) {
|
||||
checkbox->highlighted = highlighted;
|
||||
uiWidgetLabelSetColor(
|
||||
&checkbox->label, highlighted ? COLOR_RED : COLOR_WHITE
|
||||
);
|
||||
}
|
||||
|
||||
errorret_t uiCheckboxDraw(
|
||||
@@ -44,15 +50,15 @@ errorret_t uiCheckboxDraw(
|
||||
const float_t x,
|
||||
const float_t y
|
||||
) {
|
||||
color_t color = checkbox->highlighted ? COLOR_RED : COLOR_WHITE;
|
||||
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
|
||||
));
|
||||
|
||||
errorChain(uiWidgetLabelDraw(&checkbox->label, x, y));
|
||||
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
|
||||
#include "error/error.h"
|
||||
#include "ui/widget/uiwidgetlabel.h"
|
||||
|
||||
typedef struct uicheckbox_s {
|
||||
const char_t *label;
|
||||
const char_t *rawLabel;
|
||||
uiwidgetlabel_t label;
|
||||
bool_t checked;
|
||||
bool_t highlighted;
|
||||
} uicheckbox_t;
|
||||
@@ -77,3 +79,12 @@ errorret_t uiCheckboxDraw(
|
||||
const float_t x,
|
||||
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);
|
||||
@@ -0,0 +1,110 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "uidropdown.h"
|
||||
#include "assert/assert.h"
|
||||
#include "util/memory.h"
|
||||
#include "util/string.h"
|
||||
#include "display/text/text.h"
|
||||
#include "display/color.h"
|
||||
|
||||
void uiDropdownInit(
|
||||
uidropdown_t *dropdown,
|
||||
const char_t *label,
|
||||
const char_t *const *options,
|
||||
const uint8_t optionCount,
|
||||
const uint8_t selectedIndex
|
||||
) {
|
||||
assertNotNull(dropdown, "Dropdown cannot be NULL");
|
||||
assertNotNull(label, "Label cannot be NULL");
|
||||
assertNotNull(options, "Options cannot be NULL");
|
||||
assertTrue(optionCount > 0, "Dropdown must have at least one option");
|
||||
|
||||
memoryZero(dropdown, sizeof(uidropdown_t));
|
||||
uiWidgetLabelInit(&dropdown->label, &FONT_DEFAULT);
|
||||
uiWidgetLabelSetText(&dropdown->label, label);
|
||||
uiWidgetLabelInit(&dropdown->value, &FONT_DEFAULT);
|
||||
dropdown->options = options;
|
||||
dropdown->optionCount = optionCount;
|
||||
dropdown->selectedIndex = selectedIndex < optionCount ? selectedIndex : 0;
|
||||
uiDropdownRebuildValue(dropdown);
|
||||
}
|
||||
|
||||
uint8_t uiDropdownGetSelectedIndex(const uidropdown_t *dropdown) {
|
||||
assertNotNull(dropdown, "Dropdown cannot be NULL");
|
||||
return dropdown->selectedIndex;
|
||||
}
|
||||
|
||||
const char_t *uiDropdownGetSelectedOption(const uidropdown_t *dropdown) {
|
||||
assertNotNull(dropdown, "Dropdown cannot be NULL");
|
||||
return dropdown->options[dropdown->selectedIndex];
|
||||
}
|
||||
|
||||
void uiDropdownSetSelectedIndex(uidropdown_t *dropdown, const uint8_t index) {
|
||||
assertNotNull(dropdown, "Dropdown cannot be NULL");
|
||||
assertTrue(index < dropdown->optionCount, "Dropdown index out of range");
|
||||
dropdown->selectedIndex = index;
|
||||
uiDropdownRebuildValue(dropdown);
|
||||
}
|
||||
|
||||
void uiDropdownStepNext(uidropdown_t *dropdown) {
|
||||
assertNotNull(dropdown, "Dropdown cannot be NULL");
|
||||
dropdown->selectedIndex =
|
||||
(dropdown->selectedIndex + 1) % dropdown->optionCount;
|
||||
uiDropdownRebuildValue(dropdown);
|
||||
}
|
||||
|
||||
void uiDropdownStepPrev(uidropdown_t *dropdown) {
|
||||
assertNotNull(dropdown, "Dropdown cannot be NULL");
|
||||
dropdown->selectedIndex = dropdown->selectedIndex == 0 ?
|
||||
dropdown->optionCount - 1 : dropdown->selectedIndex - 1;
|
||||
uiDropdownRebuildValue(dropdown);
|
||||
}
|
||||
|
||||
bool_t uiDropdownIsHighlighted(const uidropdown_t *dropdown) {
|
||||
assertNotNull(dropdown, "Dropdown cannot be NULL");
|
||||
return dropdown->highlighted;
|
||||
}
|
||||
|
||||
void uiDropdownSetHighlighted(
|
||||
uidropdown_t *dropdown,
|
||||
const bool_t highlighted
|
||||
) {
|
||||
assertNotNull(dropdown, "Dropdown cannot be NULL");
|
||||
dropdown->highlighted = highlighted;
|
||||
color_t color = highlighted ? COLOR_RED : COLOR_WHITE;
|
||||
uiWidgetLabelSetColor(&dropdown->label, color);
|
||||
uiWidgetLabelSetColor(&dropdown->value, color);
|
||||
}
|
||||
|
||||
errorret_t uiDropdownDraw(
|
||||
const uidropdown_t *dropdown,
|
||||
const float_t x,
|
||||
const float_t y
|
||||
) {
|
||||
assertNotNull(dropdown, "Dropdown cannot be NULL");
|
||||
|
||||
errorChain(uiWidgetLabelDraw(&dropdown->label, x, y));
|
||||
|
||||
int32_t labelW, labelH;
|
||||
uiWidgetLabelGetSize(&dropdown->label, &labelW, &labelH);
|
||||
|
||||
errorChain(uiWidgetLabelDraw(
|
||||
&dropdown->value, x + (float_t)labelW + UI_DROPDOWN_GAP, y
|
||||
));
|
||||
|
||||
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);
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
/**
|
||||
* 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 "ui/widget/uiwidgetlabel.h"
|
||||
|
||||
#define UI_DROPDOWN_GAP 4.0f
|
||||
|
||||
typedef struct {
|
||||
uiwidgetlabel_t label;
|
||||
uiwidgetlabel_t value;
|
||||
const char_t *const *options;
|
||||
uint8_t optionCount;
|
||||
uint8_t selectedIndex;
|
||||
bool_t highlighted;
|
||||
} uidropdown_t;
|
||||
|
||||
/**
|
||||
* Initializes a dropdown.
|
||||
*
|
||||
* @param dropdown The dropdown to initialize.
|
||||
* @param label Display label.
|
||||
* @param options Array of option display strings; caller-owned, must
|
||||
* outlive the dropdown.
|
||||
* @param optionCount Number of entries in options. Must be > 0.
|
||||
* @param selectedIndex Initial selected option index, clamped to
|
||||
* [0, optionCount - 1].
|
||||
*/
|
||||
void uiDropdownInit(
|
||||
uidropdown_t *dropdown,
|
||||
const char_t *label,
|
||||
const char_t *const *options,
|
||||
const uint8_t optionCount,
|
||||
const uint8_t selectedIndex
|
||||
);
|
||||
|
||||
/**
|
||||
* Returns the index of the currently selected option.
|
||||
*
|
||||
* @param dropdown The dropdown to query.
|
||||
* @returns The selected option index.
|
||||
*/
|
||||
uint8_t uiDropdownGetSelectedIndex(const uidropdown_t *dropdown);
|
||||
|
||||
/**
|
||||
* Returns the currently selected option's display string.
|
||||
*
|
||||
* @param dropdown The dropdown to query.
|
||||
* @returns The selected option string.
|
||||
*/
|
||||
const char_t *uiDropdownGetSelectedOption(const uidropdown_t *dropdown);
|
||||
|
||||
/**
|
||||
* Sets the selected option by index.
|
||||
*
|
||||
* @param dropdown The dropdown to update.
|
||||
* @param index The new selected option index. Must be < optionCount.
|
||||
*/
|
||||
void uiDropdownSetSelectedIndex(uidropdown_t *dropdown, const uint8_t index);
|
||||
|
||||
/**
|
||||
* Selects the next option, wrapping around to the first option past
|
||||
* the last.
|
||||
*
|
||||
* @param dropdown The dropdown to update.
|
||||
*/
|
||||
void uiDropdownStepNext(uidropdown_t *dropdown);
|
||||
|
||||
/**
|
||||
* Selects the previous option, wrapping around to the last option
|
||||
* before the first.
|
||||
*
|
||||
* @param dropdown The dropdown to update.
|
||||
*/
|
||||
void uiDropdownStepPrev(uidropdown_t *dropdown);
|
||||
|
||||
/**
|
||||
* Returns whether the dropdown is highlighted.
|
||||
*
|
||||
* @param dropdown The dropdown to query.
|
||||
* @returns True if highlighted.
|
||||
*/
|
||||
bool_t uiDropdownIsHighlighted(const uidropdown_t *dropdown);
|
||||
|
||||
/**
|
||||
* Sets the highlighted state of the dropdown.
|
||||
*
|
||||
* @param dropdown The dropdown to update.
|
||||
* @param highlighted The new highlighted state.
|
||||
*/
|
||||
void uiDropdownSetHighlighted(
|
||||
uidropdown_t *dropdown,
|
||||
const bool_t highlighted
|
||||
);
|
||||
|
||||
/**
|
||||
* Draws the dropdown at the given screen position: label, then the
|
||||
* currently selected option surrounded by cycle arrows.
|
||||
*
|
||||
* @param dropdown The dropdown to draw.
|
||||
* @param x Screen x position.
|
||||
* @param y Screen y position.
|
||||
* @return Any error that occurs.
|
||||
*/
|
||||
errorret_t uiDropdownDraw(
|
||||
const uidropdown_t *dropdown,
|
||||
const float_t x,
|
||||
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,7 +10,11 @@
|
||||
#include "util/memory.h"
|
||||
#include "display/text/text.h"
|
||||
#include "display/color.h"
|
||||
#include "ui/widget/uiwidgetlabel.h"
|
||||
#include "ui/widget/uibutton.h"
|
||||
#include "ui/widget/uitab.h"
|
||||
#include "ui/widget/uislider.h"
|
||||
#include "ui/widget/uidropdown.h"
|
||||
|
||||
void uiMenuInit(
|
||||
uimenu_t *menu,
|
||||
@@ -61,6 +65,7 @@ void uiMenuOpen(uimenu_t *menu) {
|
||||
uiMenuFocusSelected,
|
||||
uiMenuFocusChanged,
|
||||
uiMenuFocusClosed,
|
||||
uiMenuFocusDirection,
|
||||
menu
|
||||
);
|
||||
}
|
||||
@@ -98,12 +103,8 @@ errorret_t uiMenuDraw(
|
||||
|
||||
if(item->type == UI_MENU_WIDGET_TYPE_LABEL) {
|
||||
if(col > 0) { row++; col = 0; }
|
||||
errorChain(textDraw(
|
||||
x,
|
||||
y + (float_t)row * rowHeight,
|
||||
item->label,
|
||||
COLOR_WHITE,
|
||||
&FONT_DEFAULT
|
||||
errorChain(uiWidgetLabelDraw(
|
||||
&item->label, x, y + (float_t)row * rowHeight
|
||||
));
|
||||
row++;
|
||||
continue;
|
||||
@@ -127,6 +128,18 @@ errorret_t uiMenuDraw(
|
||||
errorChain(uiButtonDraw(&item->button, ix, iy));
|
||||
break;
|
||||
|
||||
case UI_MENU_WIDGET_TYPE_TAB:
|
||||
errorChain(uiTabDraw(&item->tab, ix, iy));
|
||||
break;
|
||||
|
||||
case UI_MENU_WIDGET_TYPE_SLIDER:
|
||||
errorChain(uiSliderDraw(&item->slider, ix, iy));
|
||||
break;
|
||||
|
||||
case UI_MENU_WIDGET_TYPE_DROPDOWN:
|
||||
errorChain(uiDropdownDraw(&item->dropdown, ix, iy));
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -166,12 +179,7 @@ bool_t uiMenuFocusChanged(const uifocusitem_t *focusItem) {
|
||||
item->type == UI_MENU_WIDGET_TYPE_LABEL ||
|
||||
item->type == UI_MENU_WIDGET_TYPE_SPACER
|
||||
) continue;
|
||||
bool_t isActive = (slot == focusSlot);
|
||||
if(item->type == UI_MENU_WIDGET_TYPE_CHECKBOX) {
|
||||
uiCheckboxSetHighlighted(&item->checkbox, isActive);
|
||||
} else if(item->type == UI_MENU_WIDGET_TYPE_BUTTON) {
|
||||
uiButtonSetHighlighted(&item->button, isActive);
|
||||
}
|
||||
uiMenuItemSetHighlighted(item, slot == focusSlot);
|
||||
slot++;
|
||||
}
|
||||
|
||||
@@ -188,10 +196,59 @@ bool_t uiMenuFocusClosed(const uifocusitem_t *focusItem) {
|
||||
assertNotNull(focusItem->user, "Focus item user cannot be NULL");
|
||||
uimenu_t *menu = (uimenu_t *)focusItem->user;
|
||||
menu->focusItem = NULL;
|
||||
|
||||
for(uint8_t i = 0; i < menu->itemCount; i++) {
|
||||
uiMenuItemSetHighlighted(&menu->items[i], false);
|
||||
}
|
||||
|
||||
if(menu->closed != NULL) menu->closed(menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool_t uiMenuFocusDirection(
|
||||
const uifocusitem_t *focusItem,
|
||||
const uifocusdirection_t direction
|
||||
) {
|
||||
assertNotNull(focusItem, "Focus item cannot be NULL");
|
||||
assertNotNull(focusItem->user, "Focus item user cannot be NULL");
|
||||
if(direction != UI_FOCUS_DIRECTION_LEFT && direction != UI_FOCUS_DIRECTION_RIGHT) {
|
||||
return false;
|
||||
}
|
||||
|
||||
uimenu_t *menu = (uimenu_t *)focusItem->user;
|
||||
uint8_t slot = focusItem->y * menu->columns + focusItem->x;
|
||||
uint8_t index = uiMenuFocusSlotToIndex(menu, slot);
|
||||
if(index == 0xFF) return false;
|
||||
|
||||
uimenuitem_t *item = &menu->items[index];
|
||||
bool_t right = direction == UI_FOCUS_DIRECTION_RIGHT;
|
||||
|
||||
if(item->type == UI_MENU_WIDGET_TYPE_SLIDER) {
|
||||
if(right) {
|
||||
uiSliderStepUp(&item->slider);
|
||||
} else {
|
||||
uiSliderStepDown(&item->slider);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if(item->type == UI_MENU_WIDGET_TYPE_CHECKBOX) {
|
||||
uiCheckboxToggle(&item->checkbox);
|
||||
return true;
|
||||
}
|
||||
|
||||
if(item->type == UI_MENU_WIDGET_TYPE_DROPDOWN) {
|
||||
if(right) {
|
||||
uiDropdownStepNext(&item->dropdown);
|
||||
} else {
|
||||
uiDropdownStepPrev(&item->dropdown);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
uint8_t uiMenuFocusableCount(const uimenu_t *menu) {
|
||||
assertNotNull(menu, "Menu cannot be NULL");
|
||||
uint8_t count = 0;
|
||||
@@ -213,3 +270,24 @@ uint8_t uiMenuFocusSlotToIndex(const uimenu_t *menu, const uint8_t slot) {
|
||||
}
|
||||
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) {
|
||||
if(item->type == UI_MENU_WIDGET_TYPE_CHECKBOX) {
|
||||
uiCheckboxSetHighlighted(&item->checkbox, highlighted);
|
||||
} else if(item->type == UI_MENU_WIDGET_TYPE_BUTTON) {
|
||||
uiButtonSetHighlighted(&item->button, highlighted);
|
||||
} else if(item->type == UI_MENU_WIDGET_TYPE_TAB) {
|
||||
uiTabSetActive(&item->tab, highlighted);
|
||||
} else if(item->type == UI_MENU_WIDGET_TYPE_SLIDER) {
|
||||
uiSliderSetHighlighted(&item->slider, highlighted);
|
||||
} else if(item->type == UI_MENU_WIDGET_TYPE_DROPDOWN) {
|
||||
uiDropdownSetHighlighted(&item->dropdown, highlighted);
|
||||
}
|
||||
}
|
||||
@@ -8,10 +8,12 @@
|
||||
#pragma once
|
||||
#include "error/error.h"
|
||||
#include "ui/focus/uifocus.h"
|
||||
#include "ui/widget/uiwidgetlabel.h"
|
||||
#include "ui/widget/uibutton.h"
|
||||
#include "ui/widget/uicheckbox.h"
|
||||
|
||||
#define UI_MENU_LABEL_MAX UI_CHECKBOX_LABEL_MAX
|
||||
#include "ui/widget/uitab.h"
|
||||
#include "ui/widget/uislider.h"
|
||||
#include "ui/widget/uidropdown.h"
|
||||
|
||||
typedef struct uimenu_s uimenu_t;
|
||||
|
||||
@@ -21,14 +23,20 @@ typedef enum {
|
||||
UI_MENU_WIDGET_TYPE_SPACER,
|
||||
UI_MENU_WIDGET_TYPE_CHECKBOX,
|
||||
UI_MENU_WIDGET_TYPE_BUTTON,
|
||||
UI_MENU_WIDGET_TYPE_TAB,
|
||||
UI_MENU_WIDGET_TYPE_SLIDER,
|
||||
UI_MENU_WIDGET_TYPE_DROPDOWN,
|
||||
} uimenuwidgettype_t;
|
||||
|
||||
typedef struct {
|
||||
uimenuwidgettype_t type;
|
||||
union {
|
||||
char_t *label;
|
||||
uiwidgetlabel_t label;
|
||||
uicheckbox_t checkbox;
|
||||
uibutton_t button;
|
||||
uitab_t tab;
|
||||
uislider_t slider;
|
||||
uidropdown_t dropdown;
|
||||
};
|
||||
} uimenuitem_t;
|
||||
|
||||
@@ -55,6 +63,8 @@ typedef struct uimenu_s {
|
||||
uimenuselectedcallback_t selected;
|
||||
uimenuclosedcallback_t closed;
|
||||
uimenuchangedcallback_t changed;
|
||||
|
||||
void *user;
|
||||
} uimenu_t;
|
||||
|
||||
/**
|
||||
@@ -157,6 +167,25 @@ uint8_t uiMenuFocusableCount(const uimenu_t *menu);
|
||||
*/
|
||||
uint8_t uiMenuFocusSlotToIndex(const uimenu_t *menu, const uint8_t slot);
|
||||
|
||||
/**
|
||||
* Sets the highlighted/active state on a menu item, dispatching to the
|
||||
* matching widget's setter based on its type. No-op for label/spacer
|
||||
* items and any other type with no highlight concept.
|
||||
*
|
||||
* @param item The item to update.
|
||||
* @param highlighted The new highlighted/active state.
|
||||
*/
|
||||
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.
|
||||
*
|
||||
@@ -179,4 +208,85 @@ bool_t uiMenuFocusChanged(const uifocusitem_t *focusItem);
|
||||
* @param focusItem The active focus item; user field must point to uimenu_t.
|
||||
* @returns True.
|
||||
*/
|
||||
bool_t uiMenuFocusClosed(const uifocusitem_t *focusItem);
|
||||
bool_t uiMenuFocusClosed(const uifocusitem_t *focusItem);
|
||||
|
||||
/**
|
||||
* Internal focus callback - gives the focused item's widget a chance to
|
||||
* handle LEFT/RIGHT itself (e.g. a slider adjusting its value) before
|
||||
* falling back to the default cell-to-cell movement.
|
||||
*
|
||||
* @param focusItem The active focus item; user field must point to uimenu_t.
|
||||
* @param direction The direction that was pressed or held.
|
||||
* @returns True if the focused widget handled the direction.
|
||||
*/
|
||||
bool_t uiMenuFocusDirection(
|
||||
const uifocusitem_t *focusItem,
|
||||
const uifocusdirection_t direction
|
||||
);
|
||||
|
||||
// Helper macros
|
||||
#define MENU_BEGIN(menuPtr, itemsArray, selected, closed, changed) \
|
||||
uimenu_t *menu = (menuPtr); \
|
||||
uiMenuInit(menu, selected, closed, changed); \
|
||||
menu->items = (itemsArray); \
|
||||
uint8_t menuIndex = 0; \
|
||||
uint8_t menuCapacity = sizeof(itemsArray) / sizeof((itemsArray)[0])
|
||||
|
||||
#define MENU_LABEL(text) \
|
||||
assertTrue(menuIndex < menuCapacity, "Menu item count exceeds capacity"); \
|
||||
menu->items[menuIndex].type = UI_MENU_WIDGET_TYPE_LABEL; \
|
||||
uiMenuLabelInit(&menu->items[menuIndex], text); \
|
||||
++menuIndex
|
||||
|
||||
#define MENU_SPACER() \
|
||||
assertTrue(menuIndex < menuCapacity, "Menu item count exceeds capacity"); \
|
||||
menu->items[menuIndex].type = UI_MENU_WIDGET_TYPE_SPACER; \
|
||||
++menuIndex
|
||||
|
||||
#define MENU_CHECKBOX(label) \
|
||||
assertTrue(menuIndex < menuCapacity, "Menu item count exceeds capacity"); \
|
||||
menu->items[menuIndex].type = UI_MENU_WIDGET_TYPE_CHECKBOX; \
|
||||
uiCheckboxInit(&menu->items[menuIndex].checkbox, label); \
|
||||
++menuIndex
|
||||
|
||||
#define MENU_BUTTON(label) \
|
||||
assertTrue(menuIndex < menuCapacity, "Menu item count exceeds capacity"); \
|
||||
menu->items[menuIndex].type = UI_MENU_WIDGET_TYPE_BUTTON; \
|
||||
uiButtonInit(&menu->items[menuIndex].button, label); \
|
||||
++menuIndex
|
||||
|
||||
#define MENU_TAB(label) \
|
||||
assertTrue(menuIndex < menuCapacity, "Menu item count exceeds capacity"); \
|
||||
menu->items[menuIndex].type = UI_MENU_WIDGET_TYPE_TAB; \
|
||||
uiTabInit(&menu->items[menuIndex].tab, label); \
|
||||
++menuIndex
|
||||
|
||||
#define MENU_SLIDER_FLOAT(label, value, min, max, step) \
|
||||
assertTrue(menuIndex < menuCapacity, "Menu item count exceeds capacity"); \
|
||||
menu->items[menuIndex].type = UI_MENU_WIDGET_TYPE_SLIDER; \
|
||||
uiSliderInitFloat( \
|
||||
&menu->items[menuIndex].slider, label, value, min, max, step \
|
||||
); \
|
||||
++menuIndex
|
||||
|
||||
#define MENU_SLIDER_INT(label, value, min, max, step) \
|
||||
assertTrue(menuIndex < menuCapacity, "Menu item count exceeds capacity"); \
|
||||
menu->items[menuIndex].type = UI_MENU_WIDGET_TYPE_SLIDER; \
|
||||
uiSliderInitInt( \
|
||||
&menu->items[menuIndex].slider, label, value, min, max, step \
|
||||
); \
|
||||
++menuIndex
|
||||
|
||||
#define MENU_DROPDOWN(label, options, optionCount, selectedIndex) \
|
||||
assertTrue(menuIndex < menuCapacity, "Menu item count exceeds capacity"); \
|
||||
menu->items[menuIndex].type = UI_MENU_WIDGET_TYPE_DROPDOWN; \
|
||||
uiDropdownInit( \
|
||||
&menu->items[menuIndex].dropdown, label, options, optionCount, \
|
||||
selectedIndex \
|
||||
); \
|
||||
++menuIndex
|
||||
|
||||
#define MENU_END(itemsArray, columns) \
|
||||
uiMenuSetItems(menu, (itemsArray), menuIndex, (columns))
|
||||
|
||||
//EOF
|
||||
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "uiscrolling.h"
|
||||
#include "assert/assert.h"
|
||||
|
||||
void uiScrollingInit(uiscrolling_t *scrolling) {
|
||||
assertNotNull(scrolling, "Scrolling container cannot be NULL");
|
||||
// Nothing to initialize yet -- uiscrolling_t is currently a
|
||||
// placeholder with no fields.
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* 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 {
|
||||
|
||||
} uiscrolling_t;
|
||||
|
||||
/**
|
||||
* Initializes a scrolling container. Currently a placeholder -- no
|
||||
* scrolling behavior is implemented yet.
|
||||
*
|
||||
* @param scrolling The scrolling container to initialize.
|
||||
*/
|
||||
void uiScrollingInit(uiscrolling_t *scrolling);
|
||||
@@ -0,0 +1,282 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "uislider.h"
|
||||
#include "assert/assert.h"
|
||||
#include "util/memory.h"
|
||||
#include "util/math.h"
|
||||
#include "util/string.h"
|
||||
#include "display/text/text.h"
|
||||
#include "display/color.h"
|
||||
#include "display/spritebatch/spritebatch.h"
|
||||
#include "display/texture/texture.h"
|
||||
#include "display/shader/shaderunlit.h"
|
||||
|
||||
void uiSliderInitFloat(
|
||||
uislider_t *slider,
|
||||
const char_t *label,
|
||||
const float_t value,
|
||||
const float_t min,
|
||||
const float_t max,
|
||||
const float_t step
|
||||
) {
|
||||
assertNotNull(slider, "Slider cannot be NULL");
|
||||
assertNotNull(label, "Label cannot be NULL");
|
||||
assertTrue(max > min, "Slider max must be greater than min");
|
||||
assertTrue(step > 0.0f, "Slider step must be greater than zero");
|
||||
|
||||
memoryZero(slider, sizeof(uislider_t));
|
||||
uiWidgetLabelInit(&slider->label, &FONT_DEFAULT);
|
||||
uiWidgetLabelSetText(&slider->label, label);
|
||||
uiWidgetLabelInit(&slider->valueLabel, &FONT_DEFAULT);
|
||||
slider->type = UI_SLIDER_TYPE_FLOAT;
|
||||
slider->min.f = min;
|
||||
slider->max.f = max;
|
||||
slider->step.f = step;
|
||||
slider->value.f = mathClamp(value, min, max);
|
||||
uiSliderRebuildValueLabel(slider);
|
||||
uiSliderRebuildGeometry(slider);
|
||||
}
|
||||
|
||||
void uiSliderInitInt(
|
||||
uislider_t *slider,
|
||||
const char_t *label,
|
||||
const int32_t value,
|
||||
const int32_t min,
|
||||
const int32_t max,
|
||||
const int32_t step
|
||||
) {
|
||||
assertNotNull(slider, "Slider cannot be NULL");
|
||||
assertNotNull(label, "Label cannot be NULL");
|
||||
assertTrue(max > min, "Slider max must be greater than min");
|
||||
assertTrue(step > 0, "Slider step must be greater than zero");
|
||||
|
||||
memoryZero(slider, sizeof(uislider_t));
|
||||
uiWidgetLabelInit(&slider->label, &FONT_DEFAULT);
|
||||
uiWidgetLabelSetText(&slider->label, label);
|
||||
uiWidgetLabelInit(&slider->valueLabel, &FONT_DEFAULT);
|
||||
slider->type = UI_SLIDER_TYPE_INT;
|
||||
slider->min.i = min;
|
||||
slider->max.i = max;
|
||||
slider->step.i = step;
|
||||
slider->value.i = mathClamp(value, min, max);
|
||||
uiSliderRebuildValueLabel(slider);
|
||||
uiSliderRebuildGeometry(slider);
|
||||
}
|
||||
|
||||
float_t uiSliderGetFloat(const uislider_t *slider) {
|
||||
assertNotNull(slider, "Slider cannot be NULL");
|
||||
return slider->type == UI_SLIDER_TYPE_INT ?
|
||||
(float_t)slider->value.i : slider->value.f;
|
||||
}
|
||||
|
||||
int32_t uiSliderGetInt(const uislider_t *slider) {
|
||||
assertNotNull(slider, "Slider cannot be NULL");
|
||||
assertTrue(
|
||||
slider->type == UI_SLIDER_TYPE_INT, "Slider is not an int slider"
|
||||
);
|
||||
return slider->value.i;
|
||||
}
|
||||
|
||||
void uiSliderSetFloat(uislider_t *slider, const float_t value) {
|
||||
assertNotNull(slider, "Slider cannot be NULL");
|
||||
assertTrue(
|
||||
slider->type == UI_SLIDER_TYPE_FLOAT, "Slider is not a float slider"
|
||||
);
|
||||
slider->value.f = mathClamp(value, slider->min.f, slider->max.f);
|
||||
uiSliderRebuildValueLabel(slider);
|
||||
uiSliderRebuildGeometry(slider);
|
||||
}
|
||||
|
||||
void uiSliderSetInt(uislider_t *slider, const int32_t value) {
|
||||
assertNotNull(slider, "Slider cannot be NULL");
|
||||
assertTrue(
|
||||
slider->type == UI_SLIDER_TYPE_INT, "Slider is not an int slider"
|
||||
);
|
||||
slider->value.i = mathClamp(value, slider->min.i, slider->max.i);
|
||||
uiSliderRebuildValueLabel(slider);
|
||||
uiSliderRebuildGeometry(slider);
|
||||
}
|
||||
|
||||
void uiSliderStepUp(uislider_t *slider) {
|
||||
assertNotNull(slider, "Slider cannot be NULL");
|
||||
if(slider->type == UI_SLIDER_TYPE_INT) {
|
||||
int32_t next = slider->value.i + slider->step.i;
|
||||
uiSliderSetInt(slider, next > slider->max.i ? slider->min.i : next);
|
||||
} else {
|
||||
float_t next = slider->value.f + slider->step.f;
|
||||
uiSliderSetFloat(slider, next > slider->max.f ? slider->min.f : next);
|
||||
}
|
||||
}
|
||||
|
||||
void uiSliderStepDown(uislider_t *slider) {
|
||||
assertNotNull(slider, "Slider cannot be NULL");
|
||||
if(slider->type == UI_SLIDER_TYPE_INT) {
|
||||
int32_t next = slider->value.i - slider->step.i;
|
||||
uiSliderSetInt(slider, next < slider->min.i ? slider->max.i : next);
|
||||
} else {
|
||||
float_t next = slider->value.f - slider->step.f;
|
||||
uiSliderSetFloat(slider, next < slider->min.f ? slider->max.f : next);
|
||||
}
|
||||
}
|
||||
|
||||
float_t uiSliderGetRatio(const uislider_t *slider) {
|
||||
assertNotNull(slider, "Slider cannot be NULL");
|
||||
if(slider->type == UI_SLIDER_TYPE_INT) {
|
||||
if(slider->max.i == slider->min.i) return 0.0f;
|
||||
return (float_t)(slider->value.i - slider->min.i) /
|
||||
(float_t)(slider->max.i - slider->min.i);
|
||||
}
|
||||
|
||||
if(slider->max.f == slider->min.f) return 0.0f;
|
||||
return (slider->value.f - slider->min.f) / (slider->max.f - slider->min.f);
|
||||
}
|
||||
|
||||
int32_t uiSliderGetStepCount(const uislider_t *slider) {
|
||||
assertNotNull(slider, "Slider cannot be NULL");
|
||||
if(slider->type != UI_SLIDER_TYPE_INT || slider->step.i <= 0) return 0;
|
||||
return (slider->max.i - slider->min.i) / slider->step.i;
|
||||
}
|
||||
|
||||
bool_t uiSliderIsHighlighted(const uislider_t *slider) {
|
||||
assertNotNull(slider, "Slider cannot be NULL");
|
||||
return slider->highlighted;
|
||||
}
|
||||
|
||||
void uiSliderSetHighlighted(uislider_t *slider, const bool_t highlighted) {
|
||||
assertNotNull(slider, "Slider cannot be NULL");
|
||||
slider->highlighted = highlighted;
|
||||
color_t color = highlighted ? COLOR_RED : COLOR_WHITE;
|
||||
uiWidgetLabelSetColor(&slider->label, color);
|
||||
uiWidgetLabelSetColor(&slider->valueLabel, color);
|
||||
}
|
||||
|
||||
errorret_t uiSliderDraw(
|
||||
const uislider_t *slider,
|
||||
const float_t x,
|
||||
const float_t y
|
||||
) {
|
||||
assertNotNull(slider, "Slider cannot be NULL");
|
||||
|
||||
errorChain(uiWidgetLabelDraw(&slider->label, x, y));
|
||||
|
||||
shadermaterial_t trackMaterial = {
|
||||
.unlit = {
|
||||
.color = COLOR_DARK_GRAY,
|
||||
.texture = &TEXTURE_WHITE
|
||||
}
|
||||
};
|
||||
spritebatchsprite_t track = spriteBatchSpriteTranslate(
|
||||
&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);
|
||||
slider->cachedFillVisible = fillWidth > 0.0f;
|
||||
if(slider->cachedFillVisible) {
|
||||
slider->cachedFill = (spritebatchsprite_t){
|
||||
.min = { trackX, trackY, 0.0f },
|
||||
.max = { trackX + fillWidth, trackY + UI_SLIDER_TRACK_HEIGHT, 0.0f },
|
||||
.uvMin = { 0.0f, 0.0f },
|
||||
.uvMax = { 1.0f, 1.0f }
|
||||
};
|
||||
}
|
||||
|
||||
int32_t stepCount = uiSliderGetStepCount(slider);
|
||||
if(stepCount > 0 && stepCount < UI_SLIDER_STEP_MARKERS_MAX) {
|
||||
for(int32_t i = 0; i <= stepCount; i++) {
|
||||
float_t markerX = trackX +
|
||||
UI_SLIDER_TRACK_WIDTH * (float_t)i / (float_t)stepCount;
|
||||
slider->cachedMarkers[i] = (spritebatchsprite_t){
|
||||
.min = {
|
||||
markerX - UI_SLIDER_STEP_MARKER_WIDTH * 0.5f,
|
||||
trackY - UI_SLIDER_STEP_MARKER_OVERHANG,
|
||||
0.0f
|
||||
},
|
||||
.max = {
|
||||
markerX + UI_SLIDER_STEP_MARKER_WIDTH * 0.5f,
|
||||
trackY + UI_SLIDER_TRACK_HEIGHT + UI_SLIDER_STEP_MARKER_OVERHANG,
|
||||
0.0f
|
||||
},
|
||||
.uvMin = { 0.0f, 0.0f },
|
||||
.uvMax = { 1.0f, 1.0f }
|
||||
};
|
||||
}
|
||||
slider->cachedMarkerCount = stepCount + 1;
|
||||
} else {
|
||||
slider->cachedMarkerCount = 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
/**
|
||||
* 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 "ui/widget/uiwidgetlabel.h"
|
||||
#include "display/spritebatch/spritebatchsprite.h"
|
||||
|
||||
#define UI_SLIDER_TRACK_WIDTH 80.0f
|
||||
#define UI_SLIDER_TRACK_HEIGHT 4.0f
|
||||
#define UI_SLIDER_STEP_MARKER_WIDTH 2.0f
|
||||
#define UI_SLIDER_STEP_MARKER_OVERHANG 1.0f
|
||||
#define UI_SLIDER_GAP 4.0f
|
||||
|
||||
/**
|
||||
* Number of discrete steps below which an int slider renders individual
|
||||
* step markers on its track instead of a smooth fill.
|
||||
*/
|
||||
#define UI_SLIDER_STEP_MARKERS_MAX 10
|
||||
|
||||
typedef enum {
|
||||
UI_SLIDER_TYPE_FLOAT,
|
||||
UI_SLIDER_TYPE_INT,
|
||||
|
||||
UI_SLIDER_TYPE_COUNT
|
||||
} uislidertype_t;
|
||||
|
||||
typedef union {
|
||||
float_t f;
|
||||
int32_t i;
|
||||
} uislidervalue_t;
|
||||
|
||||
typedef struct {
|
||||
uiwidgetlabel_t label;
|
||||
uiwidgetlabel_t valueLabel;
|
||||
uislidertype_t type;
|
||||
uislidervalue_t value;
|
||||
uislidervalue_t min;
|
||||
uislidervalue_t max;
|
||||
uislidervalue_t step;
|
||||
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;
|
||||
|
||||
/**
|
||||
* Initializes a floating point slider.
|
||||
*
|
||||
* @param slider The slider to initialize.
|
||||
* @param label Display label.
|
||||
* @param value Initial value, clamped to [min, max].
|
||||
* @param min Minimum value.
|
||||
* @param max Maximum value.
|
||||
* @param step The amount a single step moves the value by.
|
||||
*/
|
||||
void uiSliderInitFloat(
|
||||
uislider_t *slider,
|
||||
const char_t *label,
|
||||
const float_t value,
|
||||
const float_t min,
|
||||
const float_t max,
|
||||
const float_t step
|
||||
);
|
||||
|
||||
/**
|
||||
* Initializes an integer slider.
|
||||
*
|
||||
* @param slider The slider to initialize.
|
||||
* @param label Display label.
|
||||
* @param value Initial value, clamped to [min, max].
|
||||
* @param min Minimum value.
|
||||
* @param max Maximum value.
|
||||
* @param step The amount a single step moves the value by.
|
||||
*/
|
||||
void uiSliderInitInt(
|
||||
uislider_t *slider,
|
||||
const char_t *label,
|
||||
const int32_t value,
|
||||
const int32_t min,
|
||||
const int32_t max,
|
||||
const int32_t step
|
||||
);
|
||||
|
||||
/**
|
||||
* Returns the slider's current value as a float. Works for both slider
|
||||
* types; int values are widened to float.
|
||||
*
|
||||
* @param slider The slider to query.
|
||||
* @returns The current value.
|
||||
*/
|
||||
float_t uiSliderGetFloat(const uislider_t *slider);
|
||||
|
||||
/**
|
||||
* Returns the slider's current value as an int. Only valid for
|
||||
* UI_SLIDER_TYPE_INT sliders.
|
||||
*
|
||||
* @param slider The slider to query.
|
||||
* @returns The current value.
|
||||
*/
|
||||
int32_t uiSliderGetInt(const uislider_t *slider);
|
||||
|
||||
/**
|
||||
* Sets the slider's value, clamped to [min, max]. Only valid for
|
||||
* UI_SLIDER_TYPE_FLOAT sliders.
|
||||
*
|
||||
* @param slider The slider to update.
|
||||
* @param value The new value.
|
||||
*/
|
||||
void uiSliderSetFloat(uislider_t *slider, const float_t value);
|
||||
|
||||
/**
|
||||
* Sets the slider's value, clamped to [min, max]. Only valid for
|
||||
* UI_SLIDER_TYPE_INT sliders.
|
||||
*
|
||||
* @param slider The slider to update.
|
||||
* @param value The new value.
|
||||
*/
|
||||
void uiSliderSetInt(uislider_t *slider, const int32_t value);
|
||||
|
||||
/**
|
||||
* Moves the slider's value up by one step, wrapping around to its min
|
||||
* if the step would exceed its max.
|
||||
*
|
||||
* @param slider The slider to update.
|
||||
*/
|
||||
void uiSliderStepUp(uislider_t *slider);
|
||||
|
||||
/**
|
||||
* Moves the slider's value down by one step, wrapping around to its
|
||||
* max if the step would go below its min.
|
||||
*
|
||||
* @param slider The slider to update.
|
||||
*/
|
||||
void uiSliderStepDown(uislider_t *slider);
|
||||
|
||||
/**
|
||||
* Returns the slider's current value normalized to a 0..1 range based
|
||||
* on its min/max.
|
||||
*
|
||||
* @param slider The slider to query.
|
||||
* @returns The normalized value.
|
||||
*/
|
||||
float_t uiSliderGetRatio(const uislider_t *slider);
|
||||
|
||||
/**
|
||||
* Returns the number of discrete steps between min and max. Always 0
|
||||
* for float sliders.
|
||||
*
|
||||
* @param slider The slider to query.
|
||||
* @returns The step count.
|
||||
*/
|
||||
int32_t uiSliderGetStepCount(const uislider_t *slider);
|
||||
|
||||
/**
|
||||
* Returns whether the slider is highlighted.
|
||||
*
|
||||
* @param slider The slider to query.
|
||||
* @returns True if highlighted.
|
||||
*/
|
||||
bool_t uiSliderIsHighlighted(const uislider_t *slider);
|
||||
|
||||
/**
|
||||
* Sets the highlighted state of the slider.
|
||||
*
|
||||
* @param slider The slider to update.
|
||||
* @param highlighted The new highlighted state.
|
||||
*/
|
||||
void uiSliderSetHighlighted(uislider_t *slider, const bool_t highlighted);
|
||||
|
||||
/**
|
||||
* Draws the slider at the given screen position: label, then track,
|
||||
* then the current value as text. Int sliders with fewer than
|
||||
* UI_SLIDER_STEP_MARKERS_MAX discrete steps render a marker per step
|
||||
* instead of a plain track.
|
||||
*
|
||||
* @param slider The slider to draw.
|
||||
* @param x Screen x position.
|
||||
* @param y Screen y position.
|
||||
* @return Any error that occurs.
|
||||
*/
|
||||
errorret_t uiSliderDraw(
|
||||
const uislider_t *slider,
|
||||
const float_t x,
|
||||
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);
|
||||
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "uitab.h"
|
||||
#include "assert/assert.h"
|
||||
#include "display/text/text.h"
|
||||
#include "display/color.h"
|
||||
#include "display/spritebatch/spritebatch.h"
|
||||
#include "display/texture/texture.h"
|
||||
#include "display/shader/shaderunlit.h"
|
||||
|
||||
void uiTabInit(uitab_t *tab, const char_t *label) {
|
||||
assertNotNull(tab, "Tab cannot be NULL");
|
||||
assertNotNull(label, "Label cannot be NULL");
|
||||
uiWidgetLabelInit(&tab->label, &FONT_DEFAULT);
|
||||
uiWidgetLabelSetText(&tab->label, label);
|
||||
tab->active = false;
|
||||
uiTabRebuildBackground(tab);
|
||||
}
|
||||
|
||||
bool_t uiTabIsActive(const uitab_t *tab) {
|
||||
assertNotNull(tab, "Tab cannot be NULL");
|
||||
return tab->active;
|
||||
}
|
||||
|
||||
void uiTabSetActive(uitab_t *tab, const bool_t active) {
|
||||
assertNotNull(tab, "Tab cannot be NULL");
|
||||
tab->active = active;
|
||||
}
|
||||
|
||||
errorret_t uiTabDraw(
|
||||
const uitab_t *tab,
|
||||
const float_t x,
|
||||
const float_t y
|
||||
) {
|
||||
assertNotNull(tab, "Tab cannot be NULL");
|
||||
|
||||
spritebatchsprite_t sprite = spriteBatchSpriteTranslate(
|
||||
&tab->cachedBackground, x, y
|
||||
);
|
||||
shadermaterial_t material = {
|
||||
.unlit = {
|
||||
.color = tab->active ? COLOR_GREEN : COLOR_RED,
|
||||
.texture = &TEXTURE_WHITE
|
||||
}
|
||||
};
|
||||
errorChain(spriteBatchBuffer(&sprite, 1, &SHADER_UNLIT, material));
|
||||
|
||||
errorChain(uiWidgetLabelDraw(&tab->label, x, y));
|
||||
|
||||
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 }
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* 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 "ui/widget/uiwidgetlabel.h"
|
||||
#include "display/spritebatch/spritebatchsprite.h"
|
||||
|
||||
typedef struct {
|
||||
uiwidgetlabel_t label;
|
||||
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;
|
||||
|
||||
/**
|
||||
* Initializes a tab.
|
||||
*
|
||||
* @param tab The tab to initialize.
|
||||
* @param label Display label.
|
||||
*/
|
||||
void uiTabInit(uitab_t *tab, const char_t *label);
|
||||
|
||||
/**
|
||||
* Returns whether the tab is active.
|
||||
*
|
||||
* @param tab The tab to query.
|
||||
* @returns True if active.
|
||||
*/
|
||||
bool_t uiTabIsActive(const uitab_t *tab);
|
||||
|
||||
/**
|
||||
* Sets the active state of the tab.
|
||||
*
|
||||
* @param tab The tab to update.
|
||||
* @param active The new active state.
|
||||
*/
|
||||
void uiTabSetActive(uitab_t *tab, const bool_t active);
|
||||
|
||||
/**
|
||||
* Draws the tab at the given screen position. Active tabs draw with a
|
||||
* green background; inactive tabs draw with a red background.
|
||||
*
|
||||
* @param tab The tab to draw.
|
||||
* @param x Screen x position.
|
||||
* @param y Screen y position.
|
||||
* @return Any error that occurs.
|
||||
*/
|
||||
errorret_t uiTabDraw(
|
||||
const uitab_t *tab,
|
||||
const float_t x,
|
||||
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
|
||||
);
|
||||
@@ -16,19 +16,35 @@
|
||||
#include "display/shader/shaderunlit.h"
|
||||
#include "ui/frame/uiframe.h"
|
||||
|
||||
void uiTextboxInit(uitextbox_t *box) {
|
||||
void uiTextboxInit(
|
||||
uitextbox_t *box,
|
||||
char_t *text,
|
||||
const uint32_t maxLength,
|
||||
uitextboxline_t *lines,
|
||||
const uint32_t linesMax
|
||||
) {
|
||||
assertNotNull(box, "Textbox cannot be NULL");
|
||||
assertNotNull(text, "Text buffer cannot be NULL");
|
||||
assertTrue(maxLength >= 1, "maxLength must be at least 1");
|
||||
assertNotNull(lines, "Lines buffer cannot be NULL");
|
||||
assertTrue(linesMax >= 1, "linesMax must be at least 1");
|
||||
memoryZero(box, sizeof(uitextbox_t));
|
||||
box->text = text;
|
||||
box->maxLength = maxLength;
|
||||
box->lines = lines;
|
||||
box->linesMax = linesMax;
|
||||
box->glyphsBuiltForPage = -1;
|
||||
}
|
||||
|
||||
void uiTextboxSetText(uitextbox_t *box, const char_t *text) {
|
||||
assertNotNull(box, "Textbox cannot be NULL");
|
||||
assertNotNull(text, "Text cannot be NULL");
|
||||
stringCopy(box->text, text, UI_TEXTBOX_TEXT_MAX);
|
||||
stringCopy(box->text, text, box->maxLength);
|
||||
box->currentPage = 0;
|
||||
box->scroll = 0;
|
||||
box->layoutWidth = 0.0f;
|
||||
box->layoutHeight = 0.0f;
|
||||
box->glyphsBuiltForPage = -1;
|
||||
}
|
||||
|
||||
void uiTextboxBuildLayout(
|
||||
@@ -38,6 +54,7 @@ void uiTextboxBuildLayout(
|
||||
) {
|
||||
assertNotNull(box, "Textbox cannot be NULL");
|
||||
|
||||
box->glyphsBuiltForPage = -1;
|
||||
box->layoutWidth = width;
|
||||
box->layoutHeight = height;
|
||||
box->lineCount = 0;
|
||||
@@ -60,12 +77,12 @@ void uiTextboxBuildLayout(
|
||||
char_t *src = box->text;
|
||||
int32_t i = 0;
|
||||
|
||||
while(src[i] != '\0' && box->lineCount < UI_TEXTBOX_LINES_MAX) {
|
||||
while(src[i] != '\0' && box->lineCount < (int32_t)box->linesMax) {
|
||||
if(src[i] == '\t') {
|
||||
i++;
|
||||
int32_t rem = box->lineCount % box->linesPerPage;
|
||||
int32_t pad = rem > 0 ? box->linesPerPage - rem : 0;
|
||||
while(pad > 0 && box->lineCount < UI_TEXTBOX_LINES_MAX) {
|
||||
while(pad > 0 && box->lineCount < (int32_t)box->linesMax) {
|
||||
box->lines[box->lineCount].start = i;
|
||||
box->lines[box->lineCount].count = 0;
|
||||
box->lineCount++;
|
||||
@@ -124,9 +141,9 @@ void uiTextboxBuildLayout(
|
||||
errorret_t uiTextboxUpdate(uitextbox_t *box) {
|
||||
assertNotNull(box, "Textbox cannot be NULL");
|
||||
|
||||
#ifdef DUSK_TIME_DYNAMIC
|
||||
if(TIME.dynamicUpdate) errorOk();
|
||||
#endif
|
||||
#ifdef DUSK_TIME_DYNAMIC
|
||||
if(TIME.dynamicUpdate) errorOk();
|
||||
#endif
|
||||
|
||||
if(!uiTextboxPageIsComplete(box)) {
|
||||
box->scroll += UI_TEXTBOX_SCROLL_CHARS_PER_TICK;
|
||||
@@ -144,24 +161,41 @@ errorret_t uiTextboxDraw(
|
||||
) {
|
||||
assertNotNull(box, "Textbox cannot be NULL");
|
||||
|
||||
float_t borderW = (float_t)UI_FRAME_BORDER_WIDTH;
|
||||
float_t borderH = (float_t)UI_FRAME_BORDER_HEIGHT;
|
||||
float_t contentX = x + borderW;
|
||||
float_t contentY = y + borderH;
|
||||
float_t contentW = width - 2.0f * borderW;
|
||||
float_t contentH = height - 2.0f * borderH;
|
||||
float_t startX = (float_t)UI_FRAME_START_X;
|
||||
float_t startY = (float_t)UI_FRAME_START_Y;
|
||||
float_t contentX = x + startX;
|
||||
float_t contentY = y + startY;
|
||||
float_t contentW = width - 2.0f * startX;
|
||||
float_t contentH = height - 2.0f * startY;
|
||||
|
||||
if(contentW != box->layoutWidth || contentH != box->layoutHeight) {
|
||||
uiTextboxBuildLayout(box, contentW, contentH);
|
||||
}
|
||||
|
||||
errorChain(uiFrameDraw(x, y, width, height));
|
||||
errorChain(spriteBatchFlush());
|
||||
errorChain(uiFrameDrawCached(&box->frameCache, x, y, width, height));
|
||||
|
||||
if(box->lineCount == 0 || box->text[0] == '\0') errorOk();
|
||||
|
||||
float_t fontW = (float_t)FONT_DEFAULT.tileset->tileWidth;
|
||||
float_t fontH = (float_t)FONT_DEFAULT.tileset->tileHeight;
|
||||
if(box->glyphsBuiltForPage != box->currentPage) {
|
||||
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 = {
|
||||
.unlit = {
|
||||
@@ -169,47 +203,47 @@ errorret_t uiTextboxDraw(
|
||||
.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 pageLast = pageFirst + box->linesPerPage;
|
||||
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];
|
||||
int32_t visible = line->count < charsLeft ? line->count : charsLeft;
|
||||
float_t lineY = contentY +
|
||||
float_t lineY =
|
||||
(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];
|
||||
if(c == ' ') continue;
|
||||
spritebatchsprite_t sprite = textGetSprite(
|
||||
(vec2){ contentX + (float_t)ci * fontW, lineY },
|
||||
c,
|
||||
&FONT_DEFAULT
|
||||
|
||||
assertTrue(
|
||||
box->glyphCount < UI_TEXTBOX_PAGE_GLYPHS_MAX,
|
||||
"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;
|
||||
}
|
||||
|
||||
if(uiTextboxPageIsComplete(box)) {
|
||||
spritebatchsprite_t caret = textGetSprite(
|
||||
(vec2){
|
||||
contentX + contentW - fontW,
|
||||
contentY + contentH - fontH
|
||||
},
|
||||
'v',
|
||||
&FONT_DEFAULT
|
||||
);
|
||||
errorChain(spriteBatchBuffer(&caret, 1, &SHADER_UNLIT, material));
|
||||
}
|
||||
|
||||
errorChain(spriteBatchFlush());
|
||||
errorOk();
|
||||
box->glyphsBuiltForPage = box->currentPage;
|
||||
}
|
||||
|
||||
int32_t uiTextboxGetPageCharCount(const uitextbox_t *box) {
|
||||
@@ -7,22 +7,40 @@
|
||||
|
||||
#pragma once
|
||||
#include "error/error.h"
|
||||
#include "ui/frame/uiframe.h"
|
||||
#include "display/spritebatch/spritebatchsprite.h"
|
||||
|
||||
#define UI_TEXTBOX_TEXT_MAX 1024
|
||||
#define UI_TEXTBOX_LINES_MAX 64
|
||||
#define UI_TEXTBOX_LINES_PER_PAGE_MAX 4
|
||||
#define UI_TEXTBOX_SCROLL_CHARS_PER_TICK 1
|
||||
#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 {
|
||||
int32_t start;
|
||||
int32_t count;
|
||||
} 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 {
|
||||
char_t text[UI_TEXTBOX_TEXT_MAX];
|
||||
spritebatchsprite_t sprite;
|
||||
int32_t revealAt;
|
||||
} uitextboxglyph_t;
|
||||
|
||||
uitextboxline_t lines[UI_TEXTBOX_LINES_MAX];
|
||||
typedef struct {
|
||||
char_t *text;
|
||||
uint32_t maxLength;
|
||||
|
||||
uitextboxline_t *lines;
|
||||
uint32_t linesMax;
|
||||
int32_t lineCount;
|
||||
int32_t charsPerLine;
|
||||
int32_t linesPerPage;
|
||||
@@ -34,14 +52,34 @@ typedef struct {
|
||||
|
||||
int32_t currentPage;
|
||||
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;
|
||||
|
||||
/**
|
||||
* Initializes a textbox, zeroing all state.
|
||||
* Initializes a textbox, zeroing all state and binding it to caller-owned
|
||||
* text and line storage.
|
||||
*
|
||||
* @param box The textbox to initialize.
|
||||
* @param text Caller-owned buffer the textbox copies its text into.
|
||||
* @param maxLength Capacity of text, in characters.
|
||||
* @param lines Caller-owned buffer the textbox lays lines out into.
|
||||
* @param linesMax Capacity of lines, in entries.
|
||||
*/
|
||||
void uiTextboxInit(uitextbox_t *box);
|
||||
void uiTextboxInit(
|
||||
uitextbox_t *box,
|
||||
char_t *text,
|
||||
const uint32_t maxLength,
|
||||
uitextboxline_t *lines,
|
||||
const uint32_t linesMax
|
||||
);
|
||||
|
||||
/**
|
||||
* Copies text into the textbox and marks layout as dirty.
|
||||
@@ -125,3 +163,13 @@ bool_t uiTextboxHasNextPage(const uitextbox_t *box);
|
||||
* @param box The textbox to advance.
|
||||
*/
|
||||
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);
|
||||
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "uitextboxmain.h"
|
||||
#include "ui/focus/uifocus.h"
|
||||
#include "display/screen/screen.h"
|
||||
#include "display/text/text.h"
|
||||
#include "display/color.h"
|
||||
#include "display/spritebatch/spritebatch.h"
|
||||
#include "display/shader/shaderunlit.h"
|
||||
#include "ui/frame/uiframe.h"
|
||||
|
||||
uitextboxmain_t UI_TEXTBOX_MAIN;
|
||||
static uifocusitem_t *focusItem = NULL;
|
||||
|
||||
errorret_t uiTextboxMainInit(void) {
|
||||
uiTextboxInit(
|
||||
&UI_TEXTBOX_MAIN.box,
|
||||
UI_TEXTBOX_MAIN.text, UI_TEXTBOX_MAIN_TEXT_MAX,
|
||||
UI_TEXTBOX_MAIN.lines, UI_TEXTBOX_MAIN_LINES_MAX
|
||||
);
|
||||
errorOk();
|
||||
}
|
||||
|
||||
void uiTextboxMainSetText(const char_t *text) {
|
||||
uiTextboxSetText(&UI_TEXTBOX_MAIN.box, text);
|
||||
if(focusItem != NULL) return;
|
||||
focusItem = uiFocusPush(
|
||||
1, 1,
|
||||
uiTextboxMainFocusSelected,
|
||||
NULL,
|
||||
uiTextboxMainFocusClosed,
|
||||
NULL,
|
||||
NULL
|
||||
);
|
||||
}
|
||||
|
||||
errorret_t uiTextboxMainUpdate(void) {
|
||||
if(focusItem == NULL) errorOk();
|
||||
return uiTextboxUpdate(&UI_TEXTBOX_MAIN.box);
|
||||
}
|
||||
|
||||
errorret_t uiTextboxMainDraw(void) {
|
||||
if(focusItem == NULL) errorOk();
|
||||
float_t fontH = (float_t)FONT_DEFAULT.tileset->tileHeight;
|
||||
float_t h = (float_t)UI_TEXTBOX_MAIN_LINES * fontH +
|
||||
(float_t)(UI_TEXTBOX_MAIN_LINES - 1) * UI_TEXTBOX_LINE_SPACING +
|
||||
2.0f * (float_t)UI_FRAME_START_Y;
|
||||
float_t w = (float_t)SCREEN.scanWidth;
|
||||
float_t x = (float_t)SCREEN.scanX;
|
||||
float_t y = (float_t)(SCREEN.scanY + SCREEN.scanHeight) - h;
|
||||
errorChain(uiTextboxDraw(&UI_TEXTBOX_MAIN.box, x, y, w, h));
|
||||
|
||||
if(!uiTextboxPageIsComplete(&UI_TEXTBOX_MAIN.box)) errorOk();
|
||||
|
||||
float_t fontW = (float_t)FONT_DEFAULT.tileset->tileWidth;
|
||||
float_t contentX = x + (float_t)UI_FRAME_START_X;
|
||||
float_t contentY = y + (float_t)UI_FRAME_START_Y;
|
||||
float_t contentW = w - 2.0f * (float_t)UI_FRAME_START_X;
|
||||
float_t contentH = h - 2.0f * (float_t)UI_FRAME_START_Y;
|
||||
|
||||
shadermaterial_t material = {
|
||||
.unlit = {
|
||||
.color = COLOR_WHITE,
|
||||
.texture = FONT_DEFAULT.texture
|
||||
}
|
||||
};
|
||||
|
||||
spritebatchsprite_t caret = textGetSprite(
|
||||
(vec2){
|
||||
contentX + contentW - fontW,
|
||||
contentY + contentH - fontH
|
||||
},
|
||||
'v',
|
||||
&FONT_DEFAULT
|
||||
);
|
||||
errorChain(spriteBatchBuffer(&caret, 1, &SHADER_UNLIT, material));
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
bool_t uiTextboxMainPageIsComplete(void) {
|
||||
return uiTextboxPageIsComplete(&UI_TEXTBOX_MAIN.box);
|
||||
}
|
||||
|
||||
bool_t uiTextboxMainHasNextPage(void) {
|
||||
return uiTextboxHasNextPage(&UI_TEXTBOX_MAIN.box);
|
||||
}
|
||||
|
||||
void uiTextboxMainNextPage(void) {
|
||||
uiTextboxNextPage(&UI_TEXTBOX_MAIN.box);
|
||||
}
|
||||
|
||||
bool_t uiTextboxMainIsActive(void) {
|
||||
return focusItem != NULL;
|
||||
}
|
||||
|
||||
bool_t uiTextboxMainFocusSelected(const uifocusitem_t *item) {
|
||||
if(!uiTextboxPageIsComplete(&UI_TEXTBOX_MAIN.box)) {
|
||||
UI_TEXTBOX_MAIN.box.scroll =
|
||||
uiTextboxGetPageCharCount(&UI_TEXTBOX_MAIN.box);
|
||||
return true;
|
||||
}
|
||||
|
||||
if(uiTextboxHasNextPage(&UI_TEXTBOX_MAIN.box)) {
|
||||
uiTextboxNextPage(&UI_TEXTBOX_MAIN.box);
|
||||
return true;
|
||||
}
|
||||
|
||||
uiFocusPopItem(focusItem);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool_t uiTextboxMainFocusClosed(const uifocusitem_t *item) {
|
||||
focusItem = NULL;
|
||||
return true;
|
||||
}
|
||||
@@ -6,12 +6,20 @@
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "ui/rpg/uitextbox.h"
|
||||
#include "uitextbox.h"
|
||||
#include "ui/focus/uifocusitem.h"
|
||||
|
||||
#define UI_TEXTBOX_MAIN_LINES 4
|
||||
#define UI_TEXTBOX_MAIN_TEXT_MAX 1024
|
||||
#define UI_TEXTBOX_MAIN_LINES_MAX 64
|
||||
|
||||
extern uitextbox_t UI_TEXTBOX_MAIN;
|
||||
typedef struct {
|
||||
uitextbox_t box;
|
||||
char_t text[UI_TEXTBOX_MAIN_TEXT_MAX];
|
||||
uitextboxline_t lines[UI_TEXTBOX_MAIN_LINES_MAX];
|
||||
} uitextboxmain_t;
|
||||
|
||||
extern uitextboxmain_t UI_TEXTBOX_MAIN;
|
||||
|
||||
/**
|
||||
* Initializes UI_TEXTBOX_MAIN.
|
||||
@@ -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.
@@ -1,21 +0,0 @@
|
||||
// Copyright (c) 2026 Dominic Masters
|
||||
//
|
||||
// This software is released under the MIT License.
|
||||
// https://opensource.org/licenses/MIT
|
||||
|
||||
const platformNames = {
|
||||
[System.PLATFORM_LINUX]: 'Linux',
|
||||
[System.PLATFORM_KNULLI]: 'Knulli',
|
||||
[System.PLATFORM_PSP]: 'PSP',
|
||||
[System.PLATFORM_GAMECUBE]: 'GameCube',
|
||||
[System.PLATFORM_WII]: 'Wii',
|
||||
};
|
||||
|
||||
Console.print('Platform: ' + (platformNames[System.platform] || 'Unknown'));
|
||||
|
||||
UIFullboxOver.setColor(Color.BLACK);
|
||||
|
||||
requireAsync('testscene.js').then(Scene.set).catch(err => {
|
||||
Console.print('Error loading scene: ' + err);
|
||||
Engine.exit();
|
||||
});
|
||||
+49
-43
@@ -10,51 +10,57 @@ msgid "ui.title"
|
||||
msgstr ""
|
||||
"Welcome"
|
||||
|
||||
#: ui/user.c:22
|
||||
msgid "ui.greeting"
|
||||
msgstr "Hello, %s!"
|
||||
#: src/dusk/ui/frame/settings/uisettings.c
|
||||
msgid "ui.settings.tabs.general"
|
||||
msgstr "General"
|
||||
|
||||
#: ui/files.c:40
|
||||
msgid "ui.file_status"
|
||||
msgstr "%s has %d files."
|
||||
#: src/dusk/ui/frame/settings/uisettings.c
|
||||
msgid "ui.settings.tabs.input"
|
||||
msgstr "Input"
|
||||
|
||||
#: ui/cart.c:55
|
||||
msgid "cart.item_count"
|
||||
msgid_plural "cart.item_count"
|
||||
msgstr[0] "%d item"
|
||||
msgstr[1] "%d items (dual)"
|
||||
msgstr[2] "%d items (few)"
|
||||
msgstr[3] "%d items (many)"
|
||||
#: src/dusk/ui/frame/settings/uisettings.c
|
||||
msgid "ui.settings.tabs.display"
|
||||
msgstr "Display"
|
||||
|
||||
#: ui/notifications.c:71
|
||||
msgid ""
|
||||
"ui.multiline_help"
|
||||
msgstr ""
|
||||
"Line one of the help text.\n"
|
||||
"Line two continues here.\n"
|
||||
"Line three ends here."
|
||||
#: src/dusk/ui/frame/settings/uisettings.c
|
||||
msgid "ui.settings.tabs.audio"
|
||||
msgstr "Audio"
|
||||
|
||||
#: ui/errors.c:90
|
||||
msgid ""
|
||||
"error.upload_failed.long"
|
||||
msgstr ""
|
||||
"Upload failed for file \"%s\".\n"
|
||||
"Please try again later or contact support."
|
||||
msgid "ui.settings.input.deadzone"
|
||||
msgstr "Deadzone"
|
||||
|
||||
#: ui/messages.c:110
|
||||
msgid ""
|
||||
"user.invite_status"
|
||||
msgid_plural ""
|
||||
"user.invite_status"
|
||||
msgstr[0] ""
|
||||
"%s invited %d user.\n"
|
||||
"Please review the request."
|
||||
msgstr[1] ""
|
||||
"%s invited %d users (dual).\n"
|
||||
"Please review the requests."
|
||||
msgstr[2] ""
|
||||
"%s invited %d users (few).\n"
|
||||
"Please review the requests."
|
||||
msgstr[3] ""
|
||||
"%s invited %d users (many).\n"
|
||||
"Please review the requests."
|
||||
#: src/dusk/ui/frame/settings/uisettingsgeneral.c
|
||||
msgid "ui.settings.general.language"
|
||||
msgstr "Language"
|
||||
|
||||
msgid "ui.settings.general.language_detail"
|
||||
msgstr "Takes effect after restarting the application."
|
||||
|
||||
#: src/dusk/ui/frame/settings/uisettings.c
|
||||
msgid "ui.settings.apply"
|
||||
msgstr "Apply"
|
||||
|
||||
#: src/dusk/ui/frame/uiconfirm.c
|
||||
msgid "ui.confirm.discard_changes"
|
||||
msgstr "Discard unsaved changes?"
|
||||
|
||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||
msgid "ui.game_menu.characters"
|
||||
msgstr "Characters"
|
||||
|
||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||
msgid "ui.game_menu.items"
|
||||
msgstr "Items"
|
||||
|
||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||
msgid "ui.game_menu.settings"
|
||||
msgstr "Settings"
|
||||
|
||||
msgid "item.potion.name"
|
||||
msgstr "Potion"
|
||||
|
||||
msgid "item.potato.name"
|
||||
msgstr "Potato"
|
||||
|
||||
msgid "item.apple.name"
|
||||
msgstr "Apple"
|
||||
@@ -0,0 +1,70 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: ExampleApp 1.0\n"
|
||||
"Language: es\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n==1 ? 0 : 1);\n"
|
||||
|
||||
#: ui/menu.c:10
|
||||
msgid "ui.title"
|
||||
msgstr ""
|
||||
"Bienvenido"
|
||||
|
||||
#: src/dusk/ui/frame/settings/uisettings.c
|
||||
msgid "ui.settings.tabs.general"
|
||||
msgstr "General"
|
||||
|
||||
#: src/dusk/ui/frame/settings/uisettings.c
|
||||
msgid "ui.settings.tabs.input"
|
||||
msgstr "Entrada"
|
||||
|
||||
#: src/dusk/ui/frame/settings/uisettings.c
|
||||
msgid "ui.settings.tabs.display"
|
||||
msgstr "Pantalla"
|
||||
|
||||
#: src/dusk/ui/frame/settings/uisettings.c
|
||||
msgid "ui.settings.tabs.audio"
|
||||
msgstr "Audio"
|
||||
|
||||
msgid "ui.settings.input.deadzone"
|
||||
msgstr "Deadzone"
|
||||
|
||||
#: src/dusk/ui/frame/settings/uisettingsgeneral.c
|
||||
msgid "ui.settings.general.language"
|
||||
msgstr "Idioma"
|
||||
|
||||
#: src/dusk/ui/frame/settings/uisettingsgeneral.c
|
||||
msgid "ui.settings.general.language_detail"
|
||||
msgstr "Se aplica después de reiniciar la aplicación."
|
||||
|
||||
#: src/dusk/ui/frame/settings/uisettings.c
|
||||
msgid "ui.settings.apply"
|
||||
msgstr "Aplicar"
|
||||
|
||||
#: src/dusk/ui/frame/uiconfirm.c
|
||||
msgid "ui.confirm.discard_changes"
|
||||
msgstr "¿Descartar los cambios no guardados?"
|
||||
|
||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||
msgid "ui.game_menu.characters"
|
||||
msgstr "Personajes"
|
||||
|
||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||
msgid "ui.game_menu.items"
|
||||
msgstr "Objetos"
|
||||
|
||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||
msgid "ui.game_menu.settings"
|
||||
msgstr "Configuración"
|
||||
|
||||
#: src/dusk/rpg/item/item.json
|
||||
msgid "item.potion.name"
|
||||
msgstr "Poción"
|
||||
|
||||
#: src/dusk/rpg/item/item.json
|
||||
msgid "item.potato.name"
|
||||
msgstr "Papa"
|
||||
|
||||
#: src/dusk/rpg/item/item.json
|
||||
msgid "item.apple.name"
|
||||
msgstr "Manzana"
|
||||
@@ -0,0 +1,70 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: ExampleApp 1.0\n"
|
||||
"Language: ja\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Plural-Forms: nplurals=1; plural=(0);\n"
|
||||
|
||||
#: ui/menu.c:10
|
||||
msgid "ui.title"
|
||||
msgstr ""
|
||||
"歓迎"
|
||||
|
||||
#: src/dusk/ui/frame/settings/uisettings.c
|
||||
msgid "ui.settings.tabs.general"
|
||||
msgstr "一般"
|
||||
|
||||
#: src/dusk/ui/frame/settings/uisettings.c
|
||||
msgid "ui.settings.tabs.input"
|
||||
msgstr "入力"
|
||||
|
||||
#: src/dusk/ui/frame/settings/uisettings.c
|
||||
msgid "ui.settings.tabs.display"
|
||||
msgstr "表示"
|
||||
|
||||
#: src/dusk/ui/frame/settings/uisettings.c
|
||||
msgid "ui.settings.tabs.audio"
|
||||
msgstr "オーディオ"
|
||||
|
||||
msgid "ui.settings.input.deadzone"
|
||||
msgstr "デッドゾーン"
|
||||
|
||||
#: src/dusk/ui/frame/settings/uisettingsgeneral.c
|
||||
msgid "ui.settings.general.language"
|
||||
msgstr "言語"
|
||||
|
||||
#: src/dusk/ui/frame/settings/uisettingsgeneral.c
|
||||
msgid "ui.settings.general.language_detail"
|
||||
msgstr "アプリケーションを再起動すると適用されます。"
|
||||
|
||||
#: src/dusk/ui/frame/settings/uisettings.c
|
||||
msgid "ui.settings.apply"
|
||||
msgstr "適用"
|
||||
|
||||
#: src/dusk/ui/frame/uiconfirm.c
|
||||
msgid "ui.confirm.discard_changes"
|
||||
msgstr "未保存の変更を破棄しますか?"
|
||||
|
||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||
msgid "ui.game_menu.characters"
|
||||
msgstr "キャラクター"
|
||||
|
||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||
msgid "ui.game_menu.items"
|
||||
msgstr "アイテム"
|
||||
|
||||
#: src/dusk/ui/frame/game/uigamemenu.c
|
||||
msgid "ui.game_menu.settings"
|
||||
msgstr "設定"
|
||||
|
||||
#: src/dusk/rpg/item/item.json
|
||||
msgid "item.potion.name"
|
||||
msgstr "ポーション"
|
||||
|
||||
#: src/dusk/rpg/item/item.json
|
||||
msgid "item.potato.name"
|
||||
msgstr "ジャガイモ"
|
||||
|
||||
#: src/dusk/rpg/item/item.json
|
||||
msgid "item.apple.name"
|
||||
msgstr "リンゴ"
|
||||
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