nitro-engine/source/NERichText.c
jonko0493 8932de1fff
Some checks failed
Documentation / Generate website (push) Has been cancelled
Documentation / Publish website with GitHub pages (push) Has been cancelled
rich text: Optionally return cursor position from dry run & allow indenting text
The goal of the changes made in this PR is to create something with this
effect ("Jonko" is in in green, the rest is in white):

    "Dear god." Jonko quickly replied,
    crestfallen.

In order to do this, I need to call `NE_RichTextRender3D` or
`NE_RichTextRender3DAlpha` three times: once to draw the first part of
the sentence with the white font, once to draw "Jonko" in turquoise, and
once to draw the remainder of the sentence in white again. In order to
ensure that text is being drawn in the correct location, I need the
position where the previous call stopped drawing, which leads to the
first change this PR makes: I have added a version of
`DSF_StringRenderDryRun` which accepts pointers to `final_x` and
`final_y` and stores the last position of `font->pointer_x` and
`font->pointer_y` in them and I have added a new variant of
`NE_RichTextRenderDryRun` called `NE_RichTextRenderDryRunWithPos` which
uses that DSF function and has analogous parameters.

These functions get us the position where we need to place the next
cursor, but the next issue is that currently NitroEngine and libDSF only
allow for specifying the position of the overall box to draw text in,
but not where to offset the cursor in that box. This leads to the
following problem ("Jonko" is in in green, the rest is in white):

    "Dear god." Jonko quickly replied,
                        crestfallen.

Thus, the second change: I have introduced new versions of
`DSF_StringRender3D` and `DSF_StringRender3DAlpha` which accept an
`xStart` position that is added to the `pointerX`. Correspondingly, I
have added versions of `NE_RichTextRender3D` and
`NE_RichTextRender3DAlpha` that use these new functions as well.

Let me know if there are any issues with how I've written this (C is not
my forte, after all), but the first screenshot is this being tested in
production code.
2025-06-11 00:03:38 +01:00

513 lines
13 KiB
C

// SPDX-License-Identifier: MIT
//
// Copyright (c) 2024 Antonio Niño Díaz
//
// This file is part of Nitro Engine
#include "NEMain.h"
#include "libdsf/dsf.h"
/// @file NERichText.c
typedef struct {
// Fields used when the font texture is stored in VRAM
NE_Material *material;
NE_Palette *palette;
// Fields used when the font texture is stored in RAM
bool has_to_free_buffers;
NE_TextureFormat fmt;
void *texture_buffer;
size_t texture_width;
size_t texture_height;
void *palette_buffer;
size_t palette_size;
// Other fields
dsf_handle handle;
bool active;
} ne_rich_textinfo_t;
static u32 NE_NumRichTextSlots = 0;
static ne_rich_textinfo_t *NE_RichTextInfo;
static int NE_RICH_TEXT_PRIORITY = 0;
void NE_RichTextPrioritySet(int priority)
{
NE_RICH_TEXT_PRIORITY = priority;
}
void NE_RichTextPriorityReset(void)
{
NE_RICH_TEXT_PRIORITY = 0;
}
void NE_RichTextInit(u32 slot)
{
// Compatibility mode -- if someone tries to allocate a slot
// without having called start system, we allocate the old maximum
// number of slots for safety
if (NE_NumRichTextSlots == 0)
NE_RichTextStartSystem(NE_DEFAULT_RICH_TEXT_FONTS);
if (slot >= NE_NumRichTextSlots)
{
NE_DebugPrint("Attempted to initialize a slot greater than the number of slots allocated; skipping");
return;
}
ne_rich_textinfo_t *info = &NE_RichTextInfo[slot];
if (info->active)
NE_RichTextEnd(slot);
memset(info, 0, sizeof(ne_rich_textinfo_t));
info->active = true;
NE_RichTextPriorityReset();
}
int NE_RichTextEnd(u32 slot)
{
if (slot >= NE_NumRichTextSlots)
return 0;
ne_rich_textinfo_t *info = &NE_RichTextInfo[slot];
if (!info->active)
return 0;
if (info->material != NULL)
NE_MaterialDelete(info->material);
if (info->palette != NULL)
NE_PaletteDelete(info->palette);
if (info->has_to_free_buffers)
{
if (info->texture_buffer != NULL)
free(info->texture_buffer);
if (info->palette_buffer != NULL)
free(info->palette_buffer);
}
int ret = 0;
if (info->handle)
{
dsf_error err = DSF_FreeFont(&(info->handle));
if (err != DSF_NO_ERROR)
ret = err;
}
memset(info, 0, sizeof(ne_rich_textinfo_t));
if (ret != 0)
return 0;
return 1;
}
int NE_RichTextStartSystem(u32 numSlots)
{
NE_NumRichTextSlots = numSlots;
NE_RichTextInfo = calloc(sizeof(ne_rich_textinfo_t), NE_NumRichTextSlots);
if (NE_RichTextInfo == NULL)
{
NE_DebugPrint("Failed to allocate array for NE_RichTextInfo");
return 0;
}
return 1;
}
void NE_RichTextResetSystem(void)
{
for (int i = 0; i < NE_NumRichTextSlots; i++)
NE_RichTextEnd(i);
free(NE_RichTextInfo);
NE_NumRichTextSlots = 0;
}
int NE_RichTextMetadataLoadFAT(u32 slot, const char *path)
{
NE_AssertPointer(path, "NULL path pointer");
if (slot >= NE_NumRichTextSlots)
return 0;
ne_rich_textinfo_t *info = &NE_RichTextInfo[slot];
if (!info->active)
return 0;
dsf_handle handle;
dsf_error ret = DSF_LoadFontFilesystem(&handle, path);
if (ret != DSF_NO_ERROR)
{
NE_DebugPrint("DSF_LoadFontFilesystem(): %d\n", ret);
return 0;
}
info->handle = handle;
return 1;
}
int NE_RichTextMetadataLoadMemory(u32 slot, const void *data, size_t data_size)
{
NE_AssertPointer(data, "NULL data pointer");
if (slot >= NE_NumRichTextSlots)
return 0;
ne_rich_textinfo_t *info = &NE_RichTextInfo[slot];
if (!info->active)
return 0;
dsf_handle handle;
dsf_error ret = DSF_LoadFontMemory(&handle, data, data_size);
if (ret != DSF_NO_ERROR)
{
NE_DebugPrint("DSF_LoadFontMemory(): %d\n", ret);
return 0;
}
info->handle = handle;
return 1;
}
int NE_RichTextMaterialLoadGRF(u32 slot, const char *path)
{
NE_AssertPointer(path, "NULL path pointer");
if (slot >= NE_NumRichTextSlots)
return 0;
ne_rich_textinfo_t *info = &NE_RichTextInfo[slot];
if (!info->active)
return 0;
info->material = NE_MaterialCreate();
info->palette = NE_PaletteCreate();
int ret = NE_MaterialTexLoadGRF(info->material, info->palette,
NE_TEXGEN_TEXCOORD | NE_TEXTURE_COLOR0_TRANSPARENT, path);
if (ret == 0)
{
NE_MaterialDelete(info->material);
NE_PaletteDelete(info->palette);
return 0;
}
return 1;
}
int NE_RichTextMaterialSet(u32 slot, NE_Material *mat, NE_Palette *pal)
{
NE_AssertPointer(mat, "NULL material pointer");
if (slot >= NE_NumRichTextSlots)
return 0;
ne_rich_textinfo_t *info = &NE_RichTextInfo[slot];
if (!info->active)
return 0;
info->material = mat;
info->palette = pal;
return 1;
}
int NE_RichTextBitmapLoadGRF(u32 slot, const char *path)
{
#ifndef NE_BLOCKSDS
(void)slot;
(void)path;
NE_DebugPrint("%s only supported in BlocksDS", __func__);
return 0;
#else // NE_BLOCKSDS
NE_AssertPointer(path, "NULL path pointer");
if (slot >= NE_NumRichTextSlots)
return 0;
ne_rich_textinfo_t *info = &NE_RichTextInfo[slot];
if (!info->active)
return 0;
if (info->has_to_free_buffers)
{
if (info->texture_buffer != NULL)
free(info->texture_buffer);
if (info->palette_buffer != NULL)
free(info->palette_buffer);
}
void *gfxDst = NULL;
void *palDst = NULL;
size_t palSize;
GRFHeader header = { 0 };
GRFError err = grfLoadPath(path, &header, &gfxDst, NULL, NULL, NULL,
&palDst, &palSize);
if (err != GRF_NO_ERROR)
{
NE_DebugPrint("Couldn't load GRF file: %d", err);
goto error;
}
if (gfxDst == NULL)
{
NE_DebugPrint("No graphics found in GRF file");
goto error;
}
bool palette_required = true;
switch (header.gfxAttr)
{
case GRF_TEXFMT_A5I3:
info->fmt = NE_A5PAL8;
break;
case GRF_TEXFMT_A3I5:
info->fmt = NE_A3PAL32;
break;
case GRF_TEXFMT_4x4:
info->fmt = NE_TEX4X4;
break;
case 16:
info->fmt = NE_A1RGB5;
palette_required = false;
break;
case 8:
info->fmt = NE_PAL256;
break;
case 4:
info->fmt = NE_PAL16;
break;
case 2:
info->fmt = NE_PAL4;
break;
default:
NE_DebugPrint("Invalid format in GRF file");
goto error;
}
info->texture_buffer = gfxDst;
info->texture_width = header.gfxWidth;
info->texture_height = header.gfxHeight;
if (palDst != NULL)
{
info->palette_buffer = palDst;
info->palette_size = palSize;
}
else
{
if (palette_required)
{
NE_DebugPrint("No palette found in GRF, but format requires it");
goto error;
}
info->palette_buffer = NULL;
info->palette_size = 0;
}
info->has_to_free_buffers = true;
return 1; // Success
error:
free(gfxDst);
free(palDst);
return 0;
#endif // NE_BLOCKSDS
}
int NE_RichTextBitmapSet(u32 slot, const void *texture_buffer,
size_t texture_width, size_t texture_height,
NE_TextureFormat texture_fmt,
const void *palette_buffer, size_t palette_size)
{
NE_AssertPointer(texture_buffer, "NULL texture pointer");
NE_AssertPointer(palette_buffer, "NULL palette pointer");
if (slot >= NE_NumRichTextSlots)
return 0;
ne_rich_textinfo_t *info = &NE_RichTextInfo[slot];
if (!info->active)
return 0;
if (info->has_to_free_buffers)
{
if (info->texture_buffer != NULL)
free(info->texture_buffer);
if (info->palette_buffer != NULL)
free(info->palette_buffer);
}
info->has_to_free_buffers = false;
info->texture_buffer = (void *)texture_buffer;
info->texture_width = texture_width;
info->texture_height = texture_height;
info->fmt = texture_fmt;
info->palette_buffer = (void *)palette_buffer;
info->palette_size = palette_size;
return 1;
}
int NE_RichTextRenderDryRunWithPos(u32 slot, const char *str,
size_t *size_x, size_t *size_y,
size_t *final_x, size_t *final_y)
{
NE_AssertPointer(str, "NULL str pointer");
NE_AssertPointer(size_x, "NULL size X pointer");
NE_AssertPointer(size_y, "NULL size Y pointer");
NE_AssertPointer(final_x, "NULL final X pointer");
NE_AssertPointer(final_y, "NULL final Y pointer");
if (slot >= NE_NumRichTextSlots)
return 0;
ne_rich_textinfo_t *info = &NE_RichTextInfo[slot];
if (!info->active)
return 0;
dsf_error err = DSF_StringRenderDryRunWithCursor(info->handle, str,
size_x, size_y,
final_x, final_y);
if (err != DSF_NO_ERROR)
return 0;
return 1;
}
int NE_RichTextRenderDryRun(u32 slot, const char *str,
size_t *size_x, size_t *size_y)
{
size_t final_x, final_y;
return NE_RichTextRenderDryRunWithPos(slot, str, size_x, size_y, &final_x, &final_y);
}
int NE_RichTextRender3DWithIndent(u32 slot, const char *str, s32 x, s32 y,
s32 xIndent)
{
NE_AssertPointer(str, "NULL str pointer");
if (slot >= NE_NumRichTextSlots)
return 0;
ne_rich_textinfo_t *info = &NE_RichTextInfo[slot];
if (!info->active)
return 0;
NE_MaterialUse(info->material);
dsf_error err = DSF_StringRender3DWithIndent(info->handle, str, x, y,
NE_RICH_TEXT_PRIORITY, xIndent);
if (err != DSF_NO_ERROR)
return 0;
return 1;
}
int NE_RichTextRender3D(u32 slot, const char *str, s32 x, s32 y)
{
return NE_RichTextRender3DWithIndent(slot, str, x, y, 0);
}
int NE_RichTextRender3DAlphaWithIndent(u32 slot, const char *str, s32 x, s32 y,
uint32_t poly_fmt, int poly_id_base,
s32 xIndent)
{
NE_AssertPointer(str, "NULL str pointer");
if (slot >= NE_NumRichTextSlots)
return 0;
ne_rich_textinfo_t *info = &NE_RichTextInfo[slot];
if (!info->active)
return 0;
NE_MaterialUse(info->material);
dsf_error err = DSF_StringRender3DAlphaWithIndent(info->handle, str, x, y,
NE_RICH_TEXT_PRIORITY,
poly_fmt, poly_id_base, xIndent);
if (err != DSF_NO_ERROR)
return 0;
return 1;
}
int NE_RichTextRender3DAlpha(u32 slot, const char *str, s32 x, s32 y,
uint32_t poly_fmt, int poly_id_base)
{
return NE_RichTextRender3DAlphaWithIndent(slot, str, x, y, poly_fmt, poly_id_base, 0);
}
int NE_RichTextRenderMaterial(u32 slot, const char *str, NE_Material **mat,
NE_Palette **pal)
{
NE_AssertPointer(str, "NULL str pointer");
NE_AssertPointer(mat, "NULL mat pointer");
NE_AssertPointer(pal, "NULL pal pointer");
if (slot >= NE_NumRichTextSlots)
return 0;
ne_rich_textinfo_t *info = &NE_RichTextInfo[slot];
if (!info->active)
return 0;
void *out_texture = NULL;
size_t out_width, out_height;
dsf_error err = DSF_StringRenderToTexture(info->handle,
str, info->fmt, info->texture_buffer,
info->texture_width, info->texture_height,
&out_texture, &out_width, &out_height);
if (err != DSF_NO_ERROR)
{
free(out_texture);
return 0;
}
*mat = NE_MaterialCreate();
if (NE_MaterialTexLoad(*mat, info->fmt, out_width, out_height,
NE_TEXGEN_TEXCOORD | NE_TEXTURE_COLOR0_TRANSPARENT,
out_texture) == 0)
{
free(out_texture);
return 0;
}
if (info->palette_buffer != NULL)
{
NE_Palette *palette = NE_PaletteCreate();
if (NE_PaletteLoad(palette, info->palette_buffer,
info->palette_size / 2, info->fmt) == 0)
{
NE_MaterialDelete(*mat);
free(out_texture);
return 0;
}
NE_MaterialSetPalette(*mat, palette);
// If the caller has requested the pointer to the palette, return the
// value to the user. If not, mark it to be autodeleted.
if (pal)
*pal = palette;
else
NE_MaterialAutodeletePalette(*mat);
}
// This isn't needed after it has been loaded to VRAM
free(out_texture);
return 1;
}