/** * @file textbox.c * @brief The dialogue panel: two rectangles and a string, drawn over the world. * * Text in libakgl is immediate mode. Every akgl_text_rendertextat call * rasterizes the string through SDL_ttf, uploads it as a texture, blits it and * destroys the texture again -- there is no cached glyph atlas and no text * object to hold on to. That is the wrong shape for a page of static prose * redrawn sixty times a second and exactly the right shape for this: one short * line, on screen only while somebody is talking. */ #include #include #include #include #include #include #include #include #include #include #include "jrpg.h" /* * Colour R G B A */ static const SDL_Color TEXTBOX_FILL = { 24, 20, 37, 235 }; static const SDL_Color TEXTBOX_EDGE = { 240, 236, 214, 255 }; static const SDL_Color TEXTBOX_INK = { 240, 236, 214, 255 }; /** @brief Pixels between the panel and the edges of the screen. */ #define TEXTBOX_MARGIN 8.0f /** @brief Panel height in pixels. Two lines of 12pt monospace, plus padding. */ #define TEXTBOX_HEIGHT 56.0f /** @brief Pixels between the panel's edge and the text inside it. */ #define TEXTBOX_PADDING 8.0f static char textbox_text[JRPG_TEXTBOX_MAX_TEXT]; static bool textbox_visible = false; bool jrpg_textbox_showing(void) { return textbox_visible; } void jrpg_textbox_close(void) { textbox_visible = false; } akerr_ErrorContext *jrpg_textbox_open(char *text) { PREPARE_ERROR(errctx); FAIL_ZERO_RETURN(errctx, text, AKERR_NULLPOINTER, "text"); // aksl_strncpy, not strncpy: this is a fixed-width field, and strncpy at // exactly the field width leaves it unterminated. Bounded at one less than // the field so an over-long line truncates rather than being refused. PASS(errctx, aksl_strncpy( textbox_text, sizeof(textbox_text), text, sizeof(textbox_text) - 1 )); textbox_visible = true; SUCCEED_RETURN(errctx); } akerr_ErrorContext *jrpg_textbox_draw(void) { PREPARE_ERROR(errctx); TTF_Font *font = NULL; SDL_FRect panel; if ( textbox_visible == false ) { SUCCEED_RETURN(errctx); } // akgl_text_rendertextat refuses the empty string -- SDL_ttf reports "Text // has zero width" and the library passes that on as AKERR_NULLPOINTER, // while akgl_text_measure accepts it. The two disagree, so anything drawing // a line that might be empty checks for it. TODO.md, "Known and still // open". if ( textbox_text[0] == '\0' ) { SUCCEED_RETURN(errctx); } // Fonts live in a registry under a name, not in an akgl type. There is no // handle to keep, and no reference counting either: whoever calls // akgl_text_unloadfont invalidates every pointer anybody else fetched. // Fetching it per frame is the cheap way to stay honest about that. font = SDL_GetPointerProperty(AKGL_REGISTRY_FONT, JRPG_FONT_NAME, NULL); FAIL_ZERO_RETURN( errctx, font, AKGL_ERR_REGISTRY, "No font registered as \"%s\"", JRPG_FONT_NAME ); // Screen coordinates, from the camera's size rather than from a second copy // of the window dimensions. akgl_render_2d_init put them there. 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) )); SUCCEED_RETURN(errctx); }