Add the akgl_ui subsystem: clay-backed menus, HUDs and dialogs #3
@@ -14,7 +14,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
sudo apt-get update -y
|
sudo apt-get update -y
|
||||||
sudo apt-get install -y \
|
sudo apt-get install -y \
|
||||||
cmake doxygen gcc gcovr pkg-config \
|
cmake doxygen gcc gcovr graphviz pkg-config \
|
||||||
libasound2-dev libfreetype-dev libharfbuzz-dev \
|
libasound2-dev libfreetype-dev libharfbuzz-dev \
|
||||||
libpng-dev libtiff-dev libwebp-dev \
|
libpng-dev libtiff-dev libwebp-dev \
|
||||||
libudev-dev libx11-dev libxcursor-dev libxext-dev \
|
libudev-dev libx11-dev libxcursor-dev libxext-dev \
|
||||||
@@ -56,7 +56,7 @@ jobs:
|
|||||||
fail_on_failure: 'true'
|
fail_on_failure: 'true'
|
||||||
- name: Upload coverage reports
|
- name: Upload coverage reports
|
||||||
if: always()
|
if: always()
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: code-coverage
|
name: code-coverage
|
||||||
path: build/coverage/
|
path: build/coverage/
|
||||||
@@ -123,7 +123,7 @@ jobs:
|
|||||||
# the next baseline if somebody re-records PERFORMANCE.md.
|
# the next baseline if somebody re-records PERFORMANCE.md.
|
||||||
- name: Upload benchmark tables
|
- name: Upload benchmark tables
|
||||||
if: always()
|
if: always()
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: performance-baseline
|
name: performance-baseline
|
||||||
path: perf-baseline.txt
|
path: perf-baseline.txt
|
||||||
@@ -170,7 +170,7 @@ jobs:
|
|||||||
run: scripts/memcheck.sh
|
run: scripts/memcheck.sh
|
||||||
- name: Upload valgrind logs
|
- name: Upload valgrind logs
|
||||||
if: always()
|
if: always()
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: valgrind-logs
|
name: valgrind-logs
|
||||||
path: build/Testing/Temporary/MemoryChecker.*.log
|
path: build/Testing/Temporary/MemoryChecker.*.log
|
||||||
|
|||||||
16
.gitmodules
vendored
16
.gitmodules
vendored
@@ -1,18 +1,18 @@
|
|||||||
[submodule "deps/semver"]
|
[submodule "deps/semver"]
|
||||||
path = deps/semver
|
path = deps/semver
|
||||||
url = git@github.com:h2non/semver.c.git
|
url = https://github.com/h2non/semver.c.git
|
||||||
[submodule "deps/SDL"]
|
[submodule "deps/SDL"]
|
||||||
path = deps/SDL
|
path = deps/SDL
|
||||||
url = git@github.com:libsdl-org/SDL.git
|
url = https://github.com/libsdl-org/SDL.git
|
||||||
[submodule "deps/SDL_image"]
|
[submodule "deps/SDL_image"]
|
||||||
path = deps/SDL_image
|
path = deps/SDL_image
|
||||||
url = git@github.com:libsdl-org/SDL_image.git
|
url = https://github.com/libsdl-org/SDL_image.git
|
||||||
[submodule "deps/SDL_mixer"]
|
[submodule "deps/SDL_mixer"]
|
||||||
path = deps/SDL_mixer
|
path = deps/SDL_mixer
|
||||||
url = git@github.com:libsdl-org/SDL_mixer.git
|
url = https://github.com/libsdl-org/SDL_mixer.git
|
||||||
[submodule "deps/SDL_ttf"]
|
[submodule "deps/SDL_ttf"]
|
||||||
path = deps/SDL_ttf
|
path = deps/SDL_ttf
|
||||||
url = git@github.com:libsdl-org/SDL_ttf.git
|
url = https://github.com/libsdl-org/SDL_ttf.git
|
||||||
[submodule "deps/libsdlerror"]
|
[submodule "deps/libsdlerror"]
|
||||||
path = deps/libakerror
|
path = deps/libakerror
|
||||||
url = https://source.starfort.tech/andrew/libakerror.git
|
url = https://source.starfort.tech/andrew/libakerror.git
|
||||||
@@ -21,10 +21,14 @@
|
|||||||
url = https://source.starfort.tech/andrew/libakstdlib.git
|
url = https://source.starfort.tech/andrew/libakstdlib.git
|
||||||
[submodule "deps/jansson"]
|
[submodule "deps/jansson"]
|
||||||
path = deps/jansson
|
path = deps/jansson
|
||||||
url = git@github.com:akheron/jansson.git
|
url = https://github.com/akheron/jansson.git
|
||||||
[submodule "deps/libccd"]
|
[submodule "deps/libccd"]
|
||||||
path = deps/libccd
|
path = deps/libccd
|
||||||
url = https://github.com/danfis/libccd.git
|
url = https://github.com/danfis/libccd.git
|
||||||
[submodule "deps/tg"]
|
[submodule "deps/tg"]
|
||||||
path = deps/tg
|
path = deps/tg
|
||||||
url = https://github.com/tidwall/tg.git
|
url = https://github.com/tidwall/tg.git
|
||||||
|
[submodule "clay"]
|
||||||
|
path = deps/clay
|
||||||
|
url = https://github.com/nicbarker/clay.git
|
||||||
|
branch = v0.14
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.10)
|
|||||||
# The single source of truth for the library version. It drives the generated
|
# The single source of truth for the library version. It drives the generated
|
||||||
# include/akgl/version.h, the shared library's VERSION and SOVERSION, and the
|
# include/akgl/version.h, the shared library's VERSION and SOVERSION, and the
|
||||||
# Version field in akgl.pc. Bump it here and nowhere else.
|
# Version field in akgl.pc. Bump it here and nowhere else.
|
||||||
project(akgl VERSION 0.8.0 LANGUAGES C)
|
project(akgl VERSION 0.9.0 LANGUAGES C)
|
||||||
|
|
||||||
# Memory checking reuses the suites that already exist -- `ctest -T memcheck`
|
# Memory checking reuses the suites that already exist -- `ctest -T memcheck`
|
||||||
# runs every registered test under valgrind -- rather than adding programs of its
|
# runs every registered test under valgrind -- rather than adding programs of its
|
||||||
@@ -217,6 +217,7 @@ set(AKGL_PUBLIC_HEADERS
|
|||||||
text
|
text
|
||||||
tilemap
|
tilemap
|
||||||
types
|
types
|
||||||
|
ui
|
||||||
util
|
util
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -317,6 +318,8 @@ add_library(akgl SHARED
|
|||||||
src/sprite.c
|
src/sprite.c
|
||||||
src/staticstring.c
|
src/staticstring.c
|
||||||
src/tilemap.c
|
src/tilemap.c
|
||||||
|
src/ui.c
|
||||||
|
src/ui_clay.c
|
||||||
src/util.c
|
src/util.c
|
||||||
src/version.c
|
src/version.c
|
||||||
)
|
)
|
||||||
@@ -368,6 +371,26 @@ set_source_files_properties(${AKGL_CCD_SOURCES} PROPERTIES
|
|||||||
COMPILE_OPTIONS "-w;-fvisibility=hidden;-include;${CMAKE_CURRENT_SOURCE_DIR}/src/ccd_arena_shim.h"
|
COMPILE_OPTIONS "-w;-fvisibility=hidden;-include;${CMAKE_CURRENT_SOURCE_DIR}/src/ccd_arena_shim.h"
|
||||||
COMPILE_DEFINITIONS "CCD_STATIC_DEFINE")
|
COMPILE_DEFINITIONS "CCD_STATIC_DEFINE")
|
||||||
|
|
||||||
|
# clay supplies the UI layout engine. Its sources are listed rather than
|
||||||
|
# add_subdirectory()'d, on the semver/libccd precedent, because
|
||||||
|
# deps/clay/CMakeLists.txt is unusable as a subproject and none of it is ours
|
||||||
|
# to fix in a submodule:
|
||||||
|
#
|
||||||
|
# - it declares no library target at all -- clay is a single header, and the
|
||||||
|
# add_library(INTERFACE) lines at the bottom are commented out upstream.
|
||||||
|
# - option(CLAY_INCLUDE_ALL_EXAMPLES ... ON) add_subdirectory()s every
|
||||||
|
# example by default, dragging raylib, cairo and sokol into our configure.
|
||||||
|
# - cmake_minimum_required(VERSION 3.27) against this project's 3.10.
|
||||||
|
#
|
||||||
|
# src/ui_clay.c is the one translation unit that defines CLAY_IMPLEMENTATION;
|
||||||
|
# it is 99% vendored code, so it gets -w on the same terms as semver and
|
||||||
|
# libccd. Deliberately NOT -fvisibility=hidden, unlike libccd: the CLAY()
|
||||||
|
# macros in a consuming game expand to Clay__OpenElement() and friends, which
|
||||||
|
# must resolve against libakgl.so, so clay's symbols are part of the ABI on
|
||||||
|
# purpose. The matching obligation -- a consumer must not link a second clay --
|
||||||
|
# is documented in akgl/ui.h.
|
||||||
|
set_source_files_properties(src/ui_clay.c PROPERTIES COMPILE_OPTIONS "-w")
|
||||||
|
|
||||||
# PRIVATE, so that ccd/*.h never reaches a consumer's include path. The `headers`
|
# PRIVATE, so that ccd/*.h never reaches a consumer's include path. The `headers`
|
||||||
# suite compiles each public header as the first include of a translation unit
|
# suite compiles each public header as the first include of a translation unit
|
||||||
# with only include/ available, so a public header that pulled in <ccd/ccd.h>
|
# with only include/ available, so a public header that pulled in <ccd/ccd.h>
|
||||||
@@ -406,6 +429,7 @@ set(AKGL_TEST_SUITES
|
|||||||
staticstring
|
staticstring
|
||||||
text
|
text
|
||||||
tilemap
|
tilemap
|
||||||
|
ui
|
||||||
util
|
util
|
||||||
version
|
version
|
||||||
)
|
)
|
||||||
@@ -516,6 +540,11 @@ set_tests_properties(
|
|||||||
target_include_directories(akgl PUBLIC
|
target_include_directories(akgl PUBLIC
|
||||||
include/
|
include/
|
||||||
deps/semver/
|
deps/semver/
|
||||||
|
# PUBLIC, unlike libccd's PRIVATE include above, because akgl/ui.h includes
|
||||||
|
# <clay.h> in its public interface -- consumers write CLAY() blocks, so
|
||||||
|
# hiding the header would hide the point. clay.h is installed beside
|
||||||
|
# semver.h for the same reason.
|
||||||
|
deps/clay/
|
||||||
# akgl/version.h is generated by configure_file, so it lives in the build tree
|
# akgl/version.h is generated by configure_file, so it lives in the build tree
|
||||||
# rather than beside the headers it is included from.
|
# rather than beside the headers it is included from.
|
||||||
${CMAKE_CURRENT_BINARY_DIR}/include/
|
${CMAKE_CURRENT_BINARY_DIR}/include/
|
||||||
@@ -633,20 +662,26 @@ if(AKGL_VENDORED_DEPENDENCIES)
|
|||||||
"${CMAKE_CURRENT_BINARY_DIR}/deps/libakerror"
|
"${CMAKE_CURRENT_BINARY_DIR}/deps/libakerror"
|
||||||
"${CMAKE_CURRENT_BINARY_DIR}/deps/libakstdlib"
|
"${CMAKE_CURRENT_BINARY_DIR}/deps/libakstdlib"
|
||||||
)
|
)
|
||||||
|
# set_property rather than set_tests_properties for the list-valued
|
||||||
|
# property: set_tests_properties parses its PROPERTIES arguments as
|
||||||
|
# name/value pairs, so a semicolon-separated value is split and every
|
||||||
|
# element after the first is consumed as a bogus property name -- which
|
||||||
|
# silently reduced this prepend list to its first directory for as long as
|
||||||
|
# it existed. RPATH covered for it locally; a runner found it.
|
||||||
if(CMAKE_VERSION VERSION_GREATER_EQUAL "3.22")
|
if(CMAKE_VERSION VERSION_GREATER_EQUAL "3.22")
|
||||||
set(AKGL_TEST_ENV_MOD "")
|
set(AKGL_TEST_ENV_MOD "")
|
||||||
foreach(dir IN LISTS AKGL_TEST_LIBPATH)
|
foreach(dir IN LISTS AKGL_TEST_LIBPATH)
|
||||||
list(APPEND AKGL_TEST_ENV_MOD "LD_LIBRARY_PATH=path_list_prepend:${dir}")
|
list(APPEND AKGL_TEST_ENV_MOD "LD_LIBRARY_PATH=path_list_prepend:${dir}")
|
||||||
endforeach()
|
endforeach()
|
||||||
set_tests_properties(
|
set_property(
|
||||||
${AKGL_TEST_SUITES} ${AKGL_PERF_SUITES}
|
TEST ${AKGL_TEST_SUITES} ${AKGL_PERF_SUITES}
|
||||||
PROPERTIES ENVIRONMENT_MODIFICATION "${AKGL_TEST_ENV_MOD}"
|
PROPERTY ENVIRONMENT_MODIFICATION ${AKGL_TEST_ENV_MOD}
|
||||||
)
|
)
|
||||||
else()
|
else()
|
||||||
string(REPLACE ";" ":" AKGL_TEST_LIBPATH_JOINED "${AKGL_TEST_LIBPATH}")
|
string(REPLACE ";" ":" AKGL_TEST_LIBPATH_JOINED "${AKGL_TEST_LIBPATH}")
|
||||||
set_tests_properties(
|
set_property(
|
||||||
${AKGL_TEST_SUITES} ${AKGL_PERF_SUITES}
|
TEST ${AKGL_TEST_SUITES} ${AKGL_PERF_SUITES}
|
||||||
PROPERTIES ENVIRONMENT "LD_LIBRARY_PATH=${AKGL_TEST_LIBPATH_JOINED}:$ENV{LD_LIBRARY_PATH}"
|
PROPERTY ENVIRONMENT "LD_LIBRARY_PATH=${AKGL_TEST_LIBPATH_JOINED}:$ENV{LD_LIBRARY_PATH}"
|
||||||
)
|
)
|
||||||
endif()
|
endif()
|
||||||
endif()
|
endif()
|
||||||
@@ -754,7 +789,12 @@ add_test(
|
|||||||
set_tests_properties(docs_examples PROPERTIES
|
set_tests_properties(docs_examples PROPERTIES
|
||||||
WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
|
WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
|
||||||
TIMEOUT 900
|
TIMEOUT 900
|
||||||
ENVIRONMENT "SDL_VIDEODRIVER=dummy;SDL_AUDIODRIVER=dummy;SDL_RENDER_DRIVER=software"
|
)
|
||||||
|
# set_property for the same pair-splitting reason as the suites above.
|
||||||
|
set_property(TEST docs_examples PROPERTY ENVIRONMENT
|
||||||
|
"SDL_VIDEODRIVER=dummy"
|
||||||
|
"SDL_AUDIODRIVER=dummy"
|
||||||
|
"SDL_RENDER_DRIVER=software"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Every figure in docs/ re-rendered and byte-compared against the tracked copy.
|
# Every figure in docs/ re-rendered and byte-compared against the tracked copy.
|
||||||
@@ -779,7 +819,11 @@ add_test(
|
|||||||
set_tests_properties(docs_screenshots PROPERTIES
|
set_tests_properties(docs_screenshots PROPERTIES
|
||||||
WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
|
WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
|
||||||
TIMEOUT 900
|
TIMEOUT 900
|
||||||
ENVIRONMENT "SDL_VIDEODRIVER=dummy;SDL_AUDIODRIVER=dummy;SDL_RENDER_DRIVER=software"
|
)
|
||||||
|
set_property(TEST docs_screenshots PROPERTY ENVIRONMENT
|
||||||
|
"SDL_VIDEODRIVER=dummy"
|
||||||
|
"SDL_AUDIODRIVER=dummy"
|
||||||
|
"SDL_RENDER_DRIVER=software"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Regenerating the figures is a deliberate act, never part of a build: the PNGs
|
# Regenerating the figures is a deliberate act, never part of a build: the PNGs
|
||||||
@@ -832,7 +876,15 @@ if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/examples/CMakeLists.txt")
|
|||||||
--demo --frames 240
|
--demo --frames 240
|
||||||
--screenshot "${CMAKE_CURRENT_SOURCE_DIR}/docs/images/jrpg.png"
|
--screenshot "${CMAKE_CURRENT_SOURCE_DIR}/docs/images/jrpg.png"
|
||||||
--screenshot-frame 230
|
--screenshot-frame 230
|
||||||
DEPENDS sidescroller jrpg
|
# Frame 100 of the UI demo's scripted tour is the play screen with the
|
||||||
|
# dialog up and the score mid-count -- all three widgets in one frame.
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E env
|
||||||
|
SDL_VIDEODRIVER=dummy SDL_RENDER_DRIVER=software SDL_AUDIODRIVER=dummy
|
||||||
|
$<TARGET_FILE:uidemo>
|
||||||
|
--demo --frames 240
|
||||||
|
--screenshot "${CMAKE_CURRENT_SOURCE_DIR}/docs/images/uidemo.png"
|
||||||
|
--screenshot-frame 100
|
||||||
|
DEPENDS sidescroller jrpg uidemo
|
||||||
COMMENT "Regenerating the tutorial figures in docs/images"
|
COMMENT "Regenerating the tutorial figures in docs/images"
|
||||||
VERBATIM
|
VERBATIM
|
||||||
)
|
)
|
||||||
@@ -882,11 +934,14 @@ install(FILES ${CMAKE_CURRENT_BINARY_DIR}/akgl.pc DESTINATION "lib/pkgconfig/")
|
|||||||
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/include/akgl/version.h DESTINATION "include/akgl/")
|
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/include/akgl/version.h DESTINATION "include/akgl/")
|
||||||
install(TARGETS akgl DESTINATION "lib/")
|
install(TARGETS akgl DESTINATION "lib/")
|
||||||
install(FILES "deps/semver/semver.h" DESTINATION "include/")
|
install(FILES "deps/semver/semver.h" DESTINATION "include/")
|
||||||
# libccd is compiled into libakgl.so, and its BSD-3 licence requires the notice
|
install(FILES "deps/clay/clay.h" DESTINATION "include/")
|
||||||
# to travel with the binary form. Nothing else in deps/ is linked in statically
|
# libccd and clay are compiled into libakgl.so, and their licences (BSD-3 and
|
||||||
# -- SDL, jansson, libakerror and libakstdlib are all separate shared objects
|
# zlib respectively) ask that the notice travel with the binary form. Nothing
|
||||||
# that ship their own -- so this is the only third-party notice we owe.
|
# else in deps/ is linked in statically -- SDL, jansson, libakerror and
|
||||||
|
# libakstdlib are all separate shared objects that ship their own -- so these
|
||||||
|
# are the only third-party notices we owe.
|
||||||
install(FILES "deps/libccd/BSD-LICENSE" DESTINATION "share/doc/akgl/" RENAME "BSD-LICENSE.libccd")
|
install(FILES "deps/libccd/BSD-LICENSE" DESTINATION "share/doc/akgl/" RENAME "BSD-LICENSE.libccd")
|
||||||
|
install(FILES "deps/clay/LICENSE.md" DESTINATION "share/doc/akgl/" RENAME "LICENSE.clay")
|
||||||
foreach(header IN LISTS AKGL_PUBLIC_HEADERS)
|
foreach(header IN LISTS AKGL_PUBLIC_HEADERS)
|
||||||
install(FILES "include/akgl/${header}.h" DESTINATION "include/akgl/")
|
install(FILES "include/akgl/${header}.h" DESTINATION "include/akgl/")
|
||||||
endforeach()
|
endforeach()
|
||||||
|
|||||||
21
TODO.md
21
TODO.md
@@ -1273,6 +1273,15 @@ engines spend the same frame**.
|
|||||||
cached") with the raw-SDL control row the perf rules require. Budget to
|
cached") with the raw-SDL control row the perf rules require. Budget to
|
||||||
move: `tests/perf_render.c:391`.
|
move: `tests/perf_render.c:391`.
|
||||||
|
|
||||||
|
**The UI subsystem is this cache's second consumer, and the bigger one.**
|
||||||
|
`akgl_ui_execute_commands` (`src/ui.c`) draws every TEXT render command
|
||||||
|
through `akgl_text_rendertextat`, one wrapped line per command per frame —
|
||||||
|
a menu of five rows re-rasterizes five lines at 60 Hz whether or not any
|
||||||
|
of them changed, where the original HUD case was one line changing once a
|
||||||
|
second. Abstract-on-the-second-consumer says this item's time has come;
|
||||||
|
the key already fits, since clay hands the executor stable text bytes
|
||||||
|
between frames.
|
||||||
|
|
||||||
3. **Non-raising sprite lookup — target 10, a design change.** Give
|
3. **Non-raising sprite lookup — target 10, a design change.** Give
|
||||||
`akgl_character_sprite_get` (`src/character.c:84-94`) the companion that
|
`akgl_character_sprite_get` (`src/character.c:84-94`) the companion that
|
||||||
returns `NULL` without raising, then convert the three sites that raise
|
returns `NULL` without raising, then convert the three sites that raise
|
||||||
@@ -2196,6 +2205,18 @@ a live defect -- but nothing rejects it, and `max_timestep` is caller-settable.
|
|||||||
`renderfunc`. This item is the half that remains: one uniform `scale`, with no way
|
`renderfunc`. This item is the half that remains: one uniform `scale`, with no way
|
||||||
to expand a single axis.
|
to expand a single axis.
|
||||||
|
|
||||||
|
3. **`akgl_ControlMap.mouseid` and `.penid` are still dead fields, deliberately.**
|
||||||
|
The UI subsystem (0.9.0) brought the mouse into the library, and it would have
|
||||||
|
been natural to wire these up on the way -- it did not, on purpose. Control maps
|
||||||
|
translate device events into *actor handler calls*, and the UI wants absolute
|
||||||
|
pointer state per event; those are different consumers, and building
|
||||||
|
gameplay-mouse machinery into `src/controller.c` with no game asking for it is
|
||||||
|
the abstract-before-the-second-consumer mistake. The mouse path lives in
|
||||||
|
`src/ui.c` (`akgl_ui_handle_event`); when a game wants mouse-driven actors,
|
||||||
|
*that* is the consumer these fields were declared for, and the work lands in
|
||||||
|
`controller.c` then. Same for `.axis`, `.axis_range_min` and `.axis_range_max`,
|
||||||
|
which `akgl_controller_handle_event` has never consulted.
|
||||||
|
|
||||||
## Found while writing the manual
|
## Found while writing the manual
|
||||||
|
|
||||||
Twenty-one chapters and two tutorial games were written against `src/` rather than
|
Twenty-one chapters and two tutorial games were written against `src/` rather than
|
||||||
|
|||||||
1
deps/clay
vendored
Submodule
1
deps/clay
vendored
Submodule
Submodule deps/clay added at b25a31c1a1
@@ -25,7 +25,7 @@ The house rules for *writing* code against the protocol — never a `*_RETURN` i
|
|||||||
|
|
||||||
libakerror reserves statuses 0–255 for the host's `errno` values and its own `AKERR_*`
|
libakerror reserves statuses 0–255 for the host's `errno` values and its own `AKERR_*`
|
||||||
codes; consumers allocate upward from `AKERR_FIRST_CONSUMER_STATUS`, which is **256**.
|
codes; consumers allocate upward from `AKERR_FIRST_CONSUMER_STATUS`, which is **256**.
|
||||||
libakgl claims a band of six starting there, under the owner string `"libakgl"`:
|
libakgl claims a band of seven starting there, under the owner string `"libakgl"`:
|
||||||
|
|
||||||
```c excerpt=include/akgl/error.h
|
```c excerpt=include/akgl/error.h
|
||||||
#define AKGL_ERR_OWNER "libakgl"
|
#define AKGL_ERR_OWNER "libakgl"
|
||||||
@@ -37,12 +37,13 @@ libakgl claims a band of six starting there, under the owner string `"libakgl"`:
|
|||||||
#define AKGL_ERR_BEHAVIOR (AKGL_ERR_BASE + 3) /**< A component did not behave the way its contract requires */
|
#define AKGL_ERR_BEHAVIOR (AKGL_ERR_BASE + 3) /**< A component did not behave the way its contract requires */
|
||||||
#define AKGL_ERR_LOGICINTERRUPT (AKGL_ERR_BASE + 4) /**< Actor logic is telling the physics simulator to skip it */
|
#define AKGL_ERR_LOGICINTERRUPT (AKGL_ERR_BASE + 4) /**< Actor logic is telling the physics simulator to skip it */
|
||||||
#define AKGL_ERR_COLLISION (AKGL_ERR_BASE + 5) /**< A collision query could not be answered; the message says why */
|
#define AKGL_ERR_COLLISION (AKGL_ERR_BASE + 5) /**< A collision query could not be answered; the message says why */
|
||||||
|
#define AKGL_ERR_UI (AKGL_ERR_BASE + 6) /**< The UI subsystem refused, or clay reported a layout error; the message says which */
|
||||||
```
|
```
|
||||||
|
|
||||||
**`AKGL_ERR_LIMIT` is not a status code.** It is `AKGL_ERR_BASE + 6`, one past the last
|
**`AKGL_ERR_LIMIT` is not a status code.** It is `AKGL_ERR_BASE + 7`, one past the last
|
||||||
one, and exists only so `AKGL_ERR_COUNT` can be computed from it. Nothing in `src/` raises
|
one, and exists only so `AKGL_ERR_COUNT` can be computed from it. Nothing in `src/` raises
|
||||||
it and `akgl_error_init` does not name it. Do not write a `HANDLE(e, AKGL_ERR_LIMIT)`
|
it and `akgl_error_init` does not name it. Do not write a `HANDLE(e, AKGL_ERR_LIMIT)`
|
||||||
arm — it would catch nothing, and if a seventh real code is ever added it would silently
|
arm — it would catch nothing, and if an eighth real code is ever added it would silently
|
||||||
start catching that instead.
|
start catching that instead.
|
||||||
|
|
||||||
| Code | Value | Means | Raised by | What the caller does |
|
| Code | Value | Means | Raised by | What the caller does |
|
||||||
@@ -53,10 +54,12 @@ start catching that instead.
|
|||||||
| `AKGL_ERR_BEHAVIOR` | 259 | A component did not behave the way its contract requires | **Nothing in the library.** Only `tests/` raises it, through `testutil.h` | Available to you for the same purpose: asserting a contract in your own code |
|
| `AKGL_ERR_BEHAVIOR` | 259 | A component did not behave the way its contract requires | **Nothing in the library.** Only `tests/` raises it, through `testutil.h` | Available to you for the same purpose: asserting a contract in your own code |
|
||||||
| `AKGL_ERR_LOGICINTERRUPT` | 260 | **Not a failure — a control signal.** "Skip the rest of this step for this actor" | your own `movementlogicfunc` | Raise it deliberately. See the note below |
|
| `AKGL_ERR_LOGICINTERRUPT` | 260 | **Not a failure — a control signal.** "Skip the rest of this step for this actor" | your own `movementlogicfunc` | Raise it deliberately. See the note below |
|
||||||
| `AKGL_ERR_COLLISION` | 261 | A collision query could not be answered | **One cause only**: `akgl_collision_test`, when the ccd arena is exhausted. The message carries the high-water mark | Raise `AKGL_CCD_ARENA_BYTES` to the reported figure. See [Chapter 15](15-collision.md) |
|
| `AKGL_ERR_COLLISION` | 261 | A collision query could not be answered | **One cause only**: `akgl_collision_test`, when the ccd arena is exhausted. The message carries the high-water mark | Raise `AKGL_CCD_ARENA_BYTES` to the reported figure. See [Chapter 15](15-collision.md) |
|
||||||
|
| `AKGL_ERR_UI` | 262 | The UI subsystem refused, or clay reported a layout error | `akgl_ui_*` — lifecycle misuse (init twice, a widget outside the frame bracket), an arena or table at its ceiling, and layout errors surfacing from `akgl_ui_frame_end` | The message says which; the ceilings are in [Chapter 23](23-appendix-limits.md). See [Chapter 22](22-ui.md) |
|
||||||
|
|
||||||
`akgl_error_init` reserves the band all-or-nothing and registers a name for each of the
|
`akgl_error_init` reserves the band all-or-nothing and registers a name for each of the
|
||||||
six: `"SDL Error"`, `"Registry Error"`, `"Heap Error"`, `"Behavior Error"`,
|
seven: `"SDL Error"`, `"Registry Error"`, `"Heap Error"`, `"Behavior Error"`,
|
||||||
`"Logic Interrupt"`, `"Collision Error"`. Those names are what a stack trace prints.
|
`"Logic Interrupt"`, `"Collision Error"`, `"UI Error"`. Those names are what a stack trace
|
||||||
|
prints.
|
||||||
|
|
||||||
Two rows deserve more than a cell.
|
Two rows deserve more than a cell.
|
||||||
|
|
||||||
@@ -99,7 +102,7 @@ Statuses raised by the libraries underneath also reach you unchanged. `aksl_fope
|
|||||||
`akgl_error_init` can raise libakerror's `AKERR_STATUS_RANGE_OVERLAP`,
|
`akgl_error_init` can raise libakerror's `AKERR_STATUS_RANGE_OVERLAP`,
|
||||||
`AKERR_STATUS_RANGE_FULL` or `AKERR_STATUS_NAME_FULL`.
|
`AKERR_STATUS_RANGE_FULL` or `AKERR_STATUS_NAME_FULL`.
|
||||||
|
|
||||||
[Chapter 22](22-appendix-limits.md) has the per-function cross-reference.
|
[Chapter 23](23-appendix-limits.md) has the per-function cross-reference.
|
||||||
|
|
||||||
## Table 3 — the exit status trap
|
## Table 3 — the exit status trap
|
||||||
|
|
||||||
@@ -266,6 +269,6 @@ Four things in there are libakgl-specific and worth naming:
|
|||||||
release.
|
release.
|
||||||
- [Chapter 6](06-the-registry.md) — the name lookups behind `AKERR_KEY` and
|
- [Chapter 6](06-the-registry.md) — the name lookups behind `AKERR_KEY` and
|
||||||
`AKERR_NULLPOINTER`.
|
`AKERR_NULLPOINTER`.
|
||||||
- [Chapter 22](22-appendix-limits.md) — which function raises what.
|
- [Chapter 23](23-appendix-limits.md) — which function raises what.
|
||||||
- `deps/libakerror/README.md` — the protocol itself, and the exit-status discussion.
|
- `deps/libakerror/README.md` — the protocol itself, and the exit-status discussion.
|
||||||
- The generated Doxygen reference — every function's own `@throws` list.
|
- The generated Doxygen reference — every function's own `@throws` list.
|
||||||
|
|||||||
@@ -344,4 +344,4 @@ all visible in `src/heap.c`.
|
|||||||
|
|
||||||
- [Chapter 4](04-errors.md) — `AKGL_ERR_HEAP` in the status tables.
|
- [Chapter 4](04-errors.md) — `AKGL_ERR_HEAP` in the status tables.
|
||||||
- [Chapter 6](06-the-registry.md) — the registries the releases clear entries from.
|
- [Chapter 6](06-the-registry.md) — the registries the releases clear entries from.
|
||||||
- [Chapter 22](22-appendix-limits.md) — every `AKGL_MAX_*` in one place.
|
- [Chapter 23](23-appendix-limits.md) — every `AKGL_MAX_*` in one place.
|
||||||
|
|||||||
@@ -224,5 +224,5 @@ registry's.
|
|||||||
- [Chapter 7](07-the-game-and-the-frame.md) — where in startup the registries are created,
|
- [Chapter 7](07-the-game-and-the-frame.md) — where in startup the registries are created,
|
||||||
and where configuration has to be in place by.
|
and where configuration has to be in place by.
|
||||||
- [Chapter 14](14-physics.md) — choosing a backend, and what the `physics.*` properties do.
|
- [Chapter 14](14-physics.md) — choosing a backend, and what the `physics.*` properties do.
|
||||||
- [Chapter 22](22-appendix-limits.md) — the configuration table again, alongside every
|
- [Chapter 23](23-appendix-limits.md) — the configuration table again, alongside every
|
||||||
`AKGL_MAX_*`.
|
`AKGL_MAX_*`.
|
||||||
|
|||||||
@@ -367,4 +367,4 @@ error-reporting path. See [Chapter 4](04-errors.md).
|
|||||||
what `draw_world` does with the layers.
|
what `draw_world` does with the layers.
|
||||||
- [Chapter 14](14-physics.md) — what `simulate` does with the `dt` this chapter does not
|
- [Chapter 14](14-physics.md) — what `simulate` does with the `dt` this chapter does not
|
||||||
pace.
|
pace.
|
||||||
- [Chapter 22](22-appendix-limits.md) — `AKGL_GAME_*` and the rest of the constants.
|
- [Chapter 23](23-appendix-limits.md) — `AKGL_GAME_*` and the rest of the constants.
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ why the library's own map, `akgl_default_gamemap`, is a file-scope object in
|
|||||||
The bounds are fixed at compile time, so the size is fixed too. Every field is present
|
The bounds are fixed at compile time, so the size is fixed too. Every field is present
|
||||||
whether the map uses it or not: a 20x15 map with one tileset and two layers costs the same
|
whether the map uses it or not: a 20x15 map with one tileset and two layers costs the same
|
||||||
25.2 MiB as a 511x511 one. Raising any of the constants below raises this number and
|
25.2 MiB as a 511x511 one. Raising any of the constants below raises this number and
|
||||||
changes the ABI — see [Chapter 22](22-appendix-limits.md).
|
changes the ABI — see [Chapter 23](23-appendix-limits.md).
|
||||||
|
|
||||||
## Limits
|
## Limits
|
||||||
|
|
||||||
|
|||||||
396
docs/22-ui.md
Normal file
396
docs/22-ui.md
Normal file
@@ -0,0 +1,396 @@
|
|||||||
|
# 22. User interfaces
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Every game eventually wants a dialog box, a score counter, or a menu — and hand-rolling
|
||||||
|
them out of rectangles and `akgl_text_rendertextat` is tolerable exactly once.
|
||||||
|
[Chapter 21](21-tutorial-jrpg.md) does it once, on purpose, and its 125-line
|
||||||
|
`textbox.c` is still the right call for one panel with no ambitions. This chapter is for
|
||||||
|
everything past that point: HUDs, menus, options screens, and the layout arithmetic that
|
||||||
|
makes them miserable to maintain by hand.
|
||||||
|
|
||||||
|
The layout engine is [clay](https://github.com/nicbarker/clay), vendored under
|
||||||
|
`deps/clay` and compiled into `libakgl.so`. Per this manual's rule, clay's own API is
|
||||||
|
documented by clay — its README is thorough — and this chapter covers what libakgl adds
|
||||||
|
or constrains. The split:
|
||||||
|
|
||||||
|
| clay owns | libakgl owns |
|
||||||
|
|---|---|
|
||||||
|
| The layout algorithm and the `CLAY()` declaration DSL | The arena clay allocates from — static storage, no `malloc`, sized by `AKGL_UI_ARENA_BYTES` |
|
||||||
|
| Sizing, padding, floating elements, scroll containers | Text measurement, through SDL_ttf and the font registry |
|
||||||
|
| The render command list each frame produces | Drawing those commands, through the render backend |
|
||||||
|
| Hover and pointer-over queries | Feeding it the mouse, and telling *you* which events the UI consumed |
|
||||||
|
|
||||||
|
**There are two ways in, and they compose.** The widget helpers — `akgl_ui_dialog`,
|
||||||
|
`akgl_ui_label`, `akgl_ui_menu` — cover the common cases in one call each, and a simple
|
||||||
|
game never touches a `CLAY()` macro. When a screen outgrows them, you write clay's
|
||||||
|
declarative blocks yourself between the same two frame calls, with the whole DSL
|
||||||
|
available. The demo this chapter quotes, [`examples/uidemo`](../examples/uidemo), does
|
||||||
|
both: its title menu and HUD are widgets, its options screen is raw `CLAY()`.
|
||||||
|
|
||||||
|
**What this chapter assumes.** A program that already brings libakgl up — the
|
||||||
|
`akgl_game_init` startup order from [Chapter 7](07-the-game-and-the-frame.md) (or
|
||||||
|
[Chapter 3](03-getting-started.md)'s smallest window), plus one loaded font from
|
||||||
|
[Chapter 17](17-text-and-fonts.md). None of that is restated here, and the UI adds only
|
||||||
|
three calls to it, shown below. When something in *your* bring-up fights you, the demo's
|
||||||
|
`startup()` in [`examples/uidemo/uidemo.c`](../examples/uidemo/uidemo.c) is the complete
|
||||||
|
working sequence to diff against — including the `main()` shape, which the runnable
|
||||||
|
snippets in this manual deliberately hide.
|
||||||
|
|
||||||
|
Like collision, the subsystem is optional at runtime rather than at build time: it is
|
||||||
|
always compiled in, costs static storage until `akgl_ui_init` runs, and a game that
|
||||||
|
never calls that pays nothing else.
|
||||||
|
|
||||||
|
**One warning before any code.** libakgl ships clay inside `libakgl.so` and exports its
|
||||||
|
symbols, because the `CLAY()` macros in *your* translation units expand to calls into
|
||||||
|
them. Do not define `CLAY_IMPLEMENTATION` anywhere and do not link a second copy of clay
|
||||||
|
— two definitions of the same symbols, and the loader picks one silently.
|
||||||
|
|
||||||
|
## Bring it up, lay something out
|
||||||
|
|
||||||
|
`akgl_ui_init(width, height)` takes the layout size as parameters — deliberately not
|
||||||
|
read from the camera or the window, so a headless program can bring the UI up with no
|
||||||
|
renderer at all. It bounds clay to the `AKGL_UI_*` ceilings, checks the arena fits them,
|
||||||
|
and refuses **with both byte counts in the message** when it does not: a raised ceiling
|
||||||
|
without a raised arena is a loud startup failure, never a corruption at frame forty
|
||||||
|
thousand.
|
||||||
|
|
||||||
|
After that, a frame of UI is a bracket with declarations inside:
|
||||||
|
|
||||||
|
```c run=akglapp
|
||||||
|
PASS(errctx, akgl_ui_init(320, 240));
|
||||||
|
|
||||||
|
PASS(errctx, akgl_ui_frame_begin());
|
||||||
|
CLAY({
|
||||||
|
.id = CLAY_ID("panel"),
|
||||||
|
.layout = {
|
||||||
|
.sizing = {
|
||||||
|
.width = CLAY_SIZING_FIXED(120),
|
||||||
|
.height = CLAY_SIZING_FIXED(40)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.backgroundColor = { 24, 20, 37, 255 }
|
||||||
|
}) {}
|
||||||
|
PASS(errctx, akgl_ui_frame_end(akgl_renderer));
|
||||||
|
|
||||||
|
PASS(errctx, akgl_ui_shutdown());
|
||||||
|
printf("one panel, laid out and drawn\n");
|
||||||
|
```
|
||||||
|
|
||||||
|
```output
|
||||||
|
one panel, laid out and drawn
|
||||||
|
```
|
||||||
|
|
||||||
|
`frame_begin` starts the clay layout and feeds it the pointer state the event handler
|
||||||
|
has been accumulating; `frame_end` computes the layout and draws every render command it
|
||||||
|
produces through the backend — fills and rounded fills, borders, clip rectangles, text,
|
||||||
|
and sprites. Everything lands in screen coordinates on top of whatever is already on the
|
||||||
|
target.
|
||||||
|
|
||||||
|
Text needs one more step at startup: fonts. clay names a font by a `uint16_t` fontId;
|
||||||
|
libakgl names one by a registry key ([Chapter 17](17-text-and-fonts.md)). The bridge is
|
||||||
|
`akgl_ui_font_register`, and the demo's whole font story is three lines:
|
||||||
|
|
||||||
|
```c excerpt=examples/uidemo/uidemo.c
|
||||||
|
PASS(errctx, akgl_text_loadfont(UIDEMO_FONT_NAME, UIDEMO_FONT_FILE, UIDEMO_FONT_SIZE));
|
||||||
|
PASS(errctx, akgl_ui_init(UIDEMO_WIDTH, UIDEMO_HEIGHT));
|
||||||
|
PASS(errctx, akgl_ui_font_register(UIDEMO_FONT_NAME, &fontid));
|
||||||
|
```
|
||||||
|
|
||||||
|
The first font registered gets id 0, which is what the widgets' default style uses — so
|
||||||
|
a one-font game never mentions a fontId again. Two things worth knowing before they
|
||||||
|
surprise you:
|
||||||
|
|
||||||
|
- **`Clay_TextElementConfig.fontSize` is ignored.** A libakgl font bakes its size in at
|
||||||
|
load ([Chapter 17](17-text-and-fonts.md) explains why); the size text renders at is
|
||||||
|
the size the font behind its id was loaded at. One face at two sizes is two loads, two
|
||||||
|
registrations, two ids.
|
||||||
|
- The table stores the *name* and resolves it per use, so `akgl_text_unloadfont` on a
|
||||||
|
registered font makes the next frame fail loudly with the name in the message — not
|
||||||
|
dangle.
|
||||||
|
|
||||||
|
## The frame contract
|
||||||
|
|
||||||
|
Where the bracket goes in a real frame, from the demo — compare the JRPG's `frame()`,
|
||||||
|
which calls `akgl_game_update` where this program paints a checkerboard:
|
||||||
|
|
||||||
|
```c excerpt=examples/uidemo/uidemo.c
|
||||||
|
PASS(errctx, akgl_renderer->frame_start(akgl_renderer));
|
||||||
|
if ( state == UIDEMO_STATE_PLAY ) {
|
||||||
|
PASS(errctx, akgl_draw_background(akgl_renderer, UIDEMO_WIDTH, UIDEMO_HEIGHT));
|
||||||
|
score += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
PASS(errctx, akgl_ui_frame_begin());
|
||||||
|
switch ( state ) {
|
||||||
|
case UIDEMO_STATE_TITLE:
|
||||||
|
PASS(errctx, declare_title());
|
||||||
|
break;
|
||||||
|
case UIDEMO_STATE_OPTIONS:
|
||||||
|
PASS(errctx, declare_options());
|
||||||
|
break;
|
||||||
|
case UIDEMO_STATE_PLAY:
|
||||||
|
PASS(errctx, declare_play());
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
PASS(errctx, akgl_ui_frame_end(akgl_renderer));
|
||||||
|
```
|
||||||
|
|
||||||
|
The UI draws after the world because it is declared after the world — the overlay slot
|
||||||
|
between `akgl_game_update` and the backend's `frame_end` is exactly where the JRPG drew
|
||||||
|
its hand-rolled text box, and nothing about that slot changed.
|
||||||
|
|
||||||
|
Events go through the UI *first*. `akgl_ui_handle_event` takes every event
|
||||||
|
unconditionally — the same pass-everything contract as `akgl_controller_handle_event` —
|
||||||
|
and reports back whether the UI claimed it, so a click on a menu never leaks through and
|
||||||
|
also fires a game control:
|
||||||
|
|
||||||
|
```c excerpt=examples/uidemo/uidemo.c
|
||||||
|
PASS(errctx, akgl_ui_handle_event((void *)&akgl_game.state, event, &consumed));
|
||||||
|
if ( consumed ) {
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ( state ) {
|
||||||
|
case UIDEMO_STATE_TITLE:
|
||||||
|
PASS(errctx, akgl_ui_menu_handle_event(&title_menu, event, &consumed));
|
||||||
|
break;
|
||||||
|
```
|
||||||
|
|
||||||
|
Three rules govern what gets consumed, and each is a decision worth stating:
|
||||||
|
|
||||||
|
- **Presses, releases and the wheel are consumed when the pointer is over any UI
|
||||||
|
element.** Motion and resizes never are — the game may care where the mouse is, and
|
||||||
|
certainly cares about its window.
|
||||||
|
- **The hit test runs against the layout the previous frame declared**, because this
|
||||||
|
frame's does not exist while events are being polled. clay retains the last tree for
|
||||||
|
exactly this purpose; one frame of staleness is the standard model's accepted cost,
|
||||||
|
and before any frame has been laid out, nothing is over anything.
|
||||||
|
- **Keyboard events are never consumed by the UI itself.** Which menu hears the arrow
|
||||||
|
keys is something the application declares — the `switch` above *is* the focus model —
|
||||||
|
not something a pointer position implies.
|
||||||
|
|
||||||
|
## A dialog in one call — and what it replaces
|
||||||
|
|
||||||
|
The play screen's declarations, whole:
|
||||||
|
|
||||||
|
```c excerpt=examples/uidemo/uidemo.c
|
||||||
|
PASS(errctx, aksl_snprintf(&count, scoretext, sizeof(scoretext), "SCORE %05d", score));
|
||||||
|
PASS(errctx, aksl_snprintf(&count, livestext, sizeof(livestext), "LIVES %d", lives));
|
||||||
|
PASS(errctx, akgl_ui_label("score", scoretext, AKGL_UI_ANCHOR_TOP_RIGHT, NULL));
|
||||||
|
PASS(errctx, akgl_ui_label("lives", livestext, AKGL_UI_ANCHOR_TOP_LEFT, NULL));
|
||||||
|
if ( dialog_open ) {
|
||||||
|
PASS(errctx, akgl_ui_dialog("dialog",
|
||||||
|
"This panel is one call. Space dismisses it; "
|
||||||
|
"compare examples/jrpg/textbox.c.",
|
||||||
|
NULL));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Note what is absent: no `visible` flag, no draw call, no geometry. The dialog is open
|
||||||
|
because this frame declares it — a declarative frame *is* the flag. The text buffers are
|
||||||
|
`static` because clay borrows the pointer until `frame_end` rather than copying; format
|
||||||
|
your score into storage that outlives the bracket.
|
||||||
|
|
||||||
|
Now the same panel the way [Chapter 21](21-tutorial-jrpg.md) builds it, which is the
|
||||||
|
comparison this chapter owes you. The hand-rolled version keeps a flag and a copy of the
|
||||||
|
string, and its draw function does the geometry itself:
|
||||||
|
|
||||||
|
```c excerpt=examples/jrpg/textbox.c
|
||||||
|
panel.x = TEXTBOX_MARGIN;
|
||||||
|
panel.w = akgl_camera->w - (2.0f * TEXTBOX_MARGIN);
|
||||||
|
panel.h = TEXTBOX_HEIGHT;
|
||||||
|
panel.y = akgl_camera->h - TEXTBOX_MARGIN - panel.h;
|
||||||
|
|
||||||
|
PASS(errctx, akgl_draw_filled_rect(akgl_renderer, &panel, TEXTBOX_FILL));
|
||||||
|
PASS(errctx, akgl_draw_rect(akgl_renderer, &panel, TEXTBOX_EDGE));
|
||||||
|
PASS(errctx,
|
||||||
|
akgl_text_rendertextat(
|
||||||
|
font,
|
||||||
|
textbox_text,
|
||||||
|
TEXTBOX_INK,
|
||||||
|
(int)(panel.w - (2.0f * TEXTBOX_PADDING)),
|
||||||
|
(int)(panel.x + TEXTBOX_PADDING),
|
||||||
|
(int)(panel.y + TEXTBOX_PADDING)
|
||||||
|
));
|
||||||
|
```
|
||||||
|
|
||||||
|
The widget's default style is **deliberately that panel's palette** — near-black fill,
|
||||||
|
parchment edge and ink, 8 pixels of padding — so the two produce the same picture, and
|
||||||
|
the trade is visible with nothing hidden in a theme:
|
||||||
|
|
||||||
|
- `textbox.c` is 125 lines you own completely. It costs no new concepts, no arena, no
|
||||||
|
frame bracket; its geometry is four assignments you can read. For one panel, that is a
|
||||||
|
perfectly good deal — which is why chapter 21 still teaches it and its game still
|
||||||
|
ships it.
|
||||||
|
- `akgl_ui_dialog` is one line that costs you the subsystem: `akgl_ui_init`, a
|
||||||
|
registered font, and the bracket in your frame. The payment buys every *next* piece of
|
||||||
|
interface — the second panel is also one line, the score label is one line, the menu
|
||||||
|
is a struct — and the layout arithmetic, wrapping, and stacking are clay's problem
|
||||||
|
from then on.
|
||||||
|
|
||||||
|
Neither is deprecated. The library's own position: build one panel by hand; build an
|
||||||
|
interface on the subsystem.
|
||||||
|
|
||||||
|
## Menus: one selection, three devices
|
||||||
|
|
||||||
|
A menu is caller-owned state — a struct you keep, the way `textbox.c` keeps its statics.
|
||||||
|
There is no heap pool behind it because it owns no texture, no font, and no registry
|
||||||
|
entry:
|
||||||
|
|
||||||
|
```c excerpt=examples/uidemo/uidemo.c
|
||||||
|
static akgl_UiMenu title_menu = {
|
||||||
|
.id = "title",
|
||||||
|
.items = { "Start", "Options", "Quit" },
|
||||||
|
.count = 3,
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Declaring it each frame is `akgl_ui_menu(&title_menu)`. Input reaches it two ways, and
|
||||||
|
they meet in the same struct: `akgl_ui_menu_handle_event` moves `selected` from the
|
||||||
|
arrow keys and D-pad (wrapping at both ends) and sets `activated` on Return or gamepad
|
||||||
|
South — exactly `SDLK_UP`, `SDLK_DOWN` and `SDLK_RETURN` against `SDL_Event.key.key`,
|
||||||
|
and `SDL_GAMEPAD_BUTTON_DPAD_UP`, `_DPAD_DOWN` and `_SOUTH` against
|
||||||
|
`SDL_Event.gbutton.button`. Keycodes, not scancodes — which only matters when you
|
||||||
|
*synthesize* events, as the demo's `--demo` script does: a hand-built key event must
|
||||||
|
fill in `key.key`, or it matches nothing and the menu silently ignores it. The mouse
|
||||||
|
selects by *moving onto* a row and activates by clicking one, during the declaration
|
||||||
|
itself. A stationary pointer claims nothing — parking the mouse over the
|
||||||
|
menu must not pin the selection against the keyboard, which would otherwise fight it
|
||||||
|
sixty times a second and lose.
|
||||||
|
|
||||||
|
`activated` latches until you clear it, so the check lives wherever reading it is
|
||||||
|
convenient — the demo reads it after the frame closes, which catches both input routes:
|
||||||
|
|
||||||
|
```c excerpt=examples/uidemo/uidemo.c
|
||||||
|
if ( title_menu.activated ) {
|
||||||
|
title_menu.activated = false;
|
||||||
|
switch ( title_menu.selected ) {
|
||||||
|
case 0:
|
||||||
|
state = UIDEMO_STATE_PLAY;
|
||||||
|
score = 0;
|
||||||
|
dialog_open = false;
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
state = UIDEMO_STATE_OPTIONS;
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
default:
|
||||||
|
running = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Writing layout directly with CLAY()
|
||||||
|
|
||||||
|
The options screen uses no widgets, and exists to show that the widgets are a
|
||||||
|
convenience rather than a boundary. One row of it carries every idea — hover styling
|
||||||
|
computed *inside* the declaration, and a click paired with a press edge the application
|
||||||
|
tracks itself:
|
||||||
|
|
||||||
|
```c excerpt=examples/uidemo/uidemo.c
|
||||||
|
CLAY({
|
||||||
|
.id = CLAY_ID("options-music"),
|
||||||
|
.layout = { .padding = { 8, 8, 4, 4 } },
|
||||||
|
.backgroundColor = Clay_Hovered()
|
||||||
|
? (Clay_Color){ 64, 58, 88, 255 }
|
||||||
|
: (Clay_Color){ 0, 0, 0, 0 }
|
||||||
|
}) {
|
||||||
|
if ( Clay_Hovered() && clicked ) {
|
||||||
|
opt_music = !opt_music;
|
||||||
|
}
|
||||||
|
CLAY_TEXT(((Clay_String){ .length = (int32_t)strlen(musicrow), .chars = musicrow }),
|
||||||
|
CLAY_TEXT_CONFIG({ .textColor = { 240, 236, 214, 255 }, .fontId = 0 }));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`clicked` is one application-owned `bool`: set when a left press arrives in the event
|
||||||
|
loop — *before* `akgl_ui_handle_event` can consume it, because a click on a UI row is
|
||||||
|
always consumed — and cleared at the end of the frame. The widgets keep an equivalent
|
||||||
|
edge internally; a raw screen keeps its own. Everything else — the sizing model, scroll
|
||||||
|
containers, floating attach points, aspect ratios — is clay's DSL, and clay's README
|
||||||
|
documents it far better than a restatement here would.
|
||||||
|
|
||||||
|
## Images and styles
|
||||||
|
|
||||||
|
An image on a UI element is a sprite: set `.image.imageData` in a `CLAY()` declaration
|
||||||
|
to an `akgl_Sprite *` ([Chapter 10](10-spritesheets-and-sprites.md)) and its first frame
|
||||||
|
is drawn stretched to the element's box. Two current limits, both deliberate scope
|
||||||
|
rather than accident: clay's image tint colour and `CUSTOM` render commands are skipped
|
||||||
|
by the executor, and per-corner radii collapse to the top-left value — the widgets only
|
||||||
|
produce uniform corners. The mouse cursor stays the operating system's; a game that
|
||||||
|
wants a themed cursor draws a floating image element at the pointer, which makes a good
|
||||||
|
exercise.
|
||||||
|
|
||||||
|
A style is one struct shared by all three widgets — colours, padding, corner radius,
|
||||||
|
fontId — and `NULL` means the textbox palette described above. There is no per-field
|
||||||
|
defaulting: a transparent fill and a zero radius are things a style legitimately says,
|
||||||
|
so copy the default and change what you mean to change:
|
||||||
|
|
||||||
|
```c wrap=akglbody
|
||||||
|
akgl_UiStyle alert = {
|
||||||
|
.fill = { 120, 24, 24, 235 },
|
||||||
|
.edge = { 240, 236, 214, 255 },
|
||||||
|
.ink = { 240, 236, 214, 255 },
|
||||||
|
.padding = 8.0f,
|
||||||
|
.corner_radius = 6.0f,
|
||||||
|
.fontid = 0
|
||||||
|
};
|
||||||
|
|
||||||
|
PASS(errctx, akgl_ui_dialog("alert", "The reactor is on fire.", &alert));
|
||||||
|
```
|
||||||
|
|
||||||
|
## When things go wrong
|
||||||
|
|
||||||
|
Everything here reports through the ordinary error protocol
|
||||||
|
([Chapter 4](04-errors.md)) under one status, `AKGL_ERR_UI`, and the message says which
|
||||||
|
refusal it was. Lifecycle misuse fails at the call that misused it:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/home/andrew/source/libakgl/src/ui.c:ui_widget_ready:850: 262 (UI Error) : Widgets are declared between akgl_ui_frame_begin and akgl_ui_frame_end
|
||||||
|
/home/andrew/source/libakgl/src/ui.c:akgl_ui_dialog:866
|
||||||
|
/home/andrew/source/libakgl/main.c:main:13
|
||||||
|
/home/andrew/source/libakgl/main.c:main:16: Unhandled Error 262 (UI Error): Widgets are declared between akgl_ui_frame_begin and akgl_ui_frame_end
|
||||||
|
```
|
||||||
|
|
||||||
|
Errors *inside the layout* cannot fail at the call that caused them, because clay
|
||||||
|
reports through a void callback with libakgl nowhere on the stack. So they are logged as
|
||||||
|
they happen, stashed, and raised from `akgl_ui_frame_end` — the earliest point the
|
||||||
|
protocol can carry them — with the first message and a count of the rest. A frame that
|
||||||
|
fails is one bad frame: the next `akgl_ui_frame_begin` is legal, and the executor clears
|
||||||
|
any clip rectangle on its way out so a failed UI cannot leave the next frame's *world*
|
||||||
|
clipped.
|
||||||
|
|
||||||
|
The ceilings — elements, measured words, arena bytes, fonts, text-run length, menu
|
||||||
|
entries — are all in [Chapter 23](23-appendix-limits.md), every one of them overridable,
|
||||||
|
and every refusal names the number to raise.
|
||||||
|
|
||||||
|
## Build it and run it
|
||||||
|
|
||||||
|
The demo builds with the library:
|
||||||
|
|
||||||
|
```sh norun
|
||||||
|
cmake -S . -B build
|
||||||
|
cmake --build build -j$(nproc)
|
||||||
|
./build/examples/uidemo/uidemo
|
||||||
|
```
|
||||||
|
|
||||||
|
Arrow keys, Return, the D-pad and the mouse all drive the title menu; Space opens the
|
||||||
|
dialog on the play screen; Escape backs out. `ctest -R example_uidemo` runs the same
|
||||||
|
program headless through a scripted tour of every screen.
|
||||||
|
|
||||||
|
## Where to look next
|
||||||
|
|
||||||
|
- [clay's README](https://github.com/nicbarker/clay) — the layout DSL itself: sizing,
|
||||||
|
floating elements, scroll containers, and the debug tools.
|
||||||
|
- [Chapter 16](16-input.md) — the control maps that own the keyboard and gamepad once
|
||||||
|
the UI has declined an event.
|
||||||
|
- [Chapter 17](17-text-and-fonts.md) — fonts, the registry, and what text costs per
|
||||||
|
frame; a UI panel of text pays the same immediate-mode price.
|
||||||
|
- [Chapter 21](21-tutorial-jrpg.md) — the hand-rolled text box this chapter keeps
|
||||||
|
comparing against, in the game that earns it.
|
||||||
|
- [Chapter 23](23-appendix-limits.md) — every `AKGL_UI_*` limit and the `ui.h` status
|
||||||
|
cross-reference.
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# 22. Appendix: limits, statuses and properties
|
# 23. Appendix: limits, statuses and properties
|
||||||
|
|
||||||
Three reference tables that no single chapter owns: which function raises which status,
|
Three reference tables that no single chapter owns: which function raises which status,
|
||||||
every compile-time bound, and every configuration property the library reads.
|
every compile-time bound, and every configuration property the library reads.
|
||||||
@@ -148,6 +148,9 @@ What the statuses mean is [Chapter 4](04-errors.md).
|
|||||||
| `akgl_draw_circle` | `AKERR_NULLPOINTER`, `AKERR_OUTOFBOUNDS`, `AKGL_ERR_SDL` |
|
| `akgl_draw_circle` | `AKERR_NULLPOINTER`, `AKERR_OUTOFBOUNDS`, `AKGL_ERR_SDL` |
|
||||||
| `akgl_draw_flood_fill` | `AKERR_NULLPOINTER`, `AKGL_ERR_SDL`, `AKERR_OUTOFBOUNDS` past `AKGL_DRAW_MAX_FLOOD_SPANS` |
|
| `akgl_draw_flood_fill` | `AKERR_NULLPOINTER`, `AKGL_ERR_SDL`, `AKERR_OUTOFBOUNDS` past `AKGL_DRAW_MAX_FLOOD_SPANS` |
|
||||||
| `akgl_draw_copy_region`, `_paste_region` | `AKERR_NULLPOINTER`, `AKGL_ERR_SDL` |
|
| `akgl_draw_copy_region`, `_paste_region` | `AKERR_NULLPOINTER`, `AKGL_ERR_SDL` |
|
||||||
|
| `akgl_draw_filled_rounded_rect` | `AKERR_NULLPOINTER`, `AKGL_ERR_SDL` |
|
||||||
|
| `akgl_draw_arc` | `AKERR_NULLPOINTER`, `AKERR_OUTOFBOUNDS`, `AKGL_ERR_SDL` |
|
||||||
|
| `akgl_draw_set_clip` | `AKERR_NULLPOINTER`, `AKGL_ERR_SDL` — a `NULL` rectangle is legal here and clears the clip |
|
||||||
|
|
||||||
### `tilemap.h`
|
### `tilemap.h`
|
||||||
|
|
||||||
@@ -176,6 +179,22 @@ What the statuses mean is [Chapter 4](04-errors.md).
|
|||||||
| `akgl_controller_default` | `AKERR_OUTOFBOUNDS`, **`AKGL_ERR_REGISTRY`** — the library's only site |
|
| `akgl_controller_default` | `AKERR_OUTOFBOUNDS`, **`AKGL_ERR_REGISTRY`** — the library's only site |
|
||||||
| `akgl_controller_poll_key`, `_poll_keystroke` | `AKERR_NULLPOINTER` |
|
| `akgl_controller_poll_key`, `_poll_keystroke` | `AKERR_NULLPOINTER` |
|
||||||
|
|
||||||
|
### `ui.h`
|
||||||
|
|
||||||
|
| Function | Raises |
|
||||||
|
|---|---|
|
||||||
|
| `akgl_ui_init` | `AKERR_OUTOFBOUNDS`, `AKGL_ERR_UI` — already initialized, or an arena clay's structures do not fit; the message carries both byte counts |
|
||||||
|
| `akgl_ui_shutdown` | *nothing — idempotent by design* |
|
||||||
|
| `akgl_ui_resize` | `AKGL_ERR_UI`, `AKERR_OUTOFBOUNDS` |
|
||||||
|
| `akgl_ui_font_register` | `AKERR_NULLPOINTER`, `AKGL_ERR_UI`, `AKERR_OUTOFBOUNDS` past `AKGL_UI_FONT_NAME_LENGTH` |
|
||||||
|
| `akgl_ui_handle_event` | `AKERR_NULLPOINTER`, `AKERR_OUTOFBOUNDS` from a resize event |
|
||||||
|
| `akgl_ui_frame_begin` | `AKGL_ERR_UI` — uninitialized, or a frame already open |
|
||||||
|
| `akgl_ui_frame_end` | `AKERR_NULLPOINTER`, `AKGL_ERR_UI` — no frame open, clay layout errors, an over-long text run, an unresolvable fontId — plus whatever the draw primitives raise |
|
||||||
|
| `akgl_ui_dialog`, `_label`, `_menu` | `AKERR_NULLPOINTER`, `AKGL_ERR_UI` outside the frame bracket; `_label` and `_menu` add `AKERR_OUTOFBOUNDS` |
|
||||||
|
| `akgl_ui_menu_handle_event` | `AKERR_NULLPOINTER`, `AKERR_OUTOFBOUNDS` |
|
||||||
|
| `akgl_ui_execute_commands` | `AKERR_NULLPOINTER`, `AKGL_ERR_UI`, plus whatever the draw primitives raise |
|
||||||
|
| `akgl_ui_measure_text` | *cannot report — clay's callback signature; failures stash and surface from `akgl_ui_frame_end`* |
|
||||||
|
|
||||||
### `text.h`, `audio.h`, `assets.h`, `util.h`, `staticstring.h`
|
### `text.h`, `audio.h`, `assets.h`, `util.h`, `staticstring.h`
|
||||||
|
|
||||||
| Function | Raises |
|
| Function | Raises |
|
||||||
@@ -196,8 +215,9 @@ What the statuses mean is [Chapter 4](04-errors.md).
|
|||||||
|
|
||||||
## B. Compile-time limits
|
## B. Compile-time limits
|
||||||
|
|
||||||
Everything below is fixed when the library is compiled. **Only the `AKGL_MAX_HEAP_*` eight
|
Everything below is fixed when the library is compiled. **Only the `AKGL_MAX_HEAP_*` eight,
|
||||||
and `AKGL_CCD_ARENA_BYTES` are overridable**, and even those have to be overridden for the whole build — see
|
`AKGL_CCD_ARENA_BYTES` and the `AKGL_UI_*` family are overridable**, and even those have to
|
||||||
|
be overridden for the whole build — see
|
||||||
[Chapter 5](05-the-heap.md), "The ceilings are a compile-time ABI constraint". The rest are
|
[Chapter 5](05-the-heap.md), "The ceilings are a compile-time ABI constraint". The rest are
|
||||||
plain `#define`s with no `#ifndef` guard: changing one means editing the header and
|
plain `#define`s with no `#ifndef` guard: changing one means editing the header and
|
||||||
rebuilding libakgl and everything linking it.
|
rebuilding libakgl and everything linking it.
|
||||||
@@ -347,6 +367,31 @@ keeps instead of recursing per pixel. A region needing more pending runs at once
|
|||||||
`AKERR_OUTOFBOUNDS` rather than overflowing; an ordinary convex or moderately concave shape
|
`AKERR_OUTOFBOUNDS` rather than overflowing; an ordinary convex or moderately concave shape
|
||||||
needs a few dozen.
|
needs a few dozen.
|
||||||
|
|
||||||
|
`AKGL_DRAW_ROUNDED_RECT_SEGMENTS` is 16 — triangles per corner of
|
||||||
|
`akgl_draw_filled_rounded_rect`. `AKGL_DRAW_ARC_MAX_POINTS` is 64 — the vertex ceiling one
|
||||||
|
`akgl_draw_arc` spends, two per step along the curve; the step count scales with the swept
|
||||||
|
angle under it. Both size fixed arrays, so neither is a per-call cost and neither is
|
||||||
|
overridable.
|
||||||
|
|
||||||
|
### The UI
|
||||||
|
|
||||||
|
Every `AKGL_UI_*` limit is guarded by `#ifndef`, so a consumer may override any of them the
|
||||||
|
way the pool ceilings are overridden — for the whole build, before `<akgl/ui.h>` is seen.
|
||||||
|
`akgl_ui_init` checks that the arena still fits the element and word ceilings and refuses
|
||||||
|
with both byte counts in the message when it does not, so a raised ceiling without a raised
|
||||||
|
arena is a loud startup failure rather than a corruption.
|
||||||
|
|
||||||
|
| Constant | Value | Bounds |
|
||||||
|
|---|---|---|
|
||||||
|
| `AKGL_UI_MAX_ELEMENTS` | 1024 | Elements one layout may declare |
|
||||||
|
| `AKGL_UI_MAX_MEASURE_WORDS` | 4096 | Words clay's text-measurement cache holds |
|
||||||
|
| `AKGL_UI_ARENA_BYTES` | 1048576 | Static storage clay lays its structures out in (812544 needed at the defaults) |
|
||||||
|
| `AKGL_UI_MAX_FONTS` | 8 | fontId table slots |
|
||||||
|
| `AKGL_UI_FONT_NAME_LENGTH` | 64 | Bytes per fontId slot's registry name, terminator included |
|
||||||
|
| `AKGL_UI_MAX_TEXT_BYTES` | 1024 | Bytes one text render command's line may occupy |
|
||||||
|
| `AKGL_UI_MENU_MAX_ITEMS` | 16 | Entries one `akgl_UiMenu` may carry |
|
||||||
|
| `AKGL_UI_DIALOG_HEIGHT` | 56 | Height of the `akgl_ui_dialog` panel, in pixels |
|
||||||
|
|
||||||
### Audio
|
### Audio
|
||||||
|
|
||||||
```c excerpt=include/akgl/audio.h
|
```c excerpt=include/akgl/audio.h
|
||||||
@@ -383,8 +428,8 @@ budget and was wrong by a factor of a thousand, blocking for roughly sixteen min
|
|||||||
| Constant | Value |
|
| Constant | Value |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `AKGL_ERR_BASE` | 256 (`AKERR_FIRST_CONSUMER_STATUS`) |
|
| `AKGL_ERR_BASE` | 256 (`AKERR_FIRST_CONSUMER_STATUS`) |
|
||||||
| `AKGL_ERR_COUNT` | 5 |
|
| `AKGL_ERR_COUNT` | 7 |
|
||||||
| `AKGL_ERR_LIMIT` | 261 — **the one-past-the-end sentinel, not a status** |
|
| `AKGL_ERR_LIMIT` | 263 — **the one-past-the-end sentinel, not a status** |
|
||||||
|
|
||||||
## C. Configuration properties
|
## C. Configuration properties
|
||||||
|
|
||||||
@@ -34,7 +34,8 @@ per-function reference, build the Doxygen output with `doxygen Doxyfile`.
|
|||||||
| **[19. Utilities](19-utilities.md)** | Rectangle overlap, path resolution, the JSON accessors, static strings |
|
| **[19. Utilities](19-utilities.md)** | Rectangle overlap, path resolution, the JSON accessors, static strings |
|
||||||
| **[20. Tutorial: a 2D sidescroller](20-tutorial-sidescroller.md)** | Thirteen steps from an empty directory to a game with gravity, a jump, coins and hazards. **Start here** |
|
| **[20. Tutorial: a 2D sidescroller](20-tutorial-sidescroller.md)** | Thirteen steps from an empty directory to a game with gravity, a jump, coins and hazards. **Start here** |
|
||||||
| **[21. Tutorial: a top-down JRPG](21-tutorial-jrpg.md)** | Thirteen more, for four-way movement, NPCs, a text box, a follower, and freezing the world |
|
| **[21. Tutorial: a top-down JRPG](21-tutorial-jrpg.md)** | Thirteen more, for four-way movement, NPCs, a text box, a follower, and freezing the world |
|
||||||
| **[22. Appendix](22-appendix-limits.md)** | Every limit, every status, every configuration property |
|
| **[22. User interfaces](22-ui.md)** | Menus, HUDs and dialogs on the clay layout engine: the widgets, the frame bracket, and writing `CLAY()` yourself |
|
||||||
|
| **[23. Appendix](23-appendix-limits.md)** | Every limit, every status, every configuration property |
|
||||||
|
|
||||||
**If you are new, read chapter 20 first and read it in order.** It builds a working game from
|
**If you are new, read chapter 20 first and read it in order.** It builds a working game from
|
||||||
an empty directory and teaches the library as it needs each piece; chapter 21 assumes it.
|
an empty directory and teaches the library as it needs each piece; chapter 21 assumes it.
|
||||||
|
|||||||
BIN
docs/images/uidemo.png
Normal file
BIN
docs/images/uidemo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
@@ -1,4 +1,4 @@
|
|||||||
# The two tutorial games.
|
# The tutorial games and demos.
|
||||||
#
|
#
|
||||||
# Each one is a complete, running program that the matching chapter quotes with
|
# Each one is a complete, running program that the matching chapter quotes with
|
||||||
# `c excerpt=examples/...` blocks rather than restating -- so a tutorial cannot
|
# `c excerpt=examples/...` blocks rather than restating -- so a tutorial cannot
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
# passes and land at different times, and a configure that fails because one of
|
# passes and land at different times, and a configure that fails because one of
|
||||||
# them is not there yet would block the other. There is nothing clever about the
|
# them is not there yet would block the other. There is nothing clever about the
|
||||||
# guard; it exists so an incomplete tree still builds.
|
# guard; it exists so an incomplete tree still builds.
|
||||||
foreach(_example sidescroller jrpg)
|
foreach(_example sidescroller jrpg uidemo)
|
||||||
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/${_example}/CMakeLists.txt")
|
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/${_example}/CMakeLists.txt")
|
||||||
add_subdirectory(${_example})
|
add_subdirectory(${_example})
|
||||||
endif()
|
endif()
|
||||||
|
|||||||
@@ -47,26 +47,31 @@ endif()
|
|||||||
# it to suppress the vendored projects' registrations and lifts the suppression
|
# it to suppress the vendored projects' registrations and lifts the suppression
|
||||||
# again long before examples/ is added.
|
# again long before examples/ is added.
|
||||||
add_test(NAME example_jrpg COMMAND jrpg --frames 320 --demo)
|
add_test(NAME example_jrpg COMMAND jrpg --frames 320 --demo)
|
||||||
set_tests_properties(example_jrpg PROPERTIES
|
set_tests_properties(example_jrpg PROPERTIES TIMEOUT 120)
|
||||||
TIMEOUT 120
|
# set_property, not set_tests_properties: the latter parses its PROPERTIES
|
||||||
ENVIRONMENT "SDL_VIDEODRIVER=dummy;SDL_RENDER_DRIVER=software;SDL_AUDIODRIVER=dummy"
|
# arguments as name/value *pairs*, so a semicolon-separated value is split and
|
||||||
|
# every element after the first is consumed as a bogus property name. The
|
||||||
|
# audio and render entries of this list were silently lost that way --
|
||||||
|
# invisible on any machine whose real audio device works, and found the first
|
||||||
|
# time CI ran on a runner without one.
|
||||||
|
set_property(TEST example_jrpg PROPERTY ENVIRONMENT
|
||||||
|
"SDL_VIDEODRIVER=dummy"
|
||||||
|
"SDL_RENDER_DRIVER=software"
|
||||||
|
"SDL_AUDIODRIVER=dummy"
|
||||||
)
|
)
|
||||||
|
|
||||||
# ENVIRONMENT above replaces the environment wholesale, so LD_LIBRARY_PATH has
|
# LD_LIBRARY_PATH for the vendored satellite libraries. Only needed when they
|
||||||
# to go in the same property rather than a second one. Only needed when the
|
# were vendored; an installed build resolves them normally.
|
||||||
# dependencies were vendored; an installed build resolves them normally.
|
|
||||||
if(AKGL_VENDORED_DEPENDENCIES)
|
if(AKGL_VENDORED_DEPENDENCIES)
|
||||||
if(CMAKE_VERSION VERSION_GREATER_EQUAL "3.22")
|
if(CMAKE_VERSION VERSION_GREATER_EQUAL "3.22")
|
||||||
set(JRPG_TEST_ENV_MOD "")
|
set(JRPG_TEST_ENV_MOD "")
|
||||||
foreach(dir IN LISTS AKGL_TEST_LIBPATH)
|
foreach(dir IN LISTS AKGL_TEST_LIBPATH)
|
||||||
list(APPEND JRPG_TEST_ENV_MOD "LD_LIBRARY_PATH=path_list_prepend:${dir}")
|
list(APPEND JRPG_TEST_ENV_MOD "LD_LIBRARY_PATH=path_list_prepend:${dir}")
|
||||||
endforeach()
|
endforeach()
|
||||||
set_tests_properties(example_jrpg
|
set_property(TEST example_jrpg PROPERTY ENVIRONMENT_MODIFICATION ${JRPG_TEST_ENV_MOD})
|
||||||
PROPERTIES ENVIRONMENT_MODIFICATION "${JRPG_TEST_ENV_MOD}")
|
|
||||||
else()
|
else()
|
||||||
string(REPLACE ";" ":" JRPG_TEST_LIBPATH_JOINED "${AKGL_TEST_LIBPATH}")
|
string(REPLACE ";" ":" JRPG_TEST_LIBPATH_JOINED "${AKGL_TEST_LIBPATH}")
|
||||||
set_tests_properties(example_jrpg PROPERTIES
|
set_property(TEST example_jrpg APPEND PROPERTY ENVIRONMENT
|
||||||
ENVIRONMENT "SDL_VIDEODRIVER=dummy;SDL_RENDER_DRIVER=software;SDL_AUDIODRIVER=dummy;LD_LIBRARY_PATH=${JRPG_TEST_LIBPATH_JOINED}:$ENV{LD_LIBRARY_PATH}"
|
"LD_LIBRARY_PATH=${JRPG_TEST_LIBPATH_JOINED}:$ENV{LD_LIBRARY_PATH}")
|
||||||
)
|
|
||||||
endif()
|
endif()
|
||||||
endif()
|
endif()
|
||||||
|
|||||||
@@ -41,7 +41,15 @@ endif()
|
|||||||
# while this project is top-level, and by the time examples/ is added the
|
# while this project is top-level, and by the time examples/ is added the
|
||||||
# suppression has already been lifted. An embedded build gets the builtin.
|
# suppression has already been lifted. An embedded build gets the builtin.
|
||||||
add_test(NAME example_sidescroller COMMAND sidescroller --frames 240 --autoplay)
|
add_test(NAME example_sidescroller COMMAND sidescroller --frames 240 --autoplay)
|
||||||
set_tests_properties(example_sidescroller PROPERTIES
|
set_tests_properties(example_sidescroller PROPERTIES TIMEOUT 120)
|
||||||
TIMEOUT 120
|
# set_property, not set_tests_properties: the latter parses its PROPERTIES
|
||||||
ENVIRONMENT "SDL_VIDEODRIVER=dummy;SDL_RENDER_DRIVER=software;SDL_AUDIODRIVER=dummy"
|
# arguments as name/value *pairs*, so a semicolon-separated value is split and
|
||||||
|
# every element after the first is consumed as a bogus property name. The
|
||||||
|
# audio and render entries of this very list were silently lost that way for
|
||||||
|
# as long as the list existed -- invisible on any machine whose real audio
|
||||||
|
# device works, and found the first time CI ran on a runner without one.
|
||||||
|
set_property(TEST example_sidescroller PROPERTY ENVIRONMENT
|
||||||
|
"SDL_VIDEODRIVER=dummy"
|
||||||
|
"SDL_RENDER_DRIVER=software"
|
||||||
|
"SDL_AUDIODRIVER=dummy"
|
||||||
)
|
)
|
||||||
|
|||||||
62
examples/uidemo/CMakeLists.txt
Normal file
62
examples/uidemo/CMakeLists.txt
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
# The UI demo, quoted by the UI chapter's `c excerpt=` blocks.
|
||||||
|
#
|
||||||
|
# A real target built by `all`, on the same terms as the tutorial games: a
|
||||||
|
# chapter whose program does not compile is the failure the documentation
|
||||||
|
# harness exists to stop.
|
||||||
|
|
||||||
|
add_executable(uidemo
|
||||||
|
uidemo.c
|
||||||
|
)
|
||||||
|
|
||||||
|
target_link_libraries(uidemo
|
||||||
|
PRIVATE akstdlib::akstdlib akerror::akerror akgl
|
||||||
|
SDL3::SDL3 SDL3_ttf::SDL3_ttf SDL3_image::SDL3_image SDL3_mixer::SDL3_mixer
|
||||||
|
jansson::jansson -lm)
|
||||||
|
target_include_directories(uidemo PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}")
|
||||||
|
target_compile_options(uidemo PRIVATE ${AKGL_WARNING_FLAGS})
|
||||||
|
|
||||||
|
# The demo needs no art -- its "world" is akgl_draw_background -- but it does
|
||||||
|
# need one font, and it uses the same test-asset face the JRPG does, compiled
|
||||||
|
# in as an absolute path for the same run-from-anywhere reason.
|
||||||
|
get_filename_component(UIDEMO_REPO_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/../.." ABSOLUTE)
|
||||||
|
target_compile_definitions(uidemo PRIVATE
|
||||||
|
UIDEMO_FONT_FILE="${UIDEMO_REPO_ROOT}/tests/assets/akgl_test_mono.ttf"
|
||||||
|
)
|
||||||
|
|
||||||
|
if(AKGL_VENDORED_DEPENDENCIES)
|
||||||
|
set_target_properties(uidemo PROPERTIES BUILD_RPATH "${AKGL_VENDORED_RPATH}")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# The headless smoke run. `--demo` tours every screen through the real event
|
||||||
|
# chain -- a mouse click on the title menu (the consumed path), keyboard into
|
||||||
|
# and out of the raw-CLAY options screen, the play screen's dialog opened and
|
||||||
|
# dismissed, and Quit confirmed from the menu -- rather than just proving that
|
||||||
|
# main() returns. There is no physics here, so nothing needs a driven clock;
|
||||||
|
# `--frames` is a backstop above the script's last step, not the exit path.
|
||||||
|
add_test(NAME example_uidemo COMMAND uidemo --frames 240 --demo)
|
||||||
|
set_tests_properties(example_uidemo PROPERTIES TIMEOUT 120)
|
||||||
|
# set_property, not set_tests_properties: the latter parses its PROPERTIES
|
||||||
|
# arguments as name/value *pairs*, so a semicolon-separated value is split and
|
||||||
|
# every element after the first is consumed as a bogus property name. See the
|
||||||
|
# same note in the other examples' CMakeLists.
|
||||||
|
set_property(TEST example_uidemo PROPERTY ENVIRONMENT
|
||||||
|
"SDL_VIDEODRIVER=dummy"
|
||||||
|
"SDL_RENDER_DRIVER=software"
|
||||||
|
"SDL_AUDIODRIVER=dummy"
|
||||||
|
)
|
||||||
|
|
||||||
|
# LD_LIBRARY_PATH for the vendored satellite libraries. Only needed when they
|
||||||
|
# were vendored; an installed build resolves them normally.
|
||||||
|
if(AKGL_VENDORED_DEPENDENCIES)
|
||||||
|
if(CMAKE_VERSION VERSION_GREATER_EQUAL "3.22")
|
||||||
|
set(UIDEMO_TEST_ENV_MOD "")
|
||||||
|
foreach(dir IN LISTS AKGL_TEST_LIBPATH)
|
||||||
|
list(APPEND UIDEMO_TEST_ENV_MOD "LD_LIBRARY_PATH=path_list_prepend:${dir}")
|
||||||
|
endforeach()
|
||||||
|
set_property(TEST example_uidemo PROPERTY ENVIRONMENT_MODIFICATION ${UIDEMO_TEST_ENV_MOD})
|
||||||
|
else()
|
||||||
|
string(REPLACE ";" ":" UIDEMO_TEST_LIBPATH_JOINED "${AKGL_TEST_LIBPATH}")
|
||||||
|
set_property(TEST example_uidemo APPEND PROPERTY ENVIRONMENT
|
||||||
|
"LD_LIBRARY_PATH=${UIDEMO_TEST_LIBPATH_JOINED}:$ENV{LD_LIBRARY_PATH}")
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
609
examples/uidemo/uidemo.c
Normal file
609
examples/uidemo/uidemo.c
Normal file
@@ -0,0 +1,609 @@
|
|||||||
|
/**
|
||||||
|
* @file uidemo.c
|
||||||
|
* @brief The UI demo: a title menu, an options screen, and a HUD, three ways.
|
||||||
|
*
|
||||||
|
* The chapter on the UI subsystem quotes this program rather than restating
|
||||||
|
* it. Run it with no arguments for a window:
|
||||||
|
*
|
||||||
|
* ./examples/uidemo/uidemo
|
||||||
|
*
|
||||||
|
* The title menu answers to the arrow keys, Return, the D-pad, and the mouse.
|
||||||
|
* Start opens a stand-in play screen -- a checkerboard where a game would be
|
||||||
|
* -- with a score counter ticking in a HUD label; Space opens and closes a
|
||||||
|
* dialog panel there, and Escape backs out. Options is a screen written in
|
||||||
|
* raw CLAY() declarations, because the widgets are a convenience, not a
|
||||||
|
* boundary. `--frames N` bounds the run and `--demo` drives the whole tour
|
||||||
|
* from a script, which is what makes this runnable as a headless smoke test.
|
||||||
|
*
|
||||||
|
* What is deliberately absent: a tilemap, actors, physics. The world here is
|
||||||
|
* one draw call, so everything left is the subject -- what a UI costs and
|
||||||
|
* where it goes in a frame.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
#include <SDL3_image/SDL_image.h>
|
||||||
|
#include <akerror.h>
|
||||||
|
#include <akstdlib.h>
|
||||||
|
|
||||||
|
#include <akgl/draw.h>
|
||||||
|
#include <akgl/error.h>
|
||||||
|
#include <akgl/game.h>
|
||||||
|
#include <akgl/registry.h>
|
||||||
|
#include <akgl/renderer.h>
|
||||||
|
#include <akgl/text.h>
|
||||||
|
#include <akgl/ui.h>
|
||||||
|
|
||||||
|
#include "uidemo.h"
|
||||||
|
|
||||||
|
/** @brief Milliseconds one 60 Hz frame is allowed to take, for the interactive loop. */
|
||||||
|
#define UIDEMO_FRAME_BUDGET_MS 16
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The scripted tour `--demo` drives. Every route through the demo, by every
|
||||||
|
* input device it supports: the mouse clicks the title menu open (dead centre
|
||||||
|
* lands on the middle row by symmetry -- the menu is centred and Options is
|
||||||
|
* its middle entry), the keyboard walks back out, into the play screen,
|
||||||
|
* through the dialog, and finally down to Quit.
|
||||||
|
*
|
||||||
|
* Frame Event Key / position What it proves
|
||||||
|
*/
|
||||||
|
static const uidemo_ScriptStep UIDEMO_SCRIPT[] = {
|
||||||
|
{ 5, SDL_EVENT_MOUSE_MOTION, 0, 320.0f, 240.0f }, /* hover the menu */
|
||||||
|
{ 10, SDL_EVENT_MOUSE_BUTTON_DOWN, 0, 320.0f, 240.0f }, /* consumed click */
|
||||||
|
{ 11, SDL_EVENT_MOUSE_BUTTON_UP, 0, 320.0f, 240.0f }, /* -> Options screen */
|
||||||
|
{ 40, SDL_EVENT_KEY_DOWN, SDLK_ESCAPE, 0.0f, 0.0f }, /* back to the title */
|
||||||
|
{ 50, SDL_EVENT_KEY_DOWN, SDLK_UP, 0.0f, 0.0f }, /* select Start */
|
||||||
|
{ 60, SDL_EVENT_KEY_DOWN, SDLK_RETURN, 0.0f, 0.0f }, /* -> play screen */
|
||||||
|
{ 80, SDL_EVENT_KEY_DOWN, SDLK_SPACE, 0.0f, 0.0f }, /* open the dialog */
|
||||||
|
{ 120, SDL_EVENT_KEY_DOWN, SDLK_SPACE, 0.0f, 0.0f }, /* close it again */
|
||||||
|
{ 140, SDL_EVENT_KEY_DOWN, SDLK_ESCAPE, 0.0f, 0.0f }, /* back to the title */
|
||||||
|
{ 150, SDL_EVENT_KEY_DOWN, SDLK_DOWN, 0.0f, 0.0f }, /* down to Options */
|
||||||
|
{ 160, SDL_EVENT_KEY_DOWN, SDLK_DOWN, 0.0f, 0.0f }, /* down to Quit */
|
||||||
|
{ 170, SDL_EVENT_KEY_DOWN, SDLK_RETURN, 0.0f, 0.0f } /* and out */
|
||||||
|
};
|
||||||
|
|
||||||
|
#define UIDEMO_SCRIPT_STEPS (sizeof(UIDEMO_SCRIPT) / sizeof(UIDEMO_SCRIPT[0]))
|
||||||
|
|
||||||
|
static long frame_limit = 0;
|
||||||
|
/** @brief Where `--screenshot` writes, and on which frame. NULL means never. */
|
||||||
|
static char *shotpath = NULL;
|
||||||
|
static long shotframe = 0;
|
||||||
|
static bool demo = false;
|
||||||
|
static bool running = true;
|
||||||
|
static int exitstatus = 0;
|
||||||
|
static bool lowfps_warned = false;
|
||||||
|
|
||||||
|
/** @brief Which screen the frame declares. */
|
||||||
|
static uidemo_State state = UIDEMO_STATE_TITLE;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The title menu. Caller-owned state, zero machinery: this struct and
|
||||||
|
* the akgl_ui_menu call each frame are the whole main menu.
|
||||||
|
*/
|
||||||
|
static akgl_UiMenu title_menu = {
|
||||||
|
.id = "title",
|
||||||
|
.items = { "Start", "Options", "Quit" },
|
||||||
|
.count = 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
/** @brief The options screen's two settings. What the toggles flip. */
|
||||||
|
static bool opt_music = true;
|
||||||
|
static bool opt_sound = true;
|
||||||
|
|
||||||
|
/** @brief The play screen's HUD numbers. The score ticks so the label visibly updates. */
|
||||||
|
static int score = 0;
|
||||||
|
static int lives = 3;
|
||||||
|
/** @brief Whether the play screen's dialog is up. Space flips it. */
|
||||||
|
static bool dialog_open = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief A left click arrived this frame, wherever it landed.
|
||||||
|
*
|
||||||
|
* The raw CLAY() options screen needs a press edge to pair with
|
||||||
|
* Clay_Hovered(), and the application is the right owner of it: it sees
|
||||||
|
* every event before the UI does. Set in route_event, cleared at the end of
|
||||||
|
* each frame. The widget helpers keep their own edge internally -- this one
|
||||||
|
* exists precisely because the options screen does not use them.
|
||||||
|
*/
|
||||||
|
static bool clicked = false;
|
||||||
|
|
||||||
|
/** @brief Replacement `akgl_game.lowfpsfunc`: say it once, not sixty times a second. */
|
||||||
|
static void lowfps_quiet(void)
|
||||||
|
{
|
||||||
|
if ( lowfps_warned == false ) {
|
||||||
|
lowfps_warned = true;
|
||||||
|
SDL_Log("Frame rate is under 30 and this demo does nothing about it");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Read `--frames N`, `--demo` and the screenshot flags off the command line.
|
||||||
|
*
|
||||||
|
* @param argc Argument count, from `main`.
|
||||||
|
* @param argv Argument vector, from `main`. Required.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER If @p argv is `NULL`.
|
||||||
|
* @throws AKERR_VALUE If a flag needing an argument is last, or its argument
|
||||||
|
* is not a number.
|
||||||
|
*/
|
||||||
|
static akerr_ErrorContext *parse_args(int argc, char *argv[])
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
int i = 0;
|
||||||
|
int number = 0;
|
||||||
|
|
||||||
|
FAIL_ZERO_RETURN(errctx, argv, AKERR_NULLPOINTER, "argv");
|
||||||
|
|
||||||
|
for ( i = 1; i < argc; i++ ) {
|
||||||
|
if ( strcmp(argv[i], "--demo") == 0 ) {
|
||||||
|
demo = true;
|
||||||
|
} else if ( strcmp(argv[i], "--frames") == 0 ) {
|
||||||
|
if ( (i + 1) >= argc ) {
|
||||||
|
FAIL_RETURN(errctx, AKERR_VALUE, "--frames needs a frame count");
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
PASS(errctx, aksl_atoi(argv[i], &number));
|
||||||
|
frame_limit = number;
|
||||||
|
} else if ( strcmp(argv[i], "--screenshot") == 0 ) {
|
||||||
|
if ( (i + 1) >= argc ) {
|
||||||
|
FAIL_RETURN(errctx, AKERR_VALUE, "--screenshot needs a path");
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
shotpath = argv[i];
|
||||||
|
} else if ( strcmp(argv[i], "--screenshot-frame") == 0 ) {
|
||||||
|
if ( (i + 1) >= argc ) {
|
||||||
|
FAIL_RETURN(errctx, AKERR_VALUE, "--screenshot-frame needs a number");
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
PASS(errctx, aksl_atoi(argv[i], &number));
|
||||||
|
shotframe = number;
|
||||||
|
} else {
|
||||||
|
FAIL_RETURN(errctx, AKERR_VALUE,
|
||||||
|
"usage: uidemo [--frames N] [--demo]"
|
||||||
|
" [--screenshot PATH] [--screenshot-frame N]");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Bring the library and the UI subsystem up.
|
||||||
|
*
|
||||||
|
* The UI half is three calls: load a font, akgl_ui_init at the screen size,
|
||||||
|
* and register the font for a clay fontId. The id comes back 0 because it is
|
||||||
|
* the first registered, which is what the widgets' default style uses -- so
|
||||||
|
* nothing here ever mentions a fontId again.
|
||||||
|
*
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
*/
|
||||||
|
static akerr_ErrorContext *startup(void)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
uint16_t fontid = 0;
|
||||||
|
|
||||||
|
PASS(errctx, aksl_strncpy(akgl_game.name, sizeof(akgl_game.name), "libakgl UI demo", sizeof(akgl_game.name) - 1));
|
||||||
|
PASS(errctx, aksl_strncpy(akgl_game.version, sizeof(akgl_game.version), "1.0.0", sizeof(akgl_game.version) - 1));
|
||||||
|
PASS(errctx, aksl_strncpy(akgl_game.uri, sizeof(akgl_game.uri), "net.aklabs.libakgl.examples.uidemo", sizeof(akgl_game.uri) - 1));
|
||||||
|
|
||||||
|
PASS(errctx, akgl_game_init());
|
||||||
|
akgl_game.lowfpsfunc = &lowfps_quiet;
|
||||||
|
|
||||||
|
PASS(errctx, akgl_set_property("game.screenwidth", UIDEMO_SCREEN_WIDTH));
|
||||||
|
PASS(errctx, akgl_set_property("game.screenheight", UIDEMO_SCREEN_HEIGHT));
|
||||||
|
PASS(errctx, akgl_render_2d_init(akgl_renderer));
|
||||||
|
|
||||||
|
PASS(errctx, akgl_text_loadfont(UIDEMO_FONT_NAME, UIDEMO_FONT_FILE, UIDEMO_FONT_SIZE));
|
||||||
|
PASS(errctx, akgl_ui_init(UIDEMO_WIDTH, UIDEMO_HEIGHT));
|
||||||
|
PASS(errctx, akgl_ui_font_register(UIDEMO_FONT_NAME, &fontid));
|
||||||
|
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Route one event: the UI first, then whatever the current screen wants.
|
||||||
|
*
|
||||||
|
* This is the call-order contract from akgl/ui.h in the flesh. The UI gets
|
||||||
|
* first refusal; a consumed event goes no further, which is what keeps a
|
||||||
|
* click on a menu from also being a click in the game. Only then does the
|
||||||
|
* current screen read the keyboard -- and *that* routing is the whole focus
|
||||||
|
* model: the title menu hears keys because this function sends them there
|
||||||
|
* while the title screen is up, not because anything owns "focus".
|
||||||
|
*
|
||||||
|
* @param event The event to route. Required.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
*/
|
||||||
|
static akerr_ErrorContext *route_event(SDL_Event *event)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
bool consumed = false;
|
||||||
|
|
||||||
|
FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "event");
|
||||||
|
|
||||||
|
if ( event->type == SDL_EVENT_QUIT ) {
|
||||||
|
running = false;
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
// The options screen's press edge, recorded before the UI can consume
|
||||||
|
// the event -- a click on a toggle row is *always* consumed (the row is
|
||||||
|
// UI), so waiting until afterwards would record nothing.
|
||||||
|
if ( (event->type == SDL_EVENT_MOUSE_BUTTON_DOWN)
|
||||||
|
&& (event->button.button == SDL_BUTTON_LEFT) ) {
|
||||||
|
clicked = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
PASS(errctx, akgl_ui_handle_event((void *)&akgl_game.state, event, &consumed));
|
||||||
|
if ( consumed ) {
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ( state ) {
|
||||||
|
case UIDEMO_STATE_TITLE:
|
||||||
|
PASS(errctx, akgl_ui_menu_handle_event(&title_menu, event, &consumed));
|
||||||
|
break;
|
||||||
|
case UIDEMO_STATE_OPTIONS:
|
||||||
|
if ( (event->type == SDL_EVENT_KEY_DOWN) && (event->key.key == SDLK_ESCAPE) ) {
|
||||||
|
state = UIDEMO_STATE_TITLE;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case UIDEMO_STATE_PLAY:
|
||||||
|
if ( event->type == SDL_EVENT_KEY_DOWN ) {
|
||||||
|
if ( event->key.key == SDLK_ESCAPE ) {
|
||||||
|
state = UIDEMO_STATE_TITLE;
|
||||||
|
} else if ( event->key.key == SDLK_SPACE ) {
|
||||||
|
dialog_open = !dialog_open;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Feed the scripted events due on this frame through the real routing.
|
||||||
|
*
|
||||||
|
* Synthesized SDL_Events, not direct state changes: the demo run proves the
|
||||||
|
* event chain -- consumed and not, menu and screen -- because it travels it.
|
||||||
|
*
|
||||||
|
* @param frameno The frame about to be drawn.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
*/
|
||||||
|
static akerr_ErrorContext *demo_step(long frameno)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
SDL_Event event;
|
||||||
|
size_t i = 0;
|
||||||
|
|
||||||
|
for ( i = 0; i < UIDEMO_SCRIPT_STEPS; i++ ) {
|
||||||
|
if ( UIDEMO_SCRIPT[i].frame != frameno ) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
PASS(errctx, aksl_memset((void *)&event, 0x00, sizeof(event)));
|
||||||
|
event.type = UIDEMO_SCRIPT[i].type;
|
||||||
|
switch ( UIDEMO_SCRIPT[i].type ) {
|
||||||
|
case SDL_EVENT_KEY_DOWN:
|
||||||
|
event.key.key = UIDEMO_SCRIPT[i].key;
|
||||||
|
break;
|
||||||
|
case SDL_EVENT_MOUSE_MOTION:
|
||||||
|
event.motion.x = UIDEMO_SCRIPT[i].x;
|
||||||
|
event.motion.y = UIDEMO_SCRIPT[i].y;
|
||||||
|
break;
|
||||||
|
case SDL_EVENT_MOUSE_BUTTON_DOWN:
|
||||||
|
case SDL_EVENT_MOUSE_BUTTON_UP:
|
||||||
|
event.button.button = SDL_BUTTON_LEFT;
|
||||||
|
event.button.x = UIDEMO_SCRIPT[i].x;
|
||||||
|
event.button.y = UIDEMO_SCRIPT[i].y;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
PASS(errctx, route_event(&event));
|
||||||
|
}
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Declare the title screen: one widget call.
|
||||||
|
*
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
*/
|
||||||
|
static akerr_ErrorContext *declare_title(void)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
PASS(errctx, akgl_ui_label("heading", "libakgl UI demo", AKGL_UI_ANCHOR_TOP_LEFT, NULL));
|
||||||
|
PASS(errctx, akgl_ui_menu(&title_menu));
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Declare the options screen in raw CLAY(), no widgets anywhere.
|
||||||
|
*
|
||||||
|
* This screen exists to show the other way in: a centred panel, a column of
|
||||||
|
* rows, hover highlighting via Clay_Hovered() *inside* the declaration, and
|
||||||
|
* clicks paired with the application's own press edge. Everything the widgets
|
||||||
|
* do is done with these same pieces; a screen the widgets cannot express is
|
||||||
|
* written like this rather than waiting for a widget to exist.
|
||||||
|
*
|
||||||
|
* The row labels live in statics because clay borrows the pointer until
|
||||||
|
* frame_end -- a local buffer here would be dangling by the time the text is
|
||||||
|
* drawn.
|
||||||
|
*
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
*/
|
||||||
|
static akerr_ErrorContext *declare_options(void)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
static char musicrow[32];
|
||||||
|
static char soundrow[32];
|
||||||
|
int count = 0;
|
||||||
|
|
||||||
|
PASS(errctx, aksl_snprintf(&count, musicrow, sizeof(musicrow), "Music: %s", opt_music ? "ON" : "OFF"));
|
||||||
|
PASS(errctx, aksl_snprintf(&count, soundrow, sizeof(soundrow), "Sound: %s", opt_sound ? "ON" : "OFF"));
|
||||||
|
|
||||||
|
CLAY({
|
||||||
|
.id = CLAY_ID("options"),
|
||||||
|
.layout = {
|
||||||
|
.padding = { 16, 16, 16, 16 },
|
||||||
|
.childGap = 8,
|
||||||
|
.layoutDirection = CLAY_TOP_TO_BOTTOM
|
||||||
|
},
|
||||||
|
.backgroundColor = { 24, 20, 37, 235 },
|
||||||
|
.border = { .color = { 240, 236, 214, 255 }, .width = { 1, 1, 1, 1, 0 } },
|
||||||
|
.floating = {
|
||||||
|
.attachPoints = {
|
||||||
|
.element = CLAY_ATTACH_POINT_CENTER_CENTER,
|
||||||
|
.parent = CLAY_ATTACH_POINT_CENTER_CENTER
|
||||||
|
},
|
||||||
|
.attachTo = CLAY_ATTACH_TO_ROOT
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
CLAY_TEXT(CLAY_STRING("OPTIONS"), CLAY_TEXT_CONFIG({
|
||||||
|
.textColor = { 240, 236, 214, 255 },
|
||||||
|
.fontId = 0
|
||||||
|
}));
|
||||||
|
|
||||||
|
CLAY({
|
||||||
|
.id = CLAY_ID("options-music"),
|
||||||
|
.layout = { .padding = { 8, 8, 4, 4 } },
|
||||||
|
.backgroundColor = Clay_Hovered()
|
||||||
|
? (Clay_Color){ 64, 58, 88, 255 }
|
||||||
|
: (Clay_Color){ 0, 0, 0, 0 }
|
||||||
|
}) {
|
||||||
|
if ( Clay_Hovered() && clicked ) {
|
||||||
|
opt_music = !opt_music;
|
||||||
|
}
|
||||||
|
CLAY_TEXT(((Clay_String){ .length = (int32_t)strlen(musicrow), .chars = musicrow }),
|
||||||
|
CLAY_TEXT_CONFIG({ .textColor = { 240, 236, 214, 255 }, .fontId = 0 }));
|
||||||
|
}
|
||||||
|
|
||||||
|
CLAY({
|
||||||
|
.id = CLAY_ID("options-sound"),
|
||||||
|
.layout = { .padding = { 8, 8, 4, 4 } },
|
||||||
|
.backgroundColor = Clay_Hovered()
|
||||||
|
? (Clay_Color){ 64, 58, 88, 255 }
|
||||||
|
: (Clay_Color){ 0, 0, 0, 0 }
|
||||||
|
}) {
|
||||||
|
if ( Clay_Hovered() && clicked ) {
|
||||||
|
opt_sound = !opt_sound;
|
||||||
|
}
|
||||||
|
CLAY_TEXT(((Clay_String){ .length = (int32_t)strlen(soundrow), .chars = soundrow }),
|
||||||
|
CLAY_TEXT_CONFIG({ .textColor = { 240, 236, 214, 255 }, .fontId = 0 }));
|
||||||
|
}
|
||||||
|
|
||||||
|
CLAY({
|
||||||
|
.id = CLAY_ID("options-back"),
|
||||||
|
.layout = { .padding = { 8, 8, 4, 4 } },
|
||||||
|
.backgroundColor = Clay_Hovered()
|
||||||
|
? (Clay_Color){ 64, 58, 88, 255 }
|
||||||
|
: (Clay_Color){ 0, 0, 0, 0 }
|
||||||
|
}) {
|
||||||
|
if ( Clay_Hovered() && clicked ) {
|
||||||
|
state = UIDEMO_STATE_TITLE;
|
||||||
|
}
|
||||||
|
CLAY_TEXT(CLAY_STRING("Back (Esc)"), CLAY_TEXT_CONFIG({
|
||||||
|
.textColor = { 240, 236, 214, 255 },
|
||||||
|
.fontId = 0
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Declare the play screen's HUD: two labels, and the dialog while it is up.
|
||||||
|
*
|
||||||
|
* The score buffer is static for the same borrowed-until-frame_end reason as
|
||||||
|
* the options rows. Note what is *not* here: no visible flag, no draw call,
|
||||||
|
* no geometry. The dialog is open because this frame declares it.
|
||||||
|
*
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
*/
|
||||||
|
static akerr_ErrorContext *declare_play(void)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
static char scoretext[32];
|
||||||
|
static char livestext[32];
|
||||||
|
int count = 0;
|
||||||
|
|
||||||
|
PASS(errctx, aksl_snprintf(&count, scoretext, sizeof(scoretext), "SCORE %05d", score));
|
||||||
|
PASS(errctx, aksl_snprintf(&count, livestext, sizeof(livestext), "LIVES %d", lives));
|
||||||
|
PASS(errctx, akgl_ui_label("score", scoretext, AKGL_UI_ANCHOR_TOP_RIGHT, NULL));
|
||||||
|
PASS(errctx, akgl_ui_label("lives", livestext, AKGL_UI_ANCHOR_TOP_LEFT, NULL));
|
||||||
|
if ( dialog_open ) {
|
||||||
|
PASS(errctx, akgl_ui_dialog("dialog",
|
||||||
|
"This panel is one call. Space dismisses it; "
|
||||||
|
"compare examples/jrpg/textbox.c.",
|
||||||
|
NULL));
|
||||||
|
}
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @brief Read the render target back and write it out as a PNG, for the chapter figure. */
|
||||||
|
static akerr_ErrorContext *save_screenshot(char *path)
|
||||||
|
{
|
||||||
|
SDL_Surface *shot = NULL;
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
|
||||||
|
FAIL_ZERO_RETURN(errctx, path, AKERR_NULLPOINTER, "path");
|
||||||
|
|
||||||
|
shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL);
|
||||||
|
FAIL_ZERO_RETURN(errctx, shot, AKGL_ERR_SDL, "SDL_RenderReadPixels: %s", SDL_GetError());
|
||||||
|
|
||||||
|
ATTEMPT {
|
||||||
|
FAIL_ZERO_BREAK(errctx, IMG_SavePNG(shot, path), AKGL_ERR_SDL,
|
||||||
|
"IMG_SavePNG(%s): %s", path, SDL_GetError());
|
||||||
|
} CLEANUP {
|
||||||
|
SDL_DestroySurface(shot);
|
||||||
|
} PROCESS(errctx) {
|
||||||
|
} FINISH(errctx, true);
|
||||||
|
SDL_Log("Wrote %s", path);
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief One frame: events, the world stand-in, the UI bracket, present.
|
||||||
|
*
|
||||||
|
* The shape to compare with the JRPG's frame(): where that program calls
|
||||||
|
* akgl_game_update between frame_start and frame_end, this one paints a
|
||||||
|
* checkerboard -- and the UI bracket sits in the same overlay slot the
|
||||||
|
* hand-rolled textbox draw did.
|
||||||
|
*
|
||||||
|
* @param frameno The frame number, for the demo script.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
*/
|
||||||
|
static akerr_ErrorContext *frame(long frameno)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
SDL_Event event;
|
||||||
|
|
||||||
|
while ( SDL_PollEvent(&event) ) {
|
||||||
|
PASS(errctx, route_event(&event));
|
||||||
|
}
|
||||||
|
if ( demo ) {
|
||||||
|
PASS(errctx, demo_step(frameno));
|
||||||
|
}
|
||||||
|
|
||||||
|
PASS(errctx, akgl_renderer->frame_start(akgl_renderer));
|
||||||
|
if ( state == UIDEMO_STATE_PLAY ) {
|
||||||
|
// Where a game would call akgl_game_update. The score ticking is the
|
||||||
|
// stand-in for play, so the HUD label visibly earns its redraw.
|
||||||
|
PASS(errctx, akgl_draw_background(akgl_renderer, UIDEMO_WIDTH, UIDEMO_HEIGHT));
|
||||||
|
score += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
PASS(errctx, akgl_ui_frame_begin());
|
||||||
|
switch ( state ) {
|
||||||
|
case UIDEMO_STATE_TITLE:
|
||||||
|
PASS(errctx, declare_title());
|
||||||
|
break;
|
||||||
|
case UIDEMO_STATE_OPTIONS:
|
||||||
|
PASS(errctx, declare_options());
|
||||||
|
break;
|
||||||
|
case UIDEMO_STATE_PLAY:
|
||||||
|
PASS(errctx, declare_play());
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
PASS(errctx, akgl_ui_frame_end(akgl_renderer));
|
||||||
|
|
||||||
|
if ( (shotpath != NULL) && (frameno == shotframe) ) {
|
||||||
|
PASS(errctx, save_screenshot(shotpath));
|
||||||
|
}
|
||||||
|
PASS(errctx, akgl_renderer->frame_end(akgl_renderer));
|
||||||
|
|
||||||
|
// The press edge lives exactly one frame: the declarations above have
|
||||||
|
// seen it, so the next frame starts clean.
|
||||||
|
clicked = false;
|
||||||
|
|
||||||
|
// Menu activation is read after the frame, so it catches both routes in:
|
||||||
|
// Return through akgl_ui_menu_handle_event before the declarations, and a
|
||||||
|
// click through the declaration itself.
|
||||||
|
if ( title_menu.activated ) {
|
||||||
|
title_menu.activated = false;
|
||||||
|
switch ( title_menu.selected ) {
|
||||||
|
case 0:
|
||||||
|
state = UIDEMO_STATE_PLAY;
|
||||||
|
score = 0;
|
||||||
|
dialog_open = false;
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
state = UIDEMO_STATE_OPTIONS;
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
default:
|
||||||
|
running = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Give back what has to be given back, in the order that works.
|
||||||
|
*
|
||||||
|
* akgl_ui_shutdown first because it is the newest thing up, then the fonts
|
||||||
|
* **before** SDL_Quit -- the registry the fonts live in is an SDL property
|
||||||
|
* set, and SDL_Quit destroys it with them inside. The pools and the clay
|
||||||
|
* arena are static storage; there is nothing to free.
|
||||||
|
*/
|
||||||
|
static void teardown(void)
|
||||||
|
{
|
||||||
|
IGNORE(akgl_ui_shutdown());
|
||||||
|
IGNORE(akgl_text_unloadallfonts());
|
||||||
|
if ( akgl_window != NULL ) {
|
||||||
|
SDL_DestroyWindow(akgl_window);
|
||||||
|
akgl_window = NULL;
|
||||||
|
}
|
||||||
|
SDL_Quit();
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char *argv[])
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
long frameno = 0;
|
||||||
|
uint64_t started = 0;
|
||||||
|
uint64_t spent = 0;
|
||||||
|
|
||||||
|
ATTEMPT {
|
||||||
|
CATCH(errctx, parse_args(argc, argv));
|
||||||
|
CATCH(errctx, startup());
|
||||||
|
|
||||||
|
// The loop is the last thing in this ATTEMPT block on purpose: CATCH
|
||||||
|
// reports failure by `break`ing out of the loop, which falls straight
|
||||||
|
// into CLEANUP. See examples/jrpg/jrpg.c for the long form of this
|
||||||
|
// note.
|
||||||
|
while ( running ) {
|
||||||
|
started = SDL_GetTicks();
|
||||||
|
CATCH(errctx, frame(frameno));
|
||||||
|
frameno += 1;
|
||||||
|
if ( (frame_limit > 0) && (frameno >= frame_limit) ) {
|
||||||
|
running = false;
|
||||||
|
}
|
||||||
|
if ( demo == false ) {
|
||||||
|
spent = SDL_GetTicks() - started;
|
||||||
|
if ( spent < UIDEMO_FRAME_BUDGET_MS ) {
|
||||||
|
SDL_Delay((uint32_t)(UIDEMO_FRAME_BUDGET_MS - spent));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} CLEANUP {
|
||||||
|
teardown();
|
||||||
|
} PROCESS(errctx) {
|
||||||
|
} HANDLE_DEFAULT(errctx) {
|
||||||
|
LOG_ERROR_WITH_MESSAGE(errctx, "the UI demo could not finish");
|
||||||
|
exitstatus = 1;
|
||||||
|
} FINISH_NORETURN(errctx);
|
||||||
|
|
||||||
|
// Enough for the smoke run to be read rather than merely passed: the
|
||||||
|
// script quits from the title menu, so a run that finished its tour says
|
||||||
|
// so here.
|
||||||
|
printf("uidemo: %ld frames, score %d, music %s, sound %s\n",
|
||||||
|
frameno, score, opt_music ? "on" : "off", opt_sound ? "on" : "off");
|
||||||
|
return exitstatus;
|
||||||
|
}
|
||||||
58
examples/uidemo/uidemo.h
Normal file
58
examples/uidemo/uidemo.h
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
/**
|
||||||
|
* @file uidemo.h
|
||||||
|
* @brief Shared constants and types for the UI demo.
|
||||||
|
*
|
||||||
|
* There is deliberately little here: the demo is one translation unit, and
|
||||||
|
* this header exists so the chapter can quote the constants and the script
|
||||||
|
* type without quoting the code around them.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef _UIDEMO_H_
|
||||||
|
#define _UIDEMO_H_
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
/** @brief Window and layout width, in pixels. */
|
||||||
|
#define UIDEMO_SCREEN_WIDTH "640"
|
||||||
|
/** @brief Window and layout height, in pixels. */
|
||||||
|
#define UIDEMO_SCREEN_HEIGHT "480"
|
||||||
|
/** @brief The same width as a number, for akgl_ui_init. */
|
||||||
|
#define UIDEMO_WIDTH 640
|
||||||
|
/** @brief The same height as a number. */
|
||||||
|
#define UIDEMO_HEIGHT 480
|
||||||
|
|
||||||
|
/** @brief Registry name the demo's one font is loaded under. */
|
||||||
|
#define UIDEMO_FONT_NAME "uidemo"
|
||||||
|
/** @brief Point size the font is loaded at. The size is baked into the handle. */
|
||||||
|
#define UIDEMO_FONT_SIZE 16
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Which screen the demo is on.
|
||||||
|
*
|
||||||
|
* Three states, three ways of building an interface: the title screen is the
|
||||||
|
* menu *widget*, the options screen is raw `CLAY()` layout, and the play
|
||||||
|
* screen is the label and dialog widgets over a moving world stand-in.
|
||||||
|
*/
|
||||||
|
typedef enum {
|
||||||
|
UIDEMO_STATE_TITLE, /**< The akgl_UiMenu title menu. */
|
||||||
|
UIDEMO_STATE_OPTIONS, /**< The hand-written CLAY() options panel. */
|
||||||
|
UIDEMO_STATE_PLAY /**< HUD labels and a dialog over the "game". */
|
||||||
|
} uidemo_State;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief One scripted input in a `--demo` run.
|
||||||
|
*
|
||||||
|
* Key steps carry a keycode; mouse steps carry a position. Every step is
|
||||||
|
* synthesized as a real SDL_Event and pushed through the same routing the
|
||||||
|
* interactive loop uses, so the demo exercises the event chain rather than
|
||||||
|
* poking state behind its back.
|
||||||
|
*/
|
||||||
|
typedef struct {
|
||||||
|
long frame; /**< The frame this step fires on. */
|
||||||
|
uint32_t type; /**< SDL_EVENT_KEY_DOWN, _MOUSE_MOTION, _MOUSE_BUTTON_DOWN or _MOUSE_BUTTON_UP. */
|
||||||
|
SDL_Keycode key; /**< For key steps. */
|
||||||
|
float x; /**< For mouse steps. */
|
||||||
|
float y; /**< For mouse steps. */
|
||||||
|
} uidemo_ScriptStep;
|
||||||
|
|
||||||
|
#endif // _UIDEMO_H_
|
||||||
@@ -36,6 +36,25 @@
|
|||||||
*/
|
*/
|
||||||
#define AKGL_DRAW_MAX_FLOOD_SPANS 4096
|
#define AKGL_DRAW_MAX_FLOOD_SPANS 4096
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Triangles each corner of akgl_draw_filled_rounded_rect() is built from.
|
||||||
|
*
|
||||||
|
* A quarter circle approximated by this many segments is visually round at any
|
||||||
|
* radius a UI panel plausibly uses; the vertex arrays are fixed at this size,
|
||||||
|
* so it is a compile-time cost, not a per-call one.
|
||||||
|
*/
|
||||||
|
#define AKGL_DRAW_ROUNDED_RECT_SEGMENTS 16
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Most vertices akgl_draw_arc() will spend on one arc.
|
||||||
|
*
|
||||||
|
* An arc is a triangle strip between its inner and outer edge, two vertices
|
||||||
|
* per step, so half of these are positions along the curve. The step count
|
||||||
|
* scales with the swept angle and is clamped to fit this array -- a full
|
||||||
|
* circle gets all of them, a sliver gets a few -- rather than allocating.
|
||||||
|
*/
|
||||||
|
#define AKGL_DRAW_ARC_MAX_POINTS 64
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Paint an 8x8 grey checkerboard over a region, the way an image editor shows transparency.
|
* @brief Paint an 8x8 grey checkerboard over a region, the way an image editor shows transparency.
|
||||||
*
|
*
|
||||||
@@ -242,4 +261,86 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_copy_region(akgl_RenderBackend *sel
|
|||||||
*/
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_paste_region(akgl_RenderBackend *self, SDL_Surface *src, float32_t x, float32_t y);
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_paste_region(akgl_RenderBackend *self, SDL_Surface *src, float32_t x, float32_t y);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Fill a rectangle whose corners are rounded off with quarter circles.
|
||||||
|
*
|
||||||
|
* The body is three axis-aligned fills and the corners are triangle fans
|
||||||
|
* through `SDL_RenderGeometry` -- #AKGL_DRAW_ROUNDED_RECT_SEGMENTS triangles
|
||||||
|
* each -- so nothing here is anti-aliased, matching every other primitive in
|
||||||
|
* this header.
|
||||||
|
*
|
||||||
|
* @param self The backend to draw through. Required, along with its
|
||||||
|
* `sdl_renderer`.
|
||||||
|
* @param rect The rectangle, in render-target pixels. Required. A zero or
|
||||||
|
* negative width or height fills nothing and is not reported.
|
||||||
|
* @param radius Corner radius in pixels. Zero or negative falls through to
|
||||||
|
* akgl_draw_filled_rect() -- a square corner is not an error.
|
||||||
|
* Larger than half the shorter side is clamped to it, which at
|
||||||
|
* the limit turns a square into a circle rather than folding the
|
||||||
|
* corners over each other.
|
||||||
|
* @param color Colour to fill with, including alpha.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER If @p self, `self->sdl_renderer`, or @p rect is
|
||||||
|
* `NULL`.
|
||||||
|
* @throws AKGL_ERR_SDL If the draw colour cannot be read or set, or if a band
|
||||||
|
* or corner cannot be drawn. A failure partway leaves the shape
|
||||||
|
* partially drawn.
|
||||||
|
*/
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_filled_rounded_rect(akgl_RenderBackend *self, SDL_FRect *rect, float32_t radius, SDL_Color color);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Stroke a circular arc of a given thickness between two angles.
|
||||||
|
*
|
||||||
|
* Angles are in degrees, 0 pointing along +x (three o'clock) and increasing
|
||||||
|
* clockwise on screen -- the same convention as the rotation argument the
|
||||||
|
* render backend's `draw_texture` takes. The stroke runs from the arc's outer
|
||||||
|
* edge at @p radius inward by @p thickness, drawn as one triangle strip, not
|
||||||
|
* anti-aliased.
|
||||||
|
*
|
||||||
|
* @param self The backend to draw through. Required, along with its
|
||||||
|
* `sdl_renderer`.
|
||||||
|
* @param x Horizontal position of the arc's centre.
|
||||||
|
* @param y Vertical position of the arc's centre.
|
||||||
|
* @param radius Outer radius in pixels. Must be positive.
|
||||||
|
* @param start_deg Angle the arc starts at.
|
||||||
|
* @param end_deg Angle the arc ends at. At or below @p start_deg nothing is
|
||||||
|
* drawn and nothing is reported, matching the zero-area
|
||||||
|
* rectangle fills above. Neither angle is normalized, so an
|
||||||
|
* arc crossing three o'clock is 350 to 370, not 350 to 10; a
|
||||||
|
* sweep beyond 360 degrees is clamped to one full turn.
|
||||||
|
* @param thickness Stroke width in pixels, measured inward from @p radius.
|
||||||
|
* Must be positive; at or above @p radius the arc becomes a
|
||||||
|
* filled wedge to the centre.
|
||||||
|
* @param color Colour to stroke in, including alpha.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER If @p self or `self->sdl_renderer` is `NULL`.
|
||||||
|
* @throws AKERR_OUTOFBOUNDS If @p radius or @p thickness is not positive. The
|
||||||
|
* message reports the offending value.
|
||||||
|
* @throws AKGL_ERR_SDL If the strip cannot be drawn.
|
||||||
|
*/
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_arc(akgl_RenderBackend *self, float32_t x, float32_t y, float32_t radius, float32_t start_deg, float32_t end_deg, float32_t thickness, SDL_Color color);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Restrict every subsequent draw to a rectangle of the render target.
|
||||||
|
*
|
||||||
|
* Everything drawn through @p self after this -- primitives here, textures,
|
||||||
|
* text -- is clipped to @p rect until the restriction is cleared. This is the
|
||||||
|
* only entry point in this header where a `NULL` rectangle is legal rather
|
||||||
|
* than an error: it means "clear the restriction", matching what
|
||||||
|
* `SDL_SetRenderClipRect` does with it, and there is no other value that
|
||||||
|
* could.
|
||||||
|
*
|
||||||
|
* The clip is renderer state, not saved and restored around anything: a
|
||||||
|
* caller that sets it owns putting it back, the way a `CLEANUP` block puts
|
||||||
|
* back a draw colour. Leaving it set clips the next frame's world too.
|
||||||
|
*
|
||||||
|
* @param self The backend to clip. Required, along with its `sdl_renderer`.
|
||||||
|
* @param rect Rectangle to clip to, in render-target pixels -- integer, since
|
||||||
|
* a clip boundary is a pixel boundary. `NULL` clears the clip.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER If @p self or `self->sdl_renderer` is `NULL`.
|
||||||
|
* @throws AKGL_ERR_SDL If the clip cannot be set or cleared.
|
||||||
|
*/
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_draw_set_clip(akgl_RenderBackend *self, SDL_Rect *rect);
|
||||||
|
|
||||||
#endif //_AKGL_DRAW_H_
|
#endif //_AKGL_DRAW_H_
|
||||||
|
|||||||
@@ -76,11 +76,12 @@
|
|||||||
#define AKGL_ERR_BEHAVIOR (AKGL_ERR_BASE + 3) /**< A component did not behave the way its contract requires */
|
#define AKGL_ERR_BEHAVIOR (AKGL_ERR_BASE + 3) /**< A component did not behave the way its contract requires */
|
||||||
#define AKGL_ERR_LOGICINTERRUPT (AKGL_ERR_BASE + 4) /**< Actor logic is telling the physics simulator to skip it */
|
#define AKGL_ERR_LOGICINTERRUPT (AKGL_ERR_BASE + 4) /**< Actor logic is telling the physics simulator to skip it */
|
||||||
#define AKGL_ERR_COLLISION (AKGL_ERR_BASE + 5) /**< A collision query could not be answered; the message says why */
|
#define AKGL_ERR_COLLISION (AKGL_ERR_BASE + 5) /**< A collision query could not be answered; the message says why */
|
||||||
|
#define AKGL_ERR_UI (AKGL_ERR_BASE + 6) /**< The UI subsystem refused, or clay reported a layout error; the message says which */
|
||||||
|
|
||||||
// One past the last libakgl status. The reservation is all-or-nothing -- a
|
// One past the last libakgl status. The reservation is all-or-nothing -- a
|
||||||
// subset or superset of an existing one is refused -- so this must stay one
|
// subset or superset of an existing one is refused -- so this must stay one
|
||||||
// past the highest code above.
|
// past the highest code above.
|
||||||
#define AKGL_ERR_LIMIT (AKGL_ERR_BASE + 6)
|
#define AKGL_ERR_LIMIT (AKGL_ERR_BASE + 7)
|
||||||
#define AKGL_ERR_COUNT (AKGL_ERR_LIMIT - AKGL_ERR_BASE)
|
#define AKGL_ERR_COUNT (AKGL_ERR_LIMIT - AKGL_ERR_BASE)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
552
include/akgl/ui.h
Normal file
552
include/akgl/ui.h
Normal file
@@ -0,0 +1,552 @@
|
|||||||
|
/**
|
||||||
|
* @file ui.h
|
||||||
|
* @brief Screen-space user interface layouts, built on the clay layout engine.
|
||||||
|
*
|
||||||
|
* This subsystem exists because every game eventually wants a dialog box, a
|
||||||
|
* HUD counter, or a menu, and hand-rolling them out of rectangles and
|
||||||
|
* akgl_text_rendertextat() is tedious the first time and unmaintainable by the
|
||||||
|
* fourth (`examples/jrpg/textbox.c` is the honest record of the first time).
|
||||||
|
* clay computes the layout; libakgl owns everything that has to touch SDL or
|
||||||
|
* the rest of the library -- the arena clay allocates from, the text
|
||||||
|
* measurement it calls back into, the render commands it emits, and the
|
||||||
|
* pointer state it is fed.
|
||||||
|
*
|
||||||
|
* There are two ways in, and they compose:
|
||||||
|
*
|
||||||
|
* - Write clay's declarative `CLAY({...}) { ... }` blocks yourself between
|
||||||
|
* akgl_ui_frame_begin() and akgl_ui_frame_end(). The whole of clay's API
|
||||||
|
* is available -- this header includes `<clay.h>` on purpose, the same way
|
||||||
|
* SDL types appear undisguised elsewhere in this library.
|
||||||
|
* - Call the widget helpers, which emit those blocks for you.
|
||||||
|
*
|
||||||
|
* The subsystem is optional the way collision is optional: always compiled in,
|
||||||
|
* inert until akgl_ui_init() runs, and costing nothing but static storage
|
||||||
|
* before that.
|
||||||
|
*
|
||||||
|
* @warning libakgl compiles clay into `libakgl.so` (the `CLAY_IMPLEMENTATION`
|
||||||
|
* translation unit is `src/ui_clay.c`) and exports its symbols,
|
||||||
|
* because the `CLAY()` macros in *your* translation units expand to
|
||||||
|
* calls into them. Do not define `CLAY_IMPLEMENTATION` yourself and
|
||||||
|
* do not link another copy of clay: two definitions of the same
|
||||||
|
* symbols and the loader picks one silently, which is the exact
|
||||||
|
* defect AGENTS.md records against an exported `renderer`.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef _AKGL_UI_H_
|
||||||
|
#define _AKGL_UI_H_
|
||||||
|
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
#include <akerror.h>
|
||||||
|
#include <clay.h>
|
||||||
|
|
||||||
|
#include <akgl/renderer.h>
|
||||||
|
#include <akgl/types.h>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Most elements one layout may declare.
|
||||||
|
*
|
||||||
|
* Passed to `Clay_SetMaxElementCount` before the arena is sized, so it is a
|
||||||
|
* compile-time ceiling in the same sense as the `AKGL_MAX_HEAP_*` family:
|
||||||
|
* define it before including this header to raise it, and see
|
||||||
|
* #AKGL_UI_ARENA_BYTES, which must grow with it.
|
||||||
|
*/
|
||||||
|
#ifndef AKGL_UI_MAX_ELEMENTS
|
||||||
|
#define AKGL_UI_MAX_ELEMENTS 1024
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Most whitespace-separated words clay's text measurement cache holds.
|
||||||
|
*
|
||||||
|
* Clay measures text one word at a time and caches the result; a cache miss
|
||||||
|
* costs one call back into SDL_ttf. This bounds the cache, not the text -- a
|
||||||
|
* layout with more distinct words than this still renders, it just measures
|
||||||
|
* some of them every frame and clay reports the overflow through the error
|
||||||
|
* handler.
|
||||||
|
*/
|
||||||
|
#ifndef AKGL_UI_MAX_MEASURE_WORDS
|
||||||
|
#define AKGL_UI_MAX_MEASURE_WORDS 4096
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Bytes of static storage clay lays its internal structures out in.
|
||||||
|
*
|
||||||
|
* The arena is a fixed array in `src/ui.c` -- libakgl does not call `malloc`
|
||||||
|
* at runtime, and clay never allocates behind the arena's back. What clay
|
||||||
|
* needs is a function of #AKGL_UI_MAX_ELEMENTS and #AKGL_UI_MAX_MEASURE_WORDS
|
||||||
|
* and is only computable at runtime, so akgl_ui_init() checks
|
||||||
|
* `Clay_MinMemorySize()` against this and refuses with both numbers in the
|
||||||
|
* message when it does not fit. A too-small arena is a loud failure at
|
||||||
|
* startup, never a corruption at frame forty thousand.
|
||||||
|
*/
|
||||||
|
#ifndef AKGL_UI_ARENA_BYTES
|
||||||
|
#define AKGL_UI_ARENA_BYTES (1024 * 1024)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Most fonts akgl_ui_font_register() will map to clay fontIds.
|
||||||
|
*
|
||||||
|
* clay identifies a font as a `uint16_t`; libakgl identifies one as a name in
|
||||||
|
* #AKGL_REGISTRY_FONT. The table joining them is this many fixed slots. A
|
||||||
|
* size is baked into each registered font handle, so a game using one face at
|
||||||
|
* three sizes is using three slots.
|
||||||
|
*/
|
||||||
|
#ifndef AKGL_UI_MAX_FONTS
|
||||||
|
#define AKGL_UI_MAX_FONTS 8
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Bytes each fontId table slot holds for its registry name, terminator included.
|
||||||
|
*/
|
||||||
|
#ifndef AKGL_UI_FONT_NAME_LENGTH
|
||||||
|
#define AKGL_UI_FONT_NAME_LENGTH 64
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Bytes of scratch one text render command may occupy, terminator included.
|
||||||
|
*
|
||||||
|
* clay hands text to a renderer as a length-and-pointer slice, one command
|
||||||
|
* per wrapped line; akgl_text_rendertextat() takes a C string, so each line
|
||||||
|
* is copied through a bounded buffer on the way. A line longer than this
|
||||||
|
* fails the frame loudly rather than drawing a truncation that reads as the
|
||||||
|
* author's text.
|
||||||
|
*/
|
||||||
|
#ifndef AKGL_UI_MAX_TEXT_BYTES
|
||||||
|
#define AKGL_UI_MAX_TEXT_BYTES 1024
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Most entries one akgl_UiMenu may carry.
|
||||||
|
*
|
||||||
|
* A menu deeper than this wants scrolling and sub-screens, which is `CLAY()`
|
||||||
|
* territory rather than a widget's.
|
||||||
|
*/
|
||||||
|
#ifndef AKGL_UI_MENU_MAX_ITEMS
|
||||||
|
#define AKGL_UI_MENU_MAX_ITEMS 16
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Height of the akgl_ui_dialog() panel, in pixels.
|
||||||
|
*
|
||||||
|
* Two lines of 12pt monospace plus padding -- the JRPG textbox's height, kept
|
||||||
|
* so the widget's output is comparable with the hand-rolled panel it
|
||||||
|
* replaces.
|
||||||
|
*/
|
||||||
|
#ifndef AKGL_UI_DIALOG_HEIGHT
|
||||||
|
#define AKGL_UI_DIALOG_HEIGHT 56
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief One look, shared by every widget: colours, spacing, corners, font.
|
||||||
|
*
|
||||||
|
* Passing `NULL` wherever a style is taken means the library default, which
|
||||||
|
* is deliberately the JRPG textbox's palette -- near-black fill (24,20,37,235)
|
||||||
|
* with parchment edge and ink (240,236,214,255), 8 pixels of padding, square
|
||||||
|
* corners, fontId 0. A caller wanting one change copies those values and
|
||||||
|
* changes one; there is no per-field "zero means default" magic, because a
|
||||||
|
* transparent fill and a zero radius are things a style legitimately says.
|
||||||
|
*/
|
||||||
|
typedef struct {
|
||||||
|
SDL_Color fill; /**< Panel background. */
|
||||||
|
SDL_Color edge; /**< Border colour. Drawn one pixel wide where a widget has a border. */
|
||||||
|
SDL_Color ink; /**< Text colour. */
|
||||||
|
float32_t padding; /**< Pixels between a panel's edge and its content, and between a widget and the screen edge it anchors to. */
|
||||||
|
float32_t corner_radius; /**< Corner rounding in pixels. 0 is square. */
|
||||||
|
uint16_t fontid; /**< The clay fontId text renders with, from akgl_ui_font_register(). */
|
||||||
|
} akgl_UiStyle;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Where on the screen akgl_ui_label() pins itself.
|
||||||
|
*/
|
||||||
|
typedef enum {
|
||||||
|
AKGL_UI_ANCHOR_TOP_LEFT, /**< Inset by the style's padding from the top-left corner. */
|
||||||
|
AKGL_UI_ANCHOR_TOP_RIGHT, /**< Inset from the top-right corner. */
|
||||||
|
AKGL_UI_ANCHOR_BOTTOM_LEFT, /**< Inset from the bottom-left corner. */
|
||||||
|
AKGL_UI_ANCHOR_BOTTOM_RIGHT, /**< Inset from the bottom-right corner. */
|
||||||
|
AKGL_UI_ANCHOR_CENTER /**< Dead centre, no inset. */
|
||||||
|
} akgl_UiAnchor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief One vertical menu: its entries, its selection, and whether it fired.
|
||||||
|
*
|
||||||
|
* Caller-owned, the way the JRPG's textbox state is a static in the game --
|
||||||
|
* there is no heap layer behind this because it owns no texture, no font and
|
||||||
|
* no registry entry; it is a struct of borrowed pointers and two integers.
|
||||||
|
* Declare it static, fill in `id`, `items` and `count`, and leave the rest
|
||||||
|
* zeroed.
|
||||||
|
*
|
||||||
|
* `selected` is yours to read and the menu's to move: keyboard and gamepad
|
||||||
|
* move it through akgl_ui_menu_handle_event(), and the mouse moves it by
|
||||||
|
* hovering while akgl_ui_menu() declares the rows. `activated` latches true
|
||||||
|
* on Return, gamepad South, or a click on a row; the caller acts on it and
|
||||||
|
* sets it back to false -- the menu never clears it, so an unread activation
|
||||||
|
* is not lost between frames.
|
||||||
|
*/
|
||||||
|
typedef struct {
|
||||||
|
char *id; /**< Element id prefix, unique per menu. Required. Borrowed. */
|
||||||
|
char *items[AKGL_UI_MENU_MAX_ITEMS]; /**< The entries, top to bottom. Borrowed; they must outlive the frame. */
|
||||||
|
int32_t count; /**< How many of @c items are set. 1 to #AKGL_UI_MENU_MAX_ITEMS. */
|
||||||
|
int32_t selected; /**< Index of the highlighted entry. In and out. */
|
||||||
|
bool activated; /**< Out: the selected entry was confirmed. Caller clears. */
|
||||||
|
akgl_UiStyle *style; /**< Look to draw with, or `NULL` for the library default. */
|
||||||
|
} akgl_UiMenu;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Bring the UI subsystem up.
|
||||||
|
*
|
||||||
|
* Bounds clay to the `AKGL_UI_*` ceilings, checks that the arena is big
|
||||||
|
* enough for them, and hands it to `Clay_Initialize` with an error handler
|
||||||
|
* that routes clay's layout-time complaints into the libakgl error protocol
|
||||||
|
* (they surface from akgl_ui_frame_end(), which is the first call after they
|
||||||
|
* can happen).
|
||||||
|
*
|
||||||
|
* The layout dimensions are taken as parameters rather than read from the
|
||||||
|
* camera or the window, so a headless program -- a test, a layout tool -- can
|
||||||
|
* bring the UI up without a renderer existing at all. A windowed game passes
|
||||||
|
* its screen size and lets akgl_ui_handle_event() track resizes from there.
|
||||||
|
*
|
||||||
|
* @param width Layout width in pixels. Must be positive.
|
||||||
|
* @param height Layout height in pixels. Must be positive.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_OUTOFBOUNDS If @p width or @p height is not positive.
|
||||||
|
* @throws AKGL_ERR_UI If the subsystem is already initialized -- call
|
||||||
|
* akgl_ui_shutdown() first; if the arena cannot hold clay's
|
||||||
|
* structures at the current ceilings -- the message carries the bytes
|
||||||
|
* required and the bytes available, so the operator knows what to
|
||||||
|
* raise #AKGL_UI_ARENA_BYTES to; or if `Clay_Initialize` refuses,
|
||||||
|
* which the preceding checks should make unreachable.
|
||||||
|
*/
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_ui_init(int width, int height);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Return the UI subsystem to the inert state.
|
||||||
|
*
|
||||||
|
* clay has no teardown of its own -- everything it built lives in the arena,
|
||||||
|
* and the next akgl_ui_init() lays a new context out over it. This forgets
|
||||||
|
* the context and clears the stashed error state, and is idempotent, because
|
||||||
|
* shutdown paths run after partial startups.
|
||||||
|
*
|
||||||
|
* @warning Anything that captured clay state -- and any `CLAY()` block that
|
||||||
|
* runs after this -- is left talking to a context this subsystem has
|
||||||
|
* disowned. Stop declaring layouts before shutting down, the same
|
||||||
|
* way fonts stop being drawn before akgl_text_unloadallfonts().
|
||||||
|
*
|
||||||
|
* @return `NULL`. Shutting down an uninitialized subsystem is success.
|
||||||
|
*/
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_ui_shutdown(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Tell the layout engine the render target changed size.
|
||||||
|
*
|
||||||
|
* akgl_ui_handle_event() calls this for `SDL_EVENT_WINDOW_RESIZED`, so a game
|
||||||
|
* that routes its events through there never calls it directly. It is public
|
||||||
|
* for the host that owns its own event loop.
|
||||||
|
*
|
||||||
|
* @param width New layout width in pixels. Must be positive.
|
||||||
|
* @param height New layout height in pixels. Must be positive.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKGL_ERR_UI If the subsystem is not initialized.
|
||||||
|
* @throws AKERR_OUTOFBOUNDS If @p width or @p height is not positive.
|
||||||
|
*/
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_ui_resize(int width, int height);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Map a font-registry name to the clay fontId that text elements name it by.
|
||||||
|
*
|
||||||
|
* The table stores the *name* and resolves it through #AKGL_REGISTRY_FONT on
|
||||||
|
* every use, rather than caching the `TTF_Font *` -- fonts are not reference
|
||||||
|
* counted, so a cached handle would dangle the moment akgl_text_unloadfont()
|
||||||
|
* took the font away, while a name honestly reports "no longer registered".
|
||||||
|
* Registering a name that is already in the table returns its existing id
|
||||||
|
* rather than spending a second slot.
|
||||||
|
*
|
||||||
|
* clay's `Clay_TextElementConfig.fontSize` is **ignored** by this subsystem:
|
||||||
|
* a libakgl font bakes its size in at akgl_text_loadfont() time, so the size
|
||||||
|
* a text element renders at is the size the font behind its fontId was loaded
|
||||||
|
* at. One face at two sizes is two loads, two registrations, two ids.
|
||||||
|
*
|
||||||
|
* @param name Registry key the font was published under. Required, and it
|
||||||
|
* must already be in the registry -- registering first and
|
||||||
|
* loading later would leave a window where a fontId resolves to
|
||||||
|
* nothing.
|
||||||
|
* @param fontid Receives the id to put in `Clay_TextElementConfig.fontId`.
|
||||||
|
* Required.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER If @p name or @p fontid is `NULL`.
|
||||||
|
* @throws AKGL_ERR_UI If the subsystem is not initialized; if no font is
|
||||||
|
* registered under @p name; or if all #AKGL_UI_MAX_FONTS slots are
|
||||||
|
* taken. Each message says which.
|
||||||
|
* @throws AKERR_OUTOFBOUNDS If @p name does not fit in a table slot --
|
||||||
|
* #AKGL_UI_FONT_NAME_LENGTH bytes including the terminator.
|
||||||
|
*/
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_ui_font_register(char *name, uint16_t *fontid);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Feed one SDL event to the UI, and learn whether the UI claimed it.
|
||||||
|
*
|
||||||
|
* The mouse half of the input story -- keyboard and gamepad stay with the
|
||||||
|
* control maps and the widget helpers, because keyboard focus is something
|
||||||
|
* the application declares, not something a pointer position implies. This
|
||||||
|
* handles mouse motion, left-button presses and releases, the wheel, and
|
||||||
|
* window resizes; every other event, and every event while the subsystem is
|
||||||
|
* not initialized, reports @p consumed false and succeeds, so a host passes
|
||||||
|
* every event through unconditionally -- the same contract
|
||||||
|
* akgl_controller_handle_event() has.
|
||||||
|
*
|
||||||
|
* Call it *before* akgl_controller_handle_event() in the event loop, and skip
|
||||||
|
* the rest of the chain when it consumed:
|
||||||
|
*
|
||||||
|
* while ( SDL_PollEvent(&event) ) {
|
||||||
|
* PASS(errctx, akgl_ui_handle_event(&app, &event, &consumed));
|
||||||
|
* if ( consumed ) { continue; }
|
||||||
|
* PASS(errctx, akgl_controller_handle_event(&app, &event));
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* A press or release is consumed when the pointer is over any UI element;
|
||||||
|
* motion and resizes never are (the game may care where the mouse is, and
|
||||||
|
* certainly cares about its window). The hit test runs against the layout
|
||||||
|
* the *previous* frame declared, because this frame's does not exist while
|
||||||
|
* events are being polled -- clay retains the last tree for exactly this
|
||||||
|
* purpose, and one frame of staleness is the accepted cost of the standard
|
||||||
|
* model. Before any frame has been laid out, nothing is over anything and
|
||||||
|
* nothing is consumed.
|
||||||
|
*
|
||||||
|
* @param appstate The application state pointer the event loop owns.
|
||||||
|
* Required, though this implementation does not read it --
|
||||||
|
* the parameter exists so the signature matches
|
||||||
|
* akgl_controller_handle_event() and can grow the same way.
|
||||||
|
* @param event The event SDL handed the loop. Required.
|
||||||
|
* @param consumed Receives whether the UI claimed the event. Required.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER If @p appstate, @p event, or @p consumed is
|
||||||
|
* `NULL`.
|
||||||
|
* @throws AKERR_OUTOFBOUNDS If a resize event carries a non-positive size,
|
||||||
|
* from akgl_ui_resize().
|
||||||
|
*/
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_ui_handle_event(void *appstate, SDL_Event *event, bool *consumed);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Open the UI frame: clear the error stash and begin the clay layout.
|
||||||
|
*
|
||||||
|
* Call once per rendered frame, after akgl_game_update() and before any
|
||||||
|
* `CLAY()` block or widget helper. Everything declared between this and
|
||||||
|
* akgl_ui_frame_end() is this frame's interface; a dialog is "open" because
|
||||||
|
* the frame declares it, not because a mode was toggled somewhere.
|
||||||
|
*
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKGL_ERR_UI If the subsystem is not initialized, or if a frame is
|
||||||
|
* already open -- a begin/begin sequence means a frame_end went
|
||||||
|
* missing, and absorbing it would hide the layout of one frame inside
|
||||||
|
* another.
|
||||||
|
*/
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_ui_frame_begin(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Close the UI frame: compute the layout and draw it.
|
||||||
|
*
|
||||||
|
* Runs `Clay_EndLayout` and walks the render commands it produces through
|
||||||
|
* @p self -- fills and rounded fills, borders, clip rectangles, text through
|
||||||
|
* akgl_text_rendertextat(), images through the backend's `draw_texture`.
|
||||||
|
* Draws in screen coordinates on whatever is already on the target, so call
|
||||||
|
* it between akgl_game_update() and the backend's `frame_end`: the UI lands
|
||||||
|
* on top of the world.
|
||||||
|
*
|
||||||
|
* The frame is considered closed even when this fails: the next
|
||||||
|
* akgl_ui_frame_begin() is legal, so one bad frame is one bad frame rather
|
||||||
|
* than a wedged subsystem.
|
||||||
|
*
|
||||||
|
* If clay reported errors during layout -- through its handler, or the
|
||||||
|
* measure callback failing -- this raises the first of them here and does not
|
||||||
|
* draw, because a layout that failed is not a layout, and the raise names how
|
||||||
|
* many more followed it.
|
||||||
|
*
|
||||||
|
* @param self The backend to draw through. Required, along with its
|
||||||
|
* `sdl_renderer` and `draw_texture`.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER If @p self, its `sdl_renderer`, or its
|
||||||
|
* `draw_texture` is `NULL`.
|
||||||
|
* @throws AKGL_ERR_UI If the subsystem is not initialized; if no frame is
|
||||||
|
* open; if clay reported layout errors; if a text command's line
|
||||||
|
* exceeds #AKGL_UI_MAX_TEXT_BYTES; or if a fontId cannot be resolved.
|
||||||
|
* @throws AKERR_* Whatever the drawing primitives underneath raise.
|
||||||
|
*/
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_ui_frame_end(akgl_RenderBackend *self);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Declare a dialog panel: the JRPG textbox in one call.
|
||||||
|
*
|
||||||
|
* A panel across the bottom of the screen, inset by the style's padding,
|
||||||
|
* #AKGL_UI_DIALOG_HEIGHT pixels tall, with a one-pixel border and the text
|
||||||
|
* wrapped inside it. Legal only between akgl_ui_frame_begin() and
|
||||||
|
* akgl_ui_frame_end(). Showing and hiding is the frame's business: call this
|
||||||
|
* while somebody is talking and don't while nobody is -- there is no
|
||||||
|
* `visible` flag, because a declarative frame *is* the flag.
|
||||||
|
*
|
||||||
|
* @param id Element id for the panel, unique within the frame. Required.
|
||||||
|
* Borrowed until frame_end.
|
||||||
|
* @param text What the dialog says. Required, and borrowed until frame_end
|
||||||
|
* -- clay keeps the pointer, not a copy. May be empty, which
|
||||||
|
* draws an empty panel.
|
||||||
|
* @param style Look to draw with, or `NULL` for the textbox palette.
|
||||||
|
*
|
||||||
|
* @note The panel's height is fixed at #AKGL_UI_DIALOG_HEIGHT and the text is
|
||||||
|
* not clipped to it: a message that wraps to more lines than fit runs
|
||||||
|
* past the bottom border, visibly. That is the same behaviour as the
|
||||||
|
* hand-rolled panel this widget mirrors, and it is deliberate --
|
||||||
|
* overflow you can see beats truncation you cannot. Size the height for
|
||||||
|
* your longest message, or raise the override.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER If @p id or @p text is `NULL`.
|
||||||
|
* @throws AKGL_ERR_UI If the subsystem is not initialized or no frame is
|
||||||
|
* open.
|
||||||
|
*/
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_ui_dialog(char *id, char *text, akgl_UiStyle *style);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Declare a HUD label: one line of text on a padded panel, pinned to a corner.
|
||||||
|
*
|
||||||
|
* Fit-sized around its text, anchored per @p anchor and inset by the style's
|
||||||
|
* padding. The score counter and the lives counter are two calls to this with
|
||||||
|
* two ids. Legal only between akgl_ui_frame_begin() and akgl_ui_frame_end().
|
||||||
|
*
|
||||||
|
* The text is borrowed until frame_end, which matters for the obvious use:
|
||||||
|
* format the score into a buffer that outlives the frame bracket, not into a
|
||||||
|
* scope that closes before the draw.
|
||||||
|
*
|
||||||
|
* @param id Element id for the label, unique within the frame. Required.
|
||||||
|
* @param text What the label says. Required; empty draws an empty chip.
|
||||||
|
* @param anchor Which corner (or the centre) to pin to.
|
||||||
|
* @param style Look to draw with, or `NULL` for the library default.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER If @p id or @p text is `NULL`.
|
||||||
|
* @throws AKGL_ERR_UI If the subsystem is not initialized or no frame is
|
||||||
|
* open.
|
||||||
|
* @throws AKERR_OUTOFBOUNDS If @p anchor is not one of the enum's values.
|
||||||
|
*/
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_ui_label(char *id, char *text, akgl_UiAnchor anchor, akgl_UiStyle *style);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Declare a vertical menu from caller-owned state, centred on screen.
|
||||||
|
*
|
||||||
|
* One row per entry, the selected row drawn inverted (ink on fill swaps to
|
||||||
|
* fill on ink). Moving the mouse onto a row selects it; a left click on a
|
||||||
|
* row selects and sets `activated`. A *stationary* pointer claims nothing --
|
||||||
|
* parking the mouse over the menu must not pin the selection against the
|
||||||
|
* keyboard, which is fighting sixty times a second and losing. Keyboard and
|
||||||
|
* gamepad go through
|
||||||
|
* akgl_ui_menu_handle_event(), which the application calls for whichever
|
||||||
|
* menu currently has its attention -- that routing *is* the focus model.
|
||||||
|
* Legal only between akgl_ui_frame_begin() and akgl_ui_frame_end().
|
||||||
|
*
|
||||||
|
* @param menu The menu state. Required, with a non-`NULL` `id`, a `count` in
|
||||||
|
* range, every used entry non-`NULL`, and `selected` pointing at
|
||||||
|
* an entry.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER If @p menu, its `id`, or any of its first
|
||||||
|
* `count` items is `NULL`. The message names the index.
|
||||||
|
* @throws AKGL_ERR_UI If the subsystem is not initialized or no frame is
|
||||||
|
* open.
|
||||||
|
* @throws AKERR_OUTOFBOUNDS If `count` or `selected` is out of range.
|
||||||
|
*/
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_ui_menu(akgl_UiMenu *menu);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Drive a menu from a keyboard or gamepad event.
|
||||||
|
*
|
||||||
|
* Up and Down arrows and the D-pad move the selection, wrapping at both
|
||||||
|
* ends; Return and the gamepad South button set `activated`. Anything else
|
||||||
|
* -- and anything while the subsystem is inert -- reports @p consumed false
|
||||||
|
* and succeeds, so it slots into the same pass-everything event chain as
|
||||||
|
* akgl_ui_handle_event(), after it and before the control maps:
|
||||||
|
*
|
||||||
|
* PASS(errctx, akgl_ui_handle_event(&app, &event, &consumed));
|
||||||
|
* if ( consumed ) { continue; }
|
||||||
|
* if ( menu_active ) {
|
||||||
|
* PASS(errctx, akgl_ui_menu_handle_event(&menu, &event, &consumed));
|
||||||
|
* if ( consumed ) { continue; }
|
||||||
|
* }
|
||||||
|
* PASS(errctx, akgl_controller_handle_event(&app, &event));
|
||||||
|
*
|
||||||
|
* @param menu The menu the application is routing input to. Required,
|
||||||
|
* with `count` in range.
|
||||||
|
* @param event The event SDL handed the loop. Required.
|
||||||
|
* @param consumed Receives whether the menu claimed the event. Required.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER If @p menu, @p event, or @p consumed is `NULL`.
|
||||||
|
* @throws AKERR_OUTOFBOUNDS If `count` or `selected` is out of range.
|
||||||
|
*/
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_ui_menu_handle_event(akgl_UiMenu *menu, SDL_Event *event, bool *consumed);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The following is part of the internal API. It is exposed so the test suite
|
||||||
|
* can reach it and is not meant to be called by a game.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The measure callback akgl_ui_init() hands to clay.
|
||||||
|
*
|
||||||
|
* clay's callback signature returns dimensions by value and has no error
|
||||||
|
* channel, so this cannot raise: on any failure -- an unregistered fontId, a
|
||||||
|
* font gone from the registry, SDL_ttf refusing the string -- it reports zero
|
||||||
|
* by zero and stashes the failure where akgl_ui_frame_end() will raise it.
|
||||||
|
* The slice is measured at its stated length through SDL_ttf directly, so it
|
||||||
|
* is never copied and need not be NUL-terminated.
|
||||||
|
*
|
||||||
|
* @param text The slice of text clay wants measured.
|
||||||
|
* @param config The text element's configuration; only `fontId` is
|
||||||
|
* consulted (see akgl_ui_font_register() on `fontSize`).
|
||||||
|
* @param userData Unused. clay passes back whatever init registered.
|
||||||
|
* @return The slice's size in pixels, or zero by zero on failure.
|
||||||
|
*/
|
||||||
|
Clay_Dimensions akgl_ui_measure_text(Clay_StringSlice text, Clay_TextElementConfig *config, void *userData);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Draw one frame's render commands through a backend.
|
||||||
|
*
|
||||||
|
* The renderer half of akgl_ui_frame_end(), separated so a test can hand it
|
||||||
|
* a command array and read pixels back without a full frame bracket. Handles
|
||||||
|
* RECTANGLE, BORDER, TEXT, IMAGE and the SCISSOR pair; CUSTOM commands and
|
||||||
|
* image tint colours are skipped in this version, and per-corner radii are
|
||||||
|
* collapsed to the top-left value -- the widget helpers only produce uniform
|
||||||
|
* corners.
|
||||||
|
*
|
||||||
|
* An IMAGE command's `imageData` is an `akgl_Sprite *`; its first frame is
|
||||||
|
* drawn stretched to the command's bounding box.
|
||||||
|
*
|
||||||
|
* Any clip rectangle a command set is cleared before this returns, success or
|
||||||
|
* failure -- the UI must not leave the next frame's world clipped.
|
||||||
|
*
|
||||||
|
* @note TEXT commands draw through akgl_text_rendertextat(), whose contract
|
||||||
|
* is the *global* `akgl_renderer` -- so a host drawing its UI through
|
||||||
|
* some other backend must keep the two pointed at the same renderer,
|
||||||
|
* or its text lands somewhere else.
|
||||||
|
*
|
||||||
|
* @param self The backend to draw through. Required, along with its
|
||||||
|
* `sdl_renderer` and `draw_texture`.
|
||||||
|
* @param commands The commands `Clay_EndLayout` returned. Required.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER If @p self, its `sdl_renderer`, its
|
||||||
|
* `draw_texture`, or @p commands is `NULL`.
|
||||||
|
* @throws AKGL_ERR_UI If a text line exceeds #AKGL_UI_MAX_TEXT_BYTES, a
|
||||||
|
* fontId does not resolve, or an IMAGE command carries no sprite.
|
||||||
|
* @throws AKERR_* Whatever the drawing primitives underneath raise.
|
||||||
|
*/
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *akgl_ui_execute_commands(akgl_RenderBackend *self, Clay_RenderCommandArray *commands);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Narrow the arena, so a test can drive akgl_ui_init() into refusal.
|
||||||
|
*
|
||||||
|
* The interesting behaviour is the refusal message naming both numbers, and
|
||||||
|
* the only other way to reach it is rebuilding the library with a smaller
|
||||||
|
* #AKGL_UI_ARENA_BYTES, which CI cannot do and a reader cannot repeat. Same
|
||||||
|
* contract as akgl_ccd_arena_set_limit().
|
||||||
|
*
|
||||||
|
* @param limit Usable bytes. 0, or anything above #AKGL_UI_ARENA_BYTES,
|
||||||
|
* restores the full arena.
|
||||||
|
*/
|
||||||
|
void akgl_ui_arena_limit(size_t limit);
|
||||||
|
|
||||||
|
#endif // _AKGL_UI_H_
|
||||||
@@ -49,8 +49,20 @@ rm -f "$AKGL_MEMCHECK_LOG"/MemoryChecker.*.log
|
|||||||
# The run itself is allowed to fail: a suite that valgrind makes slow enough to
|
# The run itself is allowed to fail: a suite that valgrind makes slow enough to
|
||||||
# trip an assertion still produced a log worth reading, and the log is what
|
# trip an assertion still produced a log worth reading, and the log is what
|
||||||
# decides the exit status below.
|
# decides the exit status below.
|
||||||
|
#
|
||||||
|
# docs_examples and docs_screenshots are excluded, and the exclusion deserves
|
||||||
|
# its reasons stated: those two tests are *shell scripts* that drive gcc and
|
||||||
|
# run the compiled snippets as child processes. Valgrind wraps the test
|
||||||
|
# command, does not trace children, and so spends the whole test memchecking
|
||||||
|
# /bin/bash -- 413 of bash's own by-design leaks on the first CI run that got
|
||||||
|
# this far, and not one byte of libakgl. The snippet programs themselves are
|
||||||
|
# ordinary akgl consumers whose code paths the suites and example games
|
||||||
|
# already cover below, under valgrind, as real binaries. A caller's own -E
|
||||||
|
# overrides this one (ctest takes the last), which is fine: a narrowed run
|
||||||
|
# chose its scope on purpose.
|
||||||
set +e
|
set +e
|
||||||
ctest --test-dir "$AKGL_BUILD_DIR" -T memcheck --output-on-failure "$@"
|
ctest --test-dir "$AKGL_BUILD_DIR" -T memcheck --output-on-failure \
|
||||||
|
-E '^(docs_examples|docs_screenshots)$' "$@"
|
||||||
AKGL_CTEST_STATUS=$?
|
AKGL_CTEST_STATUS=$?
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
|
|||||||
226
src/draw.c
226
src/draw.c
@@ -619,3 +619,229 @@ akerr_ErrorContext *akgl_draw_paste_region(akgl_RenderBackend *self, SDL_Surface
|
|||||||
} FINISH(errctx, true);
|
} FINISH(errctx, true);
|
||||||
SUCCEED_RETURN(errctx);
|
SUCCEED_RETURN(errctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Fill one quarter-circle corner as a triangle fan.
|
||||||
|
*
|
||||||
|
* `SDL_RenderGeometry` colours from its vertices rather than the renderer's
|
||||||
|
* draw colour, so this neither pushes nor pops it.
|
||||||
|
*
|
||||||
|
* @param self The backend. Assumed non-`NULL` with a live `sdl_renderer`;
|
||||||
|
* the caller has already checked both.
|
||||||
|
* @param cx Horizontal position of the corner's centre -- the point the
|
||||||
|
* fan radiates from, one radius inside the rectangle.
|
||||||
|
* @param cy Vertical position of the centre.
|
||||||
|
* @param radius Radius of the quarter circle. Assumed positive.
|
||||||
|
* @param start_deg Angle the quarter starts at; it sweeps 90 degrees clockwise
|
||||||
|
* from there.
|
||||||
|
* @param color Colour to fill with.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKGL_ERR_SDL If the fan cannot be drawn.
|
||||||
|
*/
|
||||||
|
static akerr_ErrorContext *fill_corner_fan(akgl_RenderBackend *self, float32_t cx, float32_t cy, float32_t radius, float32_t start_deg, SDL_Color color)
|
||||||
|
{
|
||||||
|
SDL_Vertex vertices[AKGL_DRAW_ROUNDED_RECT_SEGMENTS + 2];
|
||||||
|
int indices[AKGL_DRAW_ROUNDED_RECT_SEGMENTS * 3];
|
||||||
|
SDL_FColor fcolor;
|
||||||
|
float32_t angle = 0.0f;
|
||||||
|
int i = 0;
|
||||||
|
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
fcolor.r = (float)color.r / 255.0f;
|
||||||
|
fcolor.g = (float)color.g / 255.0f;
|
||||||
|
fcolor.b = (float)color.b / 255.0f;
|
||||||
|
fcolor.a = (float)color.a / 255.0f;
|
||||||
|
|
||||||
|
vertices[0].position.x = cx;
|
||||||
|
vertices[0].position.y = cy;
|
||||||
|
vertices[0].color = fcolor;
|
||||||
|
vertices[0].tex_coord.x = 0.0f;
|
||||||
|
vertices[0].tex_coord.y = 0.0f;
|
||||||
|
for ( i = 0; i <= AKGL_DRAW_ROUNDED_RECT_SEGMENTS; i++ ) {
|
||||||
|
angle = (start_deg + ((90.0f / AKGL_DRAW_ROUNDED_RECT_SEGMENTS) * (float)i)) * (SDL_PI_F / 180.0f);
|
||||||
|
vertices[i + 1].position.x = cx + (radius * SDL_cosf(angle));
|
||||||
|
vertices[i + 1].position.y = cy + (radius * SDL_sinf(angle));
|
||||||
|
vertices[i + 1].color = fcolor;
|
||||||
|
vertices[i + 1].tex_coord.x = 0.0f;
|
||||||
|
vertices[i + 1].tex_coord.y = 0.0f;
|
||||||
|
}
|
||||||
|
for ( i = 0; i < AKGL_DRAW_ROUNDED_RECT_SEGMENTS; i++ ) {
|
||||||
|
indices[(i * 3) + 0] = 0;
|
||||||
|
indices[(i * 3) + 1] = i + 1;
|
||||||
|
indices[(i * 3) + 2] = i + 2;
|
||||||
|
}
|
||||||
|
FAIL_ZERO_RETURN(
|
||||||
|
errctx,
|
||||||
|
SDL_RenderGeometry(
|
||||||
|
self->sdl_renderer,
|
||||||
|
NULL,
|
||||||
|
vertices,
|
||||||
|
AKGL_DRAW_ROUNDED_RECT_SEGMENTS + 2,
|
||||||
|
indices,
|
||||||
|
AKGL_DRAW_ROUNDED_RECT_SEGMENTS * 3),
|
||||||
|
AKGL_ERR_SDL,
|
||||||
|
"%s",
|
||||||
|
SDL_GetError());
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
akerr_ErrorContext *akgl_draw_filled_rounded_rect(akgl_RenderBackend *self, SDL_FRect *rect, float32_t radius, SDL_Color color)
|
||||||
|
{
|
||||||
|
SDL_Color previous;
|
||||||
|
SDL_FRect band;
|
||||||
|
float32_t half = 0.0f;
|
||||||
|
bool pushed = false;
|
||||||
|
bool drawfailed = false;
|
||||||
|
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self");
|
||||||
|
FAIL_ZERO_RETURN(errctx, self->sdl_renderer, AKERR_NULLPOINTER, "No valid SDL rendering backend");
|
||||||
|
FAIL_ZERO_RETURN(errctx, rect, AKERR_NULLPOINTER, "NULL rectangle");
|
||||||
|
if ( (rect->w <= 0.0f) || (rect->h <= 0.0f) ) {
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
if ( radius <= 0.0f ) {
|
||||||
|
PASS(errctx, akgl_draw_filled_rect(self, rect, color));
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
half = (((rect->w < rect->h) ? rect->w : rect->h) / 2.0f);
|
||||||
|
if ( radius > half ) {
|
||||||
|
radius = half;
|
||||||
|
}
|
||||||
|
|
||||||
|
ATTEMPT {
|
||||||
|
CATCH(errctx, push_draw_color(self, color, &previous));
|
||||||
|
pushed = true;
|
||||||
|
|
||||||
|
// The body is three bands: one the full width between the corner rows,
|
||||||
|
// and one between the corners along each of the top and bottom edges.
|
||||||
|
// A radius of exactly half the shorter side leaves one or more of them
|
||||||
|
// with no area, which SDL fills as nothing -- at the limit the whole
|
||||||
|
// shape is the four fans.
|
||||||
|
band.x = rect->x;
|
||||||
|
band.y = rect->y + radius;
|
||||||
|
band.w = rect->w;
|
||||||
|
band.h = rect->h - (radius * 2.0f);
|
||||||
|
if ( SDL_RenderFillRect(self->sdl_renderer, &band) == false ) {
|
||||||
|
drawfailed = true;
|
||||||
|
}
|
||||||
|
band.x = rect->x + radius;
|
||||||
|
band.y = rect->y;
|
||||||
|
band.w = rect->w - (radius * 2.0f);
|
||||||
|
band.h = radius;
|
||||||
|
if ( SDL_RenderFillRect(self->sdl_renderer, &band) == false ) {
|
||||||
|
drawfailed = true;
|
||||||
|
}
|
||||||
|
band.y = rect->y + rect->h - radius;
|
||||||
|
if ( SDL_RenderFillRect(self->sdl_renderer, &band) == false ) {
|
||||||
|
drawfailed = true;
|
||||||
|
}
|
||||||
|
FAIL_NONZERO_BREAK(errctx, drawfailed, AKGL_ERR_SDL, "%s", SDL_GetError());
|
||||||
|
|
||||||
|
CATCH(errctx, fill_corner_fan(self, rect->x + radius, rect->y + radius, radius, 180.0f, color));
|
||||||
|
CATCH(errctx, fill_corner_fan(self, rect->x + rect->w - radius, rect->y + radius, radius, 270.0f, color));
|
||||||
|
CATCH(errctx, fill_corner_fan(self, rect->x + rect->w - radius, rect->y + rect->h - radius, radius, 0.0f, color));
|
||||||
|
CATCH(errctx, fill_corner_fan(self, rect->x + radius, rect->y + rect->h - radius, radius, 90.0f, color));
|
||||||
|
} CLEANUP {
|
||||||
|
if ( pushed == true ) {
|
||||||
|
IGNORE(pop_draw_color(self, &previous));
|
||||||
|
}
|
||||||
|
} PROCESS(errctx) {
|
||||||
|
} FINISH(errctx, true);
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
akerr_ErrorContext *akgl_draw_arc(akgl_RenderBackend *self, float32_t x, float32_t y, float32_t radius, float32_t start_deg, float32_t end_deg, float32_t thickness, SDL_Color color)
|
||||||
|
{
|
||||||
|
SDL_Vertex vertices[AKGL_DRAW_ARC_MAX_POINTS];
|
||||||
|
int indices[(AKGL_DRAW_ARC_MAX_POINTS - 2) * 3];
|
||||||
|
SDL_FColor fcolor;
|
||||||
|
float32_t span = 0.0f;
|
||||||
|
float32_t inner = 0.0f;
|
||||||
|
float32_t angle = 0.0f;
|
||||||
|
int segments = 0;
|
||||||
|
int base = 0;
|
||||||
|
int i = 0;
|
||||||
|
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self");
|
||||||
|
FAIL_ZERO_RETURN(errctx, self->sdl_renderer, AKERR_NULLPOINTER, "No valid SDL rendering backend");
|
||||||
|
FAIL_NONZERO_RETURN(errctx, (radius <= 0.0f), AKERR_OUTOFBOUNDS, "Arc radius %f is not positive", (double)radius);
|
||||||
|
FAIL_NONZERO_RETURN(errctx, (thickness <= 0.0f), AKERR_OUTOFBOUNDS, "Arc thickness %f is not positive", (double)thickness);
|
||||||
|
span = end_deg - start_deg;
|
||||||
|
if ( span <= 0.0f ) {
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
if ( span > 360.0f ) {
|
||||||
|
span = 360.0f;
|
||||||
|
}
|
||||||
|
inner = radius - thickness;
|
||||||
|
if ( inner < 0.0f ) {
|
||||||
|
inner = 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Steps in proportion to the angle swept, so a sliver of arc does not
|
||||||
|
// spend the whole vertex budget and a full circle uses all of it: the
|
||||||
|
// quarter-circle density of AKGL_DRAW_ROUNDED_RECT_SEGMENTS, capped by
|
||||||
|
// what AKGL_DRAW_ARC_MAX_POINTS vertices can carry at two per step.
|
||||||
|
segments = (int)((span / 90.0f) * (float)AKGL_DRAW_ROUNDED_RECT_SEGMENTS) + 1;
|
||||||
|
if ( segments > ((AKGL_DRAW_ARC_MAX_POINTS / 2) - 1) ) {
|
||||||
|
segments = (AKGL_DRAW_ARC_MAX_POINTS / 2) - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
fcolor.r = (float)color.r / 255.0f;
|
||||||
|
fcolor.g = (float)color.g / 255.0f;
|
||||||
|
fcolor.b = (float)color.b / 255.0f;
|
||||||
|
fcolor.a = (float)color.a / 255.0f;
|
||||||
|
for ( i = 0; i <= segments; i++ ) {
|
||||||
|
angle = (start_deg + ((span / (float)segments) * (float)i)) * (SDL_PI_F / 180.0f);
|
||||||
|
vertices[i * 2].position.x = x + (radius * SDL_cosf(angle));
|
||||||
|
vertices[i * 2].position.y = y + (radius * SDL_sinf(angle));
|
||||||
|
vertices[i * 2].color = fcolor;
|
||||||
|
vertices[i * 2].tex_coord.x = 0.0f;
|
||||||
|
vertices[i * 2].tex_coord.y = 0.0f;
|
||||||
|
vertices[(i * 2) + 1].position.x = x + (inner * SDL_cosf(angle));
|
||||||
|
vertices[(i * 2) + 1].position.y = y + (inner * SDL_sinf(angle));
|
||||||
|
vertices[(i * 2) + 1].color = fcolor;
|
||||||
|
vertices[(i * 2) + 1].tex_coord.x = 0.0f;
|
||||||
|
vertices[(i * 2) + 1].tex_coord.y = 0.0f;
|
||||||
|
}
|
||||||
|
for ( i = 0; i < segments; i++ ) {
|
||||||
|
base = i * 2;
|
||||||
|
indices[(i * 6) + 0] = base;
|
||||||
|
indices[(i * 6) + 1] = base + 2;
|
||||||
|
indices[(i * 6) + 2] = base + 1;
|
||||||
|
indices[(i * 6) + 3] = base + 1;
|
||||||
|
indices[(i * 6) + 4] = base + 2;
|
||||||
|
indices[(i * 6) + 5] = base + 3;
|
||||||
|
}
|
||||||
|
FAIL_ZERO_RETURN(
|
||||||
|
errctx,
|
||||||
|
SDL_RenderGeometry(
|
||||||
|
self->sdl_renderer,
|
||||||
|
NULL,
|
||||||
|
vertices,
|
||||||
|
(segments + 1) * 2,
|
||||||
|
indices,
|
||||||
|
segments * 6),
|
||||||
|
AKGL_ERR_SDL,
|
||||||
|
"%s",
|
||||||
|
SDL_GetError());
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
akerr_ErrorContext *akgl_draw_set_clip(akgl_RenderBackend *self, SDL_Rect *rect)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self");
|
||||||
|
FAIL_ZERO_RETURN(errctx, self->sdl_renderer, AKERR_NULLPOINTER, "No valid SDL rendering backend");
|
||||||
|
// NULL rect deliberately passes straight through: it is how the clip is
|
||||||
|
// cleared, and the header says so.
|
||||||
|
FAIL_ZERO_RETURN(
|
||||||
|
errctx,
|
||||||
|
SDL_SetRenderClipRect(self->sdl_renderer, rect),
|
||||||
|
AKGL_ERR_SDL,
|
||||||
|
"%s",
|
||||||
|
SDL_GetError());
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|||||||
@@ -21,5 +21,6 @@ akerr_ErrorContext *akgl_error_init(void)
|
|||||||
PASS(errctx, akerr_register_status_name(AKGL_ERR_OWNER, AKGL_ERR_BEHAVIOR, "Behavior Error"));
|
PASS(errctx, akerr_register_status_name(AKGL_ERR_OWNER, AKGL_ERR_BEHAVIOR, "Behavior Error"));
|
||||||
PASS(errctx, akerr_register_status_name(AKGL_ERR_OWNER, AKGL_ERR_LOGICINTERRUPT, "Logic Interrupt"));
|
PASS(errctx, akerr_register_status_name(AKGL_ERR_OWNER, AKGL_ERR_LOGICINTERRUPT, "Logic Interrupt"));
|
||||||
PASS(errctx, akerr_register_status_name(AKGL_ERR_OWNER, AKGL_ERR_COLLISION, "Collision Error"));
|
PASS(errctx, akerr_register_status_name(AKGL_ERR_OWNER, AKGL_ERR_COLLISION, "Collision Error"));
|
||||||
|
PASS(errctx, akerr_register_status_name(AKGL_ERR_OWNER, AKGL_ERR_UI, "UI Error"));
|
||||||
SUCCEED_RETURN(errctx);
|
SUCCEED_RETURN(errctx);
|
||||||
}
|
}
|
||||||
|
|||||||
21
src/ui_clay.c
Normal file
21
src/ui_clay.c
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
/**
|
||||||
|
* @file ui_clay.c
|
||||||
|
* @brief The one translation unit that compiles clay's implementation.
|
||||||
|
*
|
||||||
|
* clay is a single-header library: every other file that includes <clay.h>
|
||||||
|
* gets declarations only, and exactly one file in the whole program defines
|
||||||
|
* CLAY_IMPLEMENTATION before the include to get the definitions. This is that
|
||||||
|
* file, and it must stay the only one -- a second definition anywhere,
|
||||||
|
* including in a consuming game, is two copies of clay's globals and the
|
||||||
|
* loader picking between them silently. The warning in akgl/ui.h says so to
|
||||||
|
* consumers; this comment says so to us.
|
||||||
|
*
|
||||||
|
* Nothing else belongs here. libakgl's own UI code is src/ui.c, which
|
||||||
|
* includes <clay.h> like any other consumer. CMakeLists.txt compiles this
|
||||||
|
* file with -w on the same terms as deps/semver and deps/libccd: 99% of what
|
||||||
|
* this TU contains is vendored code, and a future clay bump must not be able
|
||||||
|
* to fail the build on a warning we do not own.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#define CLAY_IMPLEMENTATION
|
||||||
|
#include <clay.h>
|
||||||
@@ -284,6 +284,10 @@ akerr_ErrorContext *test_character_sprite_rebind_releases_displaced(void)
|
|||||||
int main(void)
|
int main(void)
|
||||||
{
|
{
|
||||||
PREPARE_ERROR(errctx);
|
PREPARE_ERROR(errctx);
|
||||||
|
|
||||||
|
SDL_SetHint(SDL_HINT_VIDEO_DRIVER, "dummy");
|
||||||
|
SDL_SetHint(SDL_HINT_AUDIO_DRIVER, "dummy");
|
||||||
|
SDL_SetHint(SDL_HINT_RENDER_DRIVER, "software");
|
||||||
ATTEMPT {
|
ATTEMPT {
|
||||||
CATCH(errctx, akgl_error_init());
|
CATCH(errctx, akgl_error_init());
|
||||||
akgl_renderer = &akgl_default_renderer;
|
akgl_renderer = &akgl_default_renderer;
|
||||||
|
|||||||
@@ -50,6 +50,7 @@
|
|||||||
#include <akgl/text.h>
|
#include <akgl/text.h>
|
||||||
#include <akgl/tilemap.h>
|
#include <akgl/tilemap.h>
|
||||||
#include <akgl/types.h>
|
#include <akgl/types.h>
|
||||||
|
#include <akgl/ui.h>
|
||||||
#include <akgl/util.h>
|
#include <akgl/util.h>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
180
tests/draw.c
180
tests/draw.c
@@ -628,6 +628,183 @@ akerr_ErrorContext *test_draw_background(void)
|
|||||||
SUCCEED_RETURN(errctx);
|
SUCCEED_RETURN(errctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
akerr_ErrorContext *test_draw_filled_rounded_rect(void)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
SDL_Surface *shot = NULL;
|
||||||
|
SDL_FRect rect = { 8.0f, 8.0f, 40.0f, 24.0f };
|
||||||
|
SDL_FRect square = { 40.0f, 40.0f, 16.0f, 16.0f };
|
||||||
|
|
||||||
|
ATTEMPT {
|
||||||
|
CATCH(errctx, clear_target());
|
||||||
|
TEST_EXPECT_OK(errctx, akgl_draw_filled_rounded_rect(akgl_renderer, &rect, 8.0f, testred),
|
||||||
|
"filling a rounded rectangle");
|
||||||
|
|
||||||
|
shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL);
|
||||||
|
FAIL_ZERO_BREAK(errctx, shot, AKGL_ERR_SDL, "%s", SDL_GetError());
|
||||||
|
TEST_ASSERT(errctx, pixel_is(shot, 28, 20, testred),
|
||||||
|
"the middle of the rounded rectangle is unfilled");
|
||||||
|
TEST_ASSERT(errctx, pixel_is(shot, 28, 8, testred),
|
||||||
|
"the top edge between the corners is unfilled");
|
||||||
|
TEST_ASSERT(errctx, pixel_is(shot, 8, 20, testred),
|
||||||
|
"the left edge between the corners is unfilled");
|
||||||
|
// 12,12 is inside the corner's quarter circle (5.7px from its centre
|
||||||
|
// at 16,16); 8,8 and 9,9 are outside it (11.3px and 9.9px). The fan's
|
||||||
|
// triangles are chords of the circle, so they cover nothing beyond it.
|
||||||
|
TEST_ASSERT(errctx, pixel_is(shot, 12, 12, testred),
|
||||||
|
"the inside of the rounded corner is unfilled");
|
||||||
|
TEST_ASSERT(errctx, pixel_is(shot, 8, 8, testblack),
|
||||||
|
"the square corner pixel was filled despite the rounding");
|
||||||
|
TEST_ASSERT(errctx, pixel_is(shot, 9, 9, testblack),
|
||||||
|
"a pixel outside the corner arc was filled");
|
||||||
|
SDL_DestroySurface(shot);
|
||||||
|
shot = NULL;
|
||||||
|
|
||||||
|
// A radius beyond half the shorter side clamps to it, which on a
|
||||||
|
// square is a circle: centre filled, corners untouched.
|
||||||
|
CATCH(errctx, clear_target());
|
||||||
|
TEST_EXPECT_OK(errctx, akgl_draw_filled_rounded_rect(akgl_renderer, &square, 100.0f, testgreen),
|
||||||
|
"filling a rounded rectangle with an oversized radius");
|
||||||
|
shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL);
|
||||||
|
FAIL_ZERO_BREAK(errctx, shot, AKGL_ERR_SDL, "%s", SDL_GetError());
|
||||||
|
TEST_ASSERT(errctx, pixel_is(shot, 48, 48, testgreen),
|
||||||
|
"the centre of the clamped-radius square is unfilled");
|
||||||
|
TEST_ASSERT(errctx, pixel_is(shot, 41, 41, testblack),
|
||||||
|
"clamping the radius still filled the square's corner");
|
||||||
|
|
||||||
|
// Radius zero is the plain fill, corners and all.
|
||||||
|
CATCH(errctx, clear_target());
|
||||||
|
TEST_EXPECT_OK(errctx, akgl_draw_filled_rounded_rect(akgl_renderer, &rect, 0.0f, testred),
|
||||||
|
"a zero radius falling through to the plain fill");
|
||||||
|
SDL_DestroySurface(shot);
|
||||||
|
shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL);
|
||||||
|
FAIL_ZERO_BREAK(errctx, shot, AKGL_ERR_SDL, "%s", SDL_GetError());
|
||||||
|
TEST_ASSERT(errctx, pixel_is(shot, 8, 8, testred),
|
||||||
|
"a zero radius did not fill the square corner");
|
||||||
|
|
||||||
|
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
|
||||||
|
akgl_draw_filled_rounded_rect(NULL, &rect, 4.0f, testred),
|
||||||
|
"rounded fill through a NULL backend");
|
||||||
|
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
|
||||||
|
akgl_draw_filled_rounded_rect(akgl_renderer, NULL, 4.0f, testred),
|
||||||
|
"rounded fill of a NULL rectangle");
|
||||||
|
} CLEANUP {
|
||||||
|
if ( shot != NULL ) {
|
||||||
|
SDL_DestroySurface(shot);
|
||||||
|
}
|
||||||
|
} PROCESS(errctx) {
|
||||||
|
} FINISH(errctx, true);
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
akerr_ErrorContext *test_draw_arc(void)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
SDL_Surface *shot = NULL;
|
||||||
|
|
||||||
|
ATTEMPT {
|
||||||
|
// A full ring: outer radius 20, stroke 4, so mid-stroke is radius 18.
|
||||||
|
CATCH(errctx, clear_target());
|
||||||
|
TEST_EXPECT_OK(errctx, akgl_draw_arc(akgl_renderer, 32.0f, 32.0f, 20.0f, 0.0f, 360.0f, 4.0f, testred),
|
||||||
|
"stroking a full circle");
|
||||||
|
shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL);
|
||||||
|
FAIL_ZERO_BREAK(errctx, shot, AKGL_ERR_SDL, "%s", SDL_GetError());
|
||||||
|
TEST_ASSERT(errctx, pixel_is(shot, 50, 32, testred),
|
||||||
|
"the ring is missing at three o'clock");
|
||||||
|
TEST_ASSERT(errctx, pixel_is(shot, 32, 50, testred),
|
||||||
|
"the ring is missing at six o'clock");
|
||||||
|
TEST_ASSERT(errctx, pixel_is(shot, 14, 32, testred),
|
||||||
|
"the ring is missing at nine o'clock");
|
||||||
|
TEST_ASSERT(errctx, pixel_is(shot, 32, 14, testred),
|
||||||
|
"the ring is missing at twelve o'clock");
|
||||||
|
TEST_ASSERT(errctx, pixel_is(shot, 32, 32, testblack),
|
||||||
|
"the centre of the ring was filled");
|
||||||
|
TEST_ASSERT(errctx, pixel_is(shot, 42, 32, testblack),
|
||||||
|
"the stroke bled inside its inner radius");
|
||||||
|
SDL_DestroySurface(shot);
|
||||||
|
shot = NULL;
|
||||||
|
|
||||||
|
// A quarter arc sweeps clockwise from three o'clock to six o'clock;
|
||||||
|
// 44,44 is on its mid-stroke at 45 degrees, twelve o'clock is not in
|
||||||
|
// the sweep at all.
|
||||||
|
CATCH(errctx, clear_target());
|
||||||
|
TEST_EXPECT_OK(errctx, akgl_draw_arc(akgl_renderer, 32.0f, 32.0f, 20.0f, 0.0f, 90.0f, 4.0f, testgreen),
|
||||||
|
"stroking a quarter arc");
|
||||||
|
shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL);
|
||||||
|
FAIL_ZERO_BREAK(errctx, shot, AKGL_ERR_SDL, "%s", SDL_GetError());
|
||||||
|
TEST_ASSERT(errctx, pixel_is(shot, 44, 44, testgreen),
|
||||||
|
"the quarter arc is missing at its midpoint");
|
||||||
|
TEST_ASSERT(errctx, pixel_is(shot, 32, 14, testblack),
|
||||||
|
"the quarter arc drew outside its sweep");
|
||||||
|
|
||||||
|
// An empty sweep draws nothing and reports nothing.
|
||||||
|
TEST_EXPECT_OK(errctx, akgl_draw_arc(akgl_renderer, 32.0f, 32.0f, 20.0f, 90.0f, 90.0f, 4.0f, testred),
|
||||||
|
"an arc with no sweep");
|
||||||
|
|
||||||
|
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
|
||||||
|
akgl_draw_arc(akgl_renderer, 32.0f, 32.0f, 0.0f, 0.0f, 90.0f, 4.0f, testred),
|
||||||
|
"an arc with no radius");
|
||||||
|
TEST_EXPECT_STATUS(errctx, AKERR_OUTOFBOUNDS,
|
||||||
|
akgl_draw_arc(akgl_renderer, 32.0f, 32.0f, 20.0f, 0.0f, 90.0f, -1.0f, testred),
|
||||||
|
"an arc with a negative thickness");
|
||||||
|
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
|
||||||
|
akgl_draw_arc(NULL, 32.0f, 32.0f, 20.0f, 0.0f, 90.0f, 4.0f, testred),
|
||||||
|
"an arc through a NULL backend");
|
||||||
|
} CLEANUP {
|
||||||
|
if ( shot != NULL ) {
|
||||||
|
SDL_DestroySurface(shot);
|
||||||
|
}
|
||||||
|
} PROCESS(errctx) {
|
||||||
|
} FINISH(errctx, true);
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
akerr_ErrorContext *test_draw_set_clip(void)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
SDL_Surface *shot = NULL;
|
||||||
|
SDL_Rect clip = { 0, 0, 16, 16 };
|
||||||
|
SDL_FRect everything = { 0.0f, 0.0f, (float)TEST_TARGET_SIZE, (float)TEST_TARGET_SIZE };
|
||||||
|
|
||||||
|
ATTEMPT {
|
||||||
|
CATCH(errctx, clear_target());
|
||||||
|
TEST_EXPECT_OK(errctx, akgl_draw_set_clip(akgl_renderer, &clip),
|
||||||
|
"setting the clip rectangle");
|
||||||
|
TEST_EXPECT_OK(errctx, akgl_draw_filled_rect(akgl_renderer, &everything, testred),
|
||||||
|
"filling the whole target under a clip");
|
||||||
|
shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL);
|
||||||
|
FAIL_ZERO_BREAK(errctx, shot, AKGL_ERR_SDL, "%s", SDL_GetError());
|
||||||
|
TEST_ASSERT(errctx, pixel_is(shot, 8, 8, testred),
|
||||||
|
"the fill did not reach inside the clip rectangle");
|
||||||
|
TEST_ASSERT(errctx, pixel_is(shot, 32, 32, testblack),
|
||||||
|
"the fill escaped the clip rectangle");
|
||||||
|
SDL_DestroySurface(shot);
|
||||||
|
shot = NULL;
|
||||||
|
|
||||||
|
TEST_EXPECT_OK(errctx, akgl_draw_set_clip(akgl_renderer, NULL),
|
||||||
|
"clearing the clip rectangle");
|
||||||
|
TEST_EXPECT_OK(errctx, akgl_draw_filled_rect(akgl_renderer, &everything, testgreen),
|
||||||
|
"filling the whole target after clearing the clip");
|
||||||
|
shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL);
|
||||||
|
FAIL_ZERO_BREAK(errctx, shot, AKGL_ERR_SDL, "%s", SDL_GetError());
|
||||||
|
TEST_ASSERT(errctx, pixel_is(shot, 32, 32, testgreen),
|
||||||
|
"clearing the clip did not restore the full target");
|
||||||
|
|
||||||
|
TEST_EXPECT_STATUS(errctx, AKERR_NULLPOINTER,
|
||||||
|
akgl_draw_set_clip(NULL, &clip),
|
||||||
|
"clipping through a NULL backend");
|
||||||
|
} CLEANUP {
|
||||||
|
// The clip is renderer state; leaving it set would clip every test
|
||||||
|
// after this one.
|
||||||
|
IGNORE(akgl_draw_set_clip(akgl_renderer, NULL));
|
||||||
|
if ( shot != NULL ) {
|
||||||
|
SDL_DestroySurface(shot);
|
||||||
|
}
|
||||||
|
} PROCESS(errctx) {
|
||||||
|
} FINISH(errctx, true);
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
int main(void)
|
int main(void)
|
||||||
{
|
{
|
||||||
PREPARE_ERROR(errctx);
|
PREPARE_ERROR(errctx);
|
||||||
@@ -668,6 +845,9 @@ int main(void)
|
|||||||
CATCH(errctx, test_draw_preserves_render_draw_color());
|
CATCH(errctx, test_draw_preserves_render_draw_color());
|
||||||
CATCH(errctx, test_draw_backend_without_a_renderer());
|
CATCH(errctx, test_draw_backend_without_a_renderer());
|
||||||
CATCH(errctx, test_draw_background());
|
CATCH(errctx, test_draw_background());
|
||||||
|
CATCH(errctx, test_draw_filled_rounded_rect());
|
||||||
|
CATCH(errctx, test_draw_arc());
|
||||||
|
CATCH(errctx, test_draw_set_clip());
|
||||||
} CLEANUP {
|
} CLEANUP {
|
||||||
SDL_Quit();
|
SDL_Quit();
|
||||||
} PROCESS(errctx) {
|
} PROCESS(errctx) {
|
||||||
|
|||||||
@@ -33,7 +33,8 @@ akerr_ErrorContext *test_error_init_owns_the_status_band(void)
|
|||||||
{ AKGL_ERR_HEAP, "Heap Error" },
|
{ AKGL_ERR_HEAP, "Heap Error" },
|
||||||
{ AKGL_ERR_BEHAVIOR, "Behavior Error" },
|
{ AKGL_ERR_BEHAVIOR, "Behavior Error" },
|
||||||
{ AKGL_ERR_LOGICINTERRUPT, "Logic Interrupt" },
|
{ AKGL_ERR_LOGICINTERRUPT, "Logic Interrupt" },
|
||||||
{ AKGL_ERR_COLLISION, "Collision Error" }
|
{ AKGL_ERR_COLLISION, "Collision Error" },
|
||||||
|
{ AKGL_ERR_UI, "UI Error" }
|
||||||
};
|
};
|
||||||
bool named = true;
|
bool named = true;
|
||||||
int i = 0;
|
int i = 0;
|
||||||
|
|||||||
@@ -240,6 +240,10 @@ int main(void)
|
|||||||
{
|
{
|
||||||
PREPARE_ERROR(errctx);
|
PREPARE_ERROR(errctx);
|
||||||
|
|
||||||
|
SDL_SetHint(SDL_HINT_VIDEO_DRIVER, "dummy");
|
||||||
|
SDL_SetHint(SDL_HINT_AUDIO_DRIVER, "dummy");
|
||||||
|
SDL_SetHint(SDL_HINT_RENDER_DRIVER, "software");
|
||||||
|
|
||||||
ATTEMPT {
|
ATTEMPT {
|
||||||
CATCH(errctx, akgl_error_init());
|
CATCH(errctx, akgl_error_init());
|
||||||
akgl_renderer = &akgl_default_renderer;
|
akgl_renderer = &akgl_default_renderer;
|
||||||
|
|||||||
@@ -740,6 +740,10 @@ int main(void)
|
|||||||
{
|
{
|
||||||
PREPARE_ERROR(errctx);
|
PREPARE_ERROR(errctx);
|
||||||
|
|
||||||
|
SDL_SetHint(SDL_HINT_VIDEO_DRIVER, "dummy");
|
||||||
|
SDL_SetHint(SDL_HINT_AUDIO_DRIVER, "dummy");
|
||||||
|
SDL_SetHint(SDL_HINT_RENDER_DRIVER, "software");
|
||||||
|
|
||||||
ATTEMPT {
|
ATTEMPT {
|
||||||
CATCH(errctx, akgl_error_init());
|
CATCH(errctx, akgl_error_init());
|
||||||
akgl_gamemap = &akgl_default_gamemap;
|
akgl_gamemap = &akgl_default_gamemap;
|
||||||
|
|||||||
1033
tests/ui.c
Normal file
1033
tests/ui.c
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user