Translate and word-wrap cutscene modal text
uiModalOpen now resolves title/message/option strings as locale message IDs first, falling back to the literal string if no match is found, and word-wraps the result to stay within the screen's scan width via the new shared textWrap helper. sceneinitial.c's no-device modal now uses locale keys instead of hardcoded English strings. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,32 @@ msgstr ""
|
|||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
"Plural-Forms: nplurals=4; plural=(n==1 ? 0 : n==2 ? 1 : (n<7 ? 2 : 3));\n"
|
"Plural-Forms: nplurals=4; plural=(n==1 ? 0 : n==2 ? 1 : (n<7 ? 2 : 3));\n"
|
||||||
|
|
||||||
|
msgid "initial.checking_save.title"
|
||||||
|
msgstr "Checking for save data"
|
||||||
|
|
||||||
|
msgid "initial.checking_save.message"
|
||||||
|
msgstr "Please wait..."
|
||||||
|
|
||||||
|
msgid "initial.no_device.title"
|
||||||
|
msgstr "No Save Device Found"
|
||||||
|
|
||||||
|
msgid "initial.no_device.message"
|
||||||
|
msgstr "Could not find a save device, ensure it is connected and try again. You can continue, but progress will not be saved."
|
||||||
|
|
||||||
|
msgid "initial.no_device.retry"
|
||||||
|
msgstr "Try again"
|
||||||
|
|
||||||
|
msgid "initial.no_device.continue"
|
||||||
|
msgstr "Continue without saving"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#: ui/menu.c:10
|
#: ui/menu.c:10
|
||||||
msgid "ui.title"
|
msgid "ui.title"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|||||||
@@ -187,3 +187,37 @@ int32_t textMeasure(
|
|||||||
|
|
||||||
return spriteCount;
|
return spriteCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void textWrap(char_t *text, const font_t *font, const float_t maxWidth) {
|
||||||
|
assertNotNull(text, "Text cannot be NULL");
|
||||||
|
assertNotNull(font, "Font cannot be NULL");
|
||||||
|
|
||||||
|
float_t fontWidth = (float_t)font->tileset->tileWidth;
|
||||||
|
if(fontWidth <= 0.0f) return;
|
||||||
|
|
||||||
|
int32_t charsPerLine = (int32_t)(maxWidth / fontWidth);
|
||||||
|
if(charsPerLine <= 0) return;
|
||||||
|
|
||||||
|
int32_t lineWidth = 0;
|
||||||
|
int32_t lastSpace = -1;
|
||||||
|
|
||||||
|
for(int32_t i = 0; text[i] != '\0'; i++) {
|
||||||
|
if(text[i] == '\n') {
|
||||||
|
lineWidth = 0;
|
||||||
|
lastSpace = -1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(text[i] == ' ') {
|
||||||
|
lastSpace = i;
|
||||||
|
}
|
||||||
|
|
||||||
|
lineWidth++;
|
||||||
|
|
||||||
|
if(lineWidth > charsPerLine && lastSpace != -1) {
|
||||||
|
text[lastSpace] = '\n';
|
||||||
|
lineWidth = i - lastSpace;
|
||||||
|
lastSpace = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -104,3 +104,17 @@ int32_t textMeasure(
|
|||||||
int32_t *outWidth,
|
int32_t *outWidth,
|
||||||
int32_t *outHeight
|
int32_t *outHeight
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Word-wraps text in place for display at up to maxWidth pixels wide,
|
||||||
|
* by replacing the space nearest each overflow point with a newline.
|
||||||
|
* Length is unchanged - this only ever swaps existing spaces for
|
||||||
|
* newlines, never inserts characters - so it's always safe to call on a
|
||||||
|
* fixed-size buffer. A single word wider than maxWidth on its own is
|
||||||
|
* left unbroken.
|
||||||
|
*
|
||||||
|
* @param text Null-terminated, caller-owned buffer to wrap in place.
|
||||||
|
* @param font Font to measure character width with.
|
||||||
|
* @param maxWidth Maximum line width, in pixels.
|
||||||
|
*/
|
||||||
|
void textWrap(char_t *text, const font_t *font, const float_t maxWidth);
|
||||||
|
|||||||
@@ -89,6 +89,9 @@ typedef struct cutscene_s {
|
|||||||
// the dialog to be dismissed. Script the rest of the interaction (e.g.
|
// the dialog to be dismissed. Script the rest of the interaction (e.g.
|
||||||
// CUTSCENE_CALLBACK to kick off work, CUTSCENE_WAIT, then
|
// CUTSCENE_CALLBACK to kick off work, CUTSCENE_WAIT, then
|
||||||
// CUTSCENE_MODAL_CLOSE) as later items in the same cutscene.
|
// CUTSCENE_MODAL_CLOSE) as later items in the same cutscene.
|
||||||
|
// TITLE and MESSAGE are each displayed as-is unless they match a
|
||||||
|
// locale message ID, in which case the translated string is shown
|
||||||
|
// instead - see uiModalLocalize.
|
||||||
#define CUTSCENE_MODAL(TITLE, MESSAGE) \
|
#define CUTSCENE_MODAL(TITLE, MESSAGE) \
|
||||||
{ \
|
{ \
|
||||||
.type = CUTSCENE_ITEM_TYPE_MODAL, \
|
.type = CUTSCENE_ITEM_TYPE_MODAL, \
|
||||||
@@ -100,8 +103,10 @@ typedef struct cutscene_s {
|
|||||||
// CALLBACK fires with the selected option index (or UI_MODAL_RESULT_NONE
|
// CALLBACK fires with the selected option index (or UI_MODAL_RESULT_NONE
|
||||||
// if backed out of) once the dialog closes. Option labels are passed as
|
// if backed out of) once the dialog closes. Option labels are passed as
|
||||||
// trailing arguments, e.g. CUTSCENE_MODAL_OPTIONS(title, message,
|
// trailing arguments, e.g. CUTSCENE_MODAL_OPTIONS(title, message,
|
||||||
// callback, "Retry", "Cancel") - their strings are not copied, so they
|
// callback, "Retry", "Cancel") - their strings are not copied by this
|
||||||
// must outlive the modal (string literals are fine).
|
// item, so they must stay valid until the modal opens (string literals
|
||||||
|
// are fine). Like TITLE and MESSAGE, each option is translated if it
|
||||||
|
// matches a locale message ID.
|
||||||
#define CUTSCENE_MODAL_OPTIONS(TITLE, MESSAGE, CALLBACK, ...) \
|
#define CUTSCENE_MODAL_OPTIONS(TITLE, MESSAGE, CALLBACK, ...) \
|
||||||
{ \
|
{ \
|
||||||
.type = CUTSCENE_ITEM_TYPE_MODAL, \
|
.type = CUTSCENE_ITEM_TYPE_MODAL, \
|
||||||
|
|||||||
@@ -28,12 +28,16 @@ typedef void (*cutscenemodaloptioncallback_t)(
|
|||||||
);
|
);
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
|
// Display text, or a locale message ID to translate - see
|
||||||
|
// uiModalLocalize.
|
||||||
char_t title[CUTSCENE_MODAL_TITLE_MAX_CHARS];
|
char_t title[CUTSCENE_MODAL_TITLE_MAX_CHARS];
|
||||||
char_t message[CUTSCENE_MODAL_MESSAGE_MAX_CHARS];
|
char_t message[CUTSCENE_MODAL_MESSAGE_MAX_CHARS];
|
||||||
|
|
||||||
// NOT copied - these pointers are stored directly by the underlying
|
// NOT copied by this struct - these pointers are only read once,
|
||||||
// buttons, so they must stay valid for as long as the modal is open
|
// while uiModalOpen is opening the modal in cutsceneModalStart, so
|
||||||
// (e.g. string literals, as CUTSCENE_MODAL_OPTIONS produces).
|
// they only need to stay valid until then (e.g. string literals, as
|
||||||
|
// CUTSCENE_MODAL_OPTIONS produces). Each is displayed text or a
|
||||||
|
// locale message ID, same as title/message.
|
||||||
const char_t **options;
|
const char_t **options;
|
||||||
uint8_t optionCount;
|
uint8_t optionCount;
|
||||||
|
|
||||||
|
|||||||
@@ -22,8 +22,7 @@ void sceneInitialSaveDeviceRetryCallback(const uint8_t opt, void *u);
|
|||||||
|
|
||||||
CUTSCENE(INITIAL, 0, DEFAULT,
|
CUTSCENE(INITIAL, 0, DEFAULT,
|
||||||
CUTSCENE_MODAL(
|
CUTSCENE_MODAL(
|
||||||
"Checking for save data...",
|
"initial.checking_save.title", "initial.checking_save.message"
|
||||||
"Please wait."
|
|
||||||
),
|
),
|
||||||
CUTSCENE_WAIT(1.0f),
|
CUTSCENE_WAIT(1.0f),
|
||||||
CUTSCENE_CALLBACK(sceneInitialFindDevices),
|
CUTSCENE_CALLBACK(sceneInitialFindDevices),
|
||||||
@@ -31,10 +30,10 @@ CUTSCENE(INITIAL, 0, DEFAULT,
|
|||||||
CUTSCENE_MARKER("NO_DEVICE"),
|
CUTSCENE_MARKER("NO_DEVICE"),
|
||||||
CUTSCENE_MODAL_CLOSE(),
|
CUTSCENE_MODAL_CLOSE(),
|
||||||
CUTSCENE_MODAL_OPTIONS(
|
CUTSCENE_MODAL_OPTIONS(
|
||||||
"No save device found",
|
"initial.no_device.title",
|
||||||
"Please ensure a save device is connected and try again.",
|
"initial.no_device.message",
|
||||||
sceneInitialSaveDeviceRetryCallback,
|
sceneInitialSaveDeviceRetryCallback,
|
||||||
"Retry", "Continue without saving"
|
"initial.no_device.retry", "initial.no_device.continue"
|
||||||
),
|
),
|
||||||
|
|
||||||
CUTSCENE_MARKER("RETRY"),
|
CUTSCENE_MARKER("RETRY"),
|
||||||
|
|||||||
@@ -17,6 +17,8 @@
|
|||||||
#include "display/spritebatch/spritebatch.h"
|
#include "display/spritebatch/spritebatch.h"
|
||||||
#include "display/texture/texture.h"
|
#include "display/texture/texture.h"
|
||||||
#include "display/shader/shaderunlit.h"
|
#include "display/shader/shaderunlit.h"
|
||||||
|
#include "locale/localemanager.h"
|
||||||
|
#include "asset/loader/locale/assetlocaleloader.h"
|
||||||
|
|
||||||
#define UI_MODAL_BACKDROP_COLOR color4b(0, 0, 0, 160)
|
#define UI_MODAL_BACKDROP_COLOR color4b(0, 0, 0, 160)
|
||||||
|
|
||||||
@@ -119,6 +121,20 @@ errorret_t uiModalDraw(void) {
|
|||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void uiModalLocalize(
|
||||||
|
const char_t *key,
|
||||||
|
char_t *buffer,
|
||||||
|
const size_t bufferSize
|
||||||
|
) {
|
||||||
|
errorret_t result = assetLocaleGetString(
|
||||||
|
&LOCALE.entry->data.locale, key, 0, buffer, bufferSize
|
||||||
|
);
|
||||||
|
if(errorIsNotOk(result)) {
|
||||||
|
errorCatch(result);
|
||||||
|
stringCopy(buffer, key, bufferSize - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bool_t uiModalIsOpen(void) {
|
bool_t uiModalIsOpen(void) {
|
||||||
return UI_MODAL.open;
|
return UI_MODAL.open;
|
||||||
}
|
}
|
||||||
@@ -146,11 +162,16 @@ void uiModalOpen(
|
|||||||
optionCount <= UI_MODAL_OPTIONS_MAX, "Too many options for modal"
|
optionCount <= UI_MODAL_OPTIONS_MAX, "Too many options for modal"
|
||||||
);
|
);
|
||||||
|
|
||||||
stringCopy(UI_MODAL.titleText, title, UI_MODAL_TITLE_TEXT_MAX - 1);
|
float_t maxContentWidth = ((float_t)SCREEN.scanWidth * UI_MODAL_MAX_WIDTH_RATIO)
|
||||||
|
- (UI_FRAME_START_X * 2.0f);
|
||||||
|
|
||||||
|
uiModalLocalize(title, UI_MODAL.titleText, UI_MODAL_TITLE_TEXT_MAX);
|
||||||
|
textWrap(UI_MODAL.titleText, &FONT_DEFAULT, maxContentWidth);
|
||||||
UI_MODAL.titleLabel.dirty = true;
|
UI_MODAL.titleLabel.dirty = true;
|
||||||
uiLabelRebuffer(&UI_MODAL.titleLabel);
|
uiLabelRebuffer(&UI_MODAL.titleLabel);
|
||||||
|
|
||||||
stringCopy(UI_MODAL.messageText, message, UI_MODAL_MESSAGE_TEXT_MAX - 1);
|
uiModalLocalize(message, UI_MODAL.messageText, UI_MODAL_MESSAGE_TEXT_MAX);
|
||||||
|
textWrap(UI_MODAL.messageText, &FONT_DEFAULT, maxContentWidth);
|
||||||
UI_MODAL.messageLabel.dirty = true;
|
UI_MODAL.messageLabel.dirty = true;
|
||||||
uiLabelRebuffer(&UI_MODAL.messageLabel);
|
uiLabelRebuffer(&UI_MODAL.messageLabel);
|
||||||
|
|
||||||
@@ -165,7 +186,10 @@ void uiModalOpen(
|
|||||||
&UI_MODAL.menu, UI_MODAL.options, uiModalSelected, uiModalClosed, NULL
|
&UI_MODAL.menu, UI_MODAL.options, uiModalSelected, uiModalClosed, NULL
|
||||||
);
|
);
|
||||||
for(uint8_t i = 0; i < optionCount; i++) {
|
for(uint8_t i = 0; i < optionCount; i++) {
|
||||||
MENU_BUTTON(options[i]);
|
uiModalLocalize(
|
||||||
|
options[i], UI_MODAL.optionsText[i], UI_MODAL_OPTION_TEXT_MAX
|
||||||
|
);
|
||||||
|
MENU_BUTTON(UI_MODAL.optionsText[i]);
|
||||||
}
|
}
|
||||||
MENU_END(UI_MODAL.options, menuIndex);
|
MENU_END(UI_MODAL.options, menuIndex);
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,12 @@
|
|||||||
#define UI_MODAL_MESSAGE_TEXT_MAX 256
|
#define UI_MODAL_MESSAGE_TEXT_MAX 256
|
||||||
#define UI_MODAL_MESSAGE_SPRITES_MAX UI_MODAL_MESSAGE_TEXT_MAX
|
#define UI_MODAL_MESSAGE_SPRITES_MAX UI_MODAL_MESSAGE_TEXT_MAX
|
||||||
#define UI_MODAL_OPTIONS_MAX 4
|
#define UI_MODAL_OPTIONS_MAX 4
|
||||||
|
#define UI_MODAL_OPTION_TEXT_MAX 32
|
||||||
#define UI_MODAL_MIN_WIDTH 160.0f
|
#define UI_MODAL_MIN_WIDTH 160.0f
|
||||||
|
// Title/message text wraps at word boundaries to stay within this
|
||||||
|
// fraction of the screen's scan width, so long (e.g. translated)
|
||||||
|
// strings don't run off-screen.
|
||||||
|
#define UI_MODAL_MAX_WIDTH_RATIO 0.8f
|
||||||
#define UI_MODAL_RESULT_NONE 0xFF
|
#define UI_MODAL_RESULT_NONE 0xFF
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -54,6 +59,7 @@ typedef struct {
|
|||||||
|
|
||||||
uimenu_t menu;
|
uimenu_t menu;
|
||||||
uimenuitem_t options[UI_MODAL_OPTIONS_MAX];
|
uimenuitem_t options[UI_MODAL_OPTIONS_MAX];
|
||||||
|
char_t optionsText[UI_MODAL_OPTIONS_MAX][UI_MODAL_OPTION_TEXT_MAX];
|
||||||
|
|
||||||
uimodaloptioncallback_t callback;
|
uimodaloptioncallback_t callback;
|
||||||
uimodalopenedcallback_t onOpen;
|
uimodalopenedcallback_t onOpen;
|
||||||
@@ -99,14 +105,19 @@ uint8_t uiModalGetResult(void);
|
|||||||
* callback is invoked exactly once with the result, whether the dialog
|
* callback is invoked exactly once with the result, whether the dialog
|
||||||
* was dismissed by selecting an option or by pressing cancel/back.
|
* was dismissed by selecting an option or by pressing cancel/back.
|
||||||
*
|
*
|
||||||
* @param title Display title; copied internally, safe to be transient.
|
* title, message, and each option are treated as locale message IDs
|
||||||
* @param message Display message; copied internally, safe to be
|
* first: if one matches an entry in the active locale file its
|
||||||
* transient.
|
* translated string is used, otherwise the string itself is displayed
|
||||||
* @param options Array of option label strings; NOT copied internally,
|
* verbatim. See uiModalLocalize.
|
||||||
* the pointers are stored directly by the underlying buttons, so they
|
*
|
||||||
* must remain valid for as long as the modal is open (e.g. string
|
* @param title Display title, or a locale message ID; copied
|
||||||
* literals or locale-owned strings). May be NULL for a message-only
|
* internally, safe to be transient.
|
||||||
* dialog with no option buttons, in which case optionCount must be 0.
|
* @param message Display message, or a locale message ID; copied
|
||||||
|
* internally, safe to be transient.
|
||||||
|
* @param options Array of option label strings/locale message IDs;
|
||||||
|
* copied internally, safe to be transient. May be NULL for a
|
||||||
|
* message-only dialog with no option buttons, in which case optionCount
|
||||||
|
* must be 0.
|
||||||
* @param optionCount Number of options, from 0 to UI_MODAL_OPTIONS_MAX.
|
* @param optionCount Number of options, from 0 to UI_MODAL_OPTIONS_MAX.
|
||||||
* @param callback Called with the result once the dialog closes. May be
|
* @param callback Called with the result once the dialog closes. May be
|
||||||
* NULL.
|
* NULL.
|
||||||
@@ -139,3 +150,18 @@ void uiModalClose(uimodalclosedcallback_t callback);
|
|||||||
* @return Any error that occurs.
|
* @return Any error that occurs.
|
||||||
*/
|
*/
|
||||||
errorret_t uiModalDispose(void);
|
errorret_t uiModalDispose(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves display text for uiModalOpen: if key matches a locale
|
||||||
|
* message ID in the active locale file, buffer is filled with its
|
||||||
|
* translated string, otherwise key is copied into buffer verbatim.
|
||||||
|
*
|
||||||
|
* @param key Display string or locale message ID.
|
||||||
|
* @param buffer Destination buffer.
|
||||||
|
* @param bufferSize Size of buffer, in bytes.
|
||||||
|
*/
|
||||||
|
void uiModalLocalize(
|
||||||
|
const char_t *key,
|
||||||
|
char_t *buffer,
|
||||||
|
const size_t bufferSize
|
||||||
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user