ui2: port examine item dialog

This commit is contained in:
Marcin Kurczewski 2025-04-14 22:22:45 +02:00
parent c4ef88a058
commit 238fd19e4f
14 changed files with 208 additions and 323 deletions

View file

@ -601,6 +601,7 @@
"OSD_UNKNOWN_COMMAND": "Unknown command: %s",
"OSD_WIREFRAME_MODE_OFF": "Wireframe mode: off",
"OSD_WIREFRAME_MODE_ON": "Wireframe mode: on",
"PAGINATION_NAV": "%d / %d",
"PASSPORT_EXIT_DEMO": "Exit Demo",
"PASSPORT_EXIT_GAME": "Exit Game",
"PASSPORT_EXIT_TO_TITLE": "Exit to Title",

View file

@ -1,5 +1,6 @@
#include "game/ui/widgets/frame.h"
#include "game/text.h"
#include "game/ui/widgets/spacer.h"
#include "memory.h"
typedef struct {

View file

@ -1,57 +0,0 @@
#include "game/ui/widgets/spacer.h"
#include "memory.h"
typedef struct {
UI_WIDGET_VTABLE vtable;
int32_t width;
int32_t height;
} UI_SPACER;
static int32_t M_GetWidth(const UI_SPACER *self);
static int32_t M_GetHeight(const UI_SPACER *self);
static void M_SetPosition(UI_SPACER *self, int32_t x, int32_t y);
static void M_Control(UI_SPACER *self);
static void M_Free(UI_SPACER *self);
static int32_t M_GetWidth(const UI_SPACER *const self)
{
if (self->vtable.is_hidden) {
return 0;
}
return self->width;
}
static int32_t M_GetHeight(const UI_SPACER *const self)
{
if (self->vtable.is_hidden) {
return 0;
}
return self->height;
}
static void M_SetPosition(
UI_SPACER *const self, const int32_t x, const int32_t y)
{
}
static void M_Free(UI_SPACER *const self)
{
Memory_Free(self);
}
UI_WIDGET *UI_Spacer_Create(const int32_t width, const int32_t height)
{
UI_SPACER *const self = Memory_Alloc(sizeof(UI_SPACER));
self->vtable = (UI_WIDGET_VTABLE) {
.control = nullptr,
.draw = nullptr,
.get_width = (UI_WIDGET_GET_WIDTH)M_GetWidth,
.get_height = (UI_WIDGET_GET_HEIGHT)M_GetHeight,
.set_position = (UI_WIDGET_SET_POSITION)M_SetPosition,
.free = (UI_WIDGET_FREE)M_Free,
};
self->width = width;
self->height = height;
return (UI_WIDGET *)self;
}

View file

@ -0,0 +1,144 @@
#include "game/ui2/dialogs/examine_item.h"
#include "game/game_string.h"
#include "game/input.h"
#include "game/sound.h"
#include "game/text.h"
#include "game/ui2/elements/anchor.h"
#include "game/ui2/elements/frame.h"
#include "game/ui2/elements/label.h"
#include "game/ui2/elements/modal.h"
#include "game/ui2/elements/pad.h"
#include "game/ui2/elements/resize.h"
#include "game/ui2/elements/spacer.h"
#include "game/ui2/elements/stack.h"
#include "memory.h"
#include "strings.h"
#include "utils.h"
#include <stdio.h>
#define TITLE_MARGIN 5.0f
#define WINDOW_MARGIN 10.0f
#define DIALOG_PADDING 8.0f
#define PADDING_SCALED (3.5f * (DIALOG_PADDING + WINDOW_MARGIN))
static bool M_SelectPage(UI2_EXAMINE_ITEM_STATE *state, int32_t new_page);
static bool M_SelectPage(
UI2_EXAMINE_ITEM_STATE *const state, const int32_t new_page)
{
if (new_page == state->current_page || new_page < 0
|| new_page >= state->page_content->count) {
return false;
}
state->current_page = new_page;
return true;
}
void UI2_ExamineItem_Init(
UI2_EXAMINE_ITEM_STATE *const state, const char *const title,
const char *const text, size_t max_lines)
{
state->current_page = 0;
state->title = String_ToUpper(title);
const char *wrapped =
String_WordWrap(text, Text_GetMaxLineLength() - PADDING_SCALED);
state->page_content = String_Paginate(wrapped, max_lines);
state->is_empty = String_IsEmpty(text);
Memory_FreePointer(&wrapped);
// Compute actual maximum number of lines on any single page, which can be
// less than the line cap.
size_t max_vis_lines = 0;
for (int32_t i = 0; i < state->page_content->count; i++) {
size_t page_lines = 1;
const char *c = *(char **)Vector_Get(state->page_content, i);
while (*c != '\0') {
page_lines += *c++ == '\n';
}
CLAMPL(max_vis_lines, page_lines);
}
state->max_lines = max_vis_lines;
}
void UI2_ExamineItem_Control(UI2_EXAMINE_ITEM_STATE *const state)
{
const int32_t page_shift =
g_InputDB.menu_left ? -1 : (g_InputDB.menu_right ? 1 : 0);
if (M_SelectPage(state, state->current_page + page_shift)) {
Sound_Effect(SFX_MENU_PASSPORT, nullptr, SPM_ALWAYS);
}
}
void UI2_ExamineItem_Free(UI2_EXAMINE_ITEM_STATE *const state)
{
Memory_FreePointer(&state->title);
for (int32_t i = state->page_content->count - 1; i >= 0; i--) {
char *const page = *(char **)Vector_Get(state->page_content, i);
Memory_Free(page);
}
Vector_Free(state->page_content);
}
void UI2_ExamineItem(UI2_EXAMINE_ITEM_STATE *const state)
{
if (state->is_empty) {
return;
}
UI2_BeginModal(0.5f, 0.5f);
UI2_BeginFrame(UI2_FRAME_DIALOG_BACKGROUND);
UI2_BeginPad(DIALOG_PADDING, DIALOG_PADDING);
UI2_BeginStackEx((UI2_STACK_SETTINGS) {
.orientation = UI2_STACK_VERTICAL,
.align = { .h = UI2_STACK_H_ALIGN_SPAN },
});
UI2_BeginAnchor(0.5f, 0.5f);
UI2_Label(state->title);
UI2_EndAnchor();
UI2_Spacer(TITLE_MARGIN, TITLE_MARGIN);
for (int32_t i = 0; i < state->page_content->count; i++) {
if (i != state->current_page) {
UI2_BeginResize(-1.0f, 0.0f);
} else if (state->page_content->count == 1) {
UI2_BeginResize(-1.0f, -1.0f);
} else {
UI2_BeginResize(-1.0f, TEXT_HEIGHT_FIXED * state->max_lines);
}
UI2_Label(*(char **)Vector_Get(state->page_content, i));
UI2_EndResize();
}
if (state->page_content->count > 1) {
UI2_Spacer(TITLE_MARGIN, TITLE_MARGIN * 3);
UI2_BeginAnchor(1.0f, 0.5f);
UI2_BeginStack(UI2_STACK_HORIZONTAL);
if (state->current_page > 0) {
UI2_Label("\\{button left} ");
}
char page_indicator[100];
sprintf(
page_indicator, GS(PAGINATION_NAV), state->current_page + 1,
state->page_content->count);
UI2_Label(page_indicator);
if (state->current_page < state->page_content->count - 1) {
UI2_Label(" \\{button right}");
}
UI2_EndStack();
UI2_EndAnchor();
}
UI2_EndStack();
UI2_EndPad();
UI2_EndFrame();
UI2_EndModal();
}

View file

@ -140,3 +140,4 @@ GS_DEFINE(DETAIL_FOG_END, "Fog end")
GS_DEFINE(DETAIL_WATER_COLOR_R, "Water color (R)")
GS_DEFINE(DETAIL_WATER_COLOR_G, "Water color (G)")
GS_DEFINE(DETAIL_WATER_COLOR_B, "Water color (B)")
GS_DEFINE(PAGINATION_NAV, "%d / %d")

View file

@ -1,5 +0,0 @@
#pragma once
#include "./base.h"
UI_WIDGET *UI_Spacer_Create(int32_t width, int32_t height);

View file

@ -1,6 +1,7 @@
#pragma once
#include "./ui2/common.h"
#include "./ui2/dialogs/examine_item.h"
#include "./ui2/dialogs/photo_mode.h"
#include "./ui2/elements/anchor.h"
#include "./ui2/elements/fade.h"

View file

@ -0,0 +1,22 @@
#pragma once
#include "../../../vector.h"
#include "../common.h"
// A widget to cycle through several pages of a text content.
typedef struct {
char *title;
size_t max_lines;
int32_t current_page;
VECTOR *page_content;
bool is_empty;
} UI2_EXAMINE_ITEM_STATE;
void UI2_ExamineItem_Init(
UI2_EXAMINE_ITEM_STATE *state, const char *title, const char *text,
size_t max_lines);
void UI2_ExamineItem_Control(UI2_EXAMINE_ITEM_STATE *state);
void UI2_ExamineItem_Free(UI2_EXAMINE_ITEM_STATE *state);
void UI2_ExamineItem(UI2_EXAMINE_ITEM_STATE *state);

View file

@ -206,10 +206,10 @@ sources = [
'game/ui/widgets/frame.c',
'game/ui/widgets/label.c',
'game/ui/widgets/requester.c',
'game/ui/widgets/spacer.c',
'game/ui/widgets/stack.c',
'game/ui/widgets/window.c',
'game/ui2/common.c',
'game/ui2/dialogs/examine_item.c',
'game/ui2/dialogs/photo_mode.c',
'game/ui2/elements/anchor.c',
'game/ui2/elements/fade.c',

View file

@ -57,4 +57,3 @@ GS_DEFINE(OSD_DOOR_CLOSE, "Close Sesame!")
GS_DEFINE(OSD_DOOR_OPEN_FAIL, "No doors in Lara's proximity")
GS_DEFINE(ITEM_EXAMINE_ROLE, "\\{button empty} %s: Examine")
GS_DEFINE(ITEM_USE_ROLE, "\\{button empty} %s: Use")
GS_DEFINE(PAGINATION_NAV, "%d / %d")

View file

@ -1,21 +1,38 @@
#include "game/option/option_examine.h"
#include "game/input.h"
#include "game/ui/widgets/paginator.h"
#include <libtrx/game/objects/names.h>
#include <libtrx/game/ui/common.h>
#include <libtrx/game/ui2.h>
#define MAX_LINES 10
static UI_WIDGET *m_PaginatorUI = nullptr;
typedef struct {
struct {
bool is_ready;
UI2_EXAMINE_ITEM_STATE state;
} ui;
} M_PRIV;
static void M_End(void);
static M_PRIV m_Priv = {};
static void M_End(void)
static void M_Init(M_PRIV *p, GAME_OBJECT_ID obj_id);
static void M_Shutdown(M_PRIV *p);
static void M_Init(M_PRIV *const p, const GAME_OBJECT_ID obj_id)
{
m_PaginatorUI->free(m_PaginatorUI);
m_PaginatorUI = nullptr;
p->ui.is_ready = true;
UI2_ExamineItem_Init(
&p->ui.state, Object_GetName(obj_id), Object_GetDescription(obj_id),
MAX_LINES);
}
static void M_Shutdown(M_PRIV *const p)
{
if (p->ui.is_ready) {
UI2_ExamineItem_Free(&p->ui.state);
p->ui.is_ready = false;
}
}
bool Option_Examine_CanExamine(const GAME_OBJECT_ID obj_id)
@ -25,37 +42,37 @@ bool Option_Examine_CanExamine(const GAME_OBJECT_ID obj_id)
bool Option_Examine_IsActive(void)
{
return m_PaginatorUI != nullptr;
const M_PRIV *const p = &m_Priv;
return p->ui.is_ready;
}
void Option_Examine_Control(const GAME_OBJECT_ID obj_id, const bool is_busy)
{
M_PRIV *const p = &m_Priv;
if (is_busy) {
return;
}
if (m_PaginatorUI == nullptr) {
m_PaginatorUI = UI_Paginator_Create(
Object_GetName(obj_id), Object_GetDescription(obj_id), MAX_LINES);
if (!p->ui.is_ready) {
M_Init(p, obj_id);
}
m_PaginatorUI->control(m_PaginatorUI);
UI2_ExamineItem_Control(&p->ui.state);
if (g_InputDB.menu_back || g_InputDB.menu_confirm) {
M_End();
M_Shutdown(p);
}
}
void Option_Examine_Draw(void)
{
if (m_PaginatorUI != nullptr) {
m_PaginatorUI->draw(m_PaginatorUI);
M_PRIV *const p = &m_Priv;
if (p->ui.is_ready) {
UI2_ExamineItem(&p->ui.state);
}
}
void Option_Examine_Shutdown(void)
{
if (m_PaginatorUI != nullptr) {
M_End();
}
M_PRIV *const p = &m_Priv;
M_Shutdown(p);
}

View file

@ -1,232 +0,0 @@
#include "game/ui/widgets/paginator.h"
#include "game/game_string.h"
#include "game/input.h"
#include "game/screen.h"
#include "game/sound.h"
#include "game/text.h"
#include <libtrx/game/ui/common.h>
#include <libtrx/game/ui/widgets/label.h>
#include <libtrx/game/ui/widgets/spacer.h>
#include <libtrx/game/ui/widgets/stack.h>
#include <libtrx/game/ui/widgets/window.h>
#include <libtrx/memory.h>
#include <libtrx/strings/common.h>
#include <stdio.h>
#define TITLE_MARGIN 5
#define WINDOW_MARGIN 10
#define DIALOG_PADDING 5
#define PADDING_SCALED (3.5 * (DIALOG_PADDING + WINDOW_MARGIN))
typedef struct {
UI_WIDGET_VTABLE vtable;
UI_WIDGET *window;
UI_WIDGET *outer_stack;
UI_WIDGET *bottom_stack;
UI_WIDGET *title;
UI_WIDGET *top_spacer;
UI_WIDGET *text;
UI_WIDGET *bottom_spacer;
UI_WIDGET *left_arrow;
UI_WIDGET *right_arrow;
UI_WIDGET *right_arrow_spacer;
UI_WIDGET *page_label;
int32_t current_page;
VECTOR *page_content;
bool shown;
} UI_PAGINATOR;
static void M_DoLayout(UI_PAGINATOR *self);
static int32_t M_GetWidth(const UI_PAGINATOR *self);
static int32_t M_GetHeight(const UI_PAGINATOR *self);
static void M_SetPosition(UI_PAGINATOR *self, int32_t x, int32_t y);
static bool M_SelectPage(UI_PAGINATOR *const self, int32_t new_page);
static void M_Control(UI_PAGINATOR *self);
static void M_Draw(UI_PAGINATOR *self);
static void M_Free(UI_PAGINATOR *self);
static void M_DoLayout(UI_PAGINATOR *const self)
{
M_SetPosition(
self, (Screen_GetResWidthDownscaled(RSR_TEXT) - M_GetWidth(self)) / 2.0,
(Screen_GetResHeightDownscaled(RSR_TEXT) - M_GetHeight(self)) / 2.0);
}
static int32_t M_GetWidth(const UI_PAGINATOR *const self)
{
return self->window->get_width(self->window);
}
static int32_t M_GetHeight(const UI_PAGINATOR *const self)
{
return self->window->get_height(self->window);
}
static void M_SetPosition(UI_PAGINATOR *const self, int32_t x, int32_t y)
{
self->window->set_position(self->window, x, y);
}
static bool M_SelectPage(UI_PAGINATOR *const self, const int32_t new_page)
{
if (new_page == self->current_page || new_page < 0
|| new_page >= self->page_content->count) {
return false;
}
self->current_page = new_page;
UI_Label_ChangeText(
self->text,
*(char **)Vector_Get(self->page_content, self->current_page));
char page_indicator[100];
sprintf(
page_indicator, GS(PAGINATION_NAV), self->current_page + 1,
self->page_content->count);
UI_Label_ChangeText(self->page_label, page_indicator);
UI_Label_SetVisible(self->left_arrow, self->current_page > 0);
UI_Label_SetVisible(
self->right_arrow, self->current_page < self->page_content->count - 1);
return true;
}
static void M_Control(UI_PAGINATOR *const self)
{
const int32_t page_shift =
g_InputDB.menu_left ? -1 : (g_InputDB.menu_right ? 1 : 0);
if (M_SelectPage(self, self->current_page + page_shift)) {
Sound_Effect(SFX_MENU_PASSPORT, nullptr, SPM_ALWAYS);
}
if (self->window->control != nullptr) {
self->window->control(self->window);
}
}
static void M_Draw(UI_PAGINATOR *const self)
{
if (self->shown && self->window->draw != nullptr) {
self->window->draw(self->window);
}
}
static void M_Free(UI_PAGINATOR *const self)
{
for (int32_t i = self->page_content->count - 1; i >= 0; i--) {
char *const page = *(char **)Vector_Get(self->page_content, i);
Memory_Free(page);
}
Vector_Free(self->page_content);
self->text->free(self->text);
self->top_spacer->free(self->top_spacer);
self->title->free(self->title);
if (self->bottom_stack != nullptr) {
self->bottom_spacer->free(self->bottom_spacer);
self->left_arrow->free(self->left_arrow);
self->right_arrow->free(self->right_arrow);
self->right_arrow_spacer->free(self->right_arrow_spacer);
self->page_label->free(self->page_label);
self->bottom_stack->free(self->bottom_stack);
}
self->outer_stack->free(self->outer_stack);
self->window->free(self->window);
Memory_Free(self);
}
UI_WIDGET *UI_Paginator_Create(
const char *const title, const char *const text, const int32_t max_lines)
{
UI_PAGINATOR *const self = Memory_Alloc(sizeof(UI_PAGINATOR));
self->vtable = (UI_WIDGET_VTABLE) {
.control = (UI_WIDGET_CONTROL)M_Control,
.draw = (UI_WIDGET_DRAW)M_Draw,
.get_width = (UI_WIDGET_GET_WIDTH)M_GetWidth,
.get_height = (UI_WIDGET_GET_HEIGHT)M_GetHeight,
.set_position = (UI_WIDGET_SET_POSITION)M_SetPosition,
.free = (UI_WIDGET_FREE)M_Free,
};
self->shown = !String_IsEmpty(text);
self->outer_stack = UI_Stack_Create(
UI_STACK_LAYOUT_VERTICAL, UI_STACK_AUTO_SIZE, UI_STACK_AUTO_SIZE);
const char *upper_title = String_ToUpper(title);
self->title =
UI_Label_Create(upper_title, UI_LABEL_AUTO_SIZE, TEXT_HEIGHT_FIXED);
Memory_FreePointer(&upper_title);
UI_Stack_AddChild(self->outer_stack, self->title);
self->top_spacer = UI_Spacer_Create(TITLE_MARGIN, TITLE_MARGIN);
UI_Stack_AddChild(self->outer_stack, self->top_spacer);
const char *wrapped =
String_WordWrap(text, Text_GetMaxLineLength() - PADDING_SCALED);
self->page_content = String_Paginate(wrapped, max_lines);
self->current_page = 0;
Memory_FreePointer(&wrapped);
self->text = UI_Label_Create(
*(char **)Vector_Get(self->page_content, 0), UI_LABEL_AUTO_SIZE,
UI_LABEL_AUTO_SIZE);
UI_Stack_AddChild(self->outer_stack, self->text);
if (self->page_content->count > 1) {
self->bottom_spacer = UI_Spacer_Create(TITLE_MARGIN, TITLE_MARGIN * 3);
UI_Stack_AddChild(self->outer_stack, self->bottom_spacer);
self->bottom_stack = UI_Stack_Create(
UI_STACK_LAYOUT_HORIZONTAL, UI_STACK_AUTO_SIZE, UI_STACK_AUTO_SIZE);
UI_Stack_AddChild(self->outer_stack, self->bottom_stack);
self->left_arrow =
UI_Label_Create("\\{button left}", 22, TEXT_HEIGHT_FIXED);
self->right_arrow =
UI_Label_Create("\\{button right}", 16, TEXT_HEIGHT_FIXED);
self->right_arrow_spacer = UI_Spacer_Create(6, TEXT_HEIGHT_FIXED);
self->page_label =
UI_Label_Create("", UI_LABEL_AUTO_SIZE, UI_LABEL_AUTO_SIZE);
UI_Stack_AddChild(self->bottom_stack, self->left_arrow);
UI_Stack_AddChild(self->bottom_stack, self->page_label);
UI_Stack_AddChild(self->bottom_stack, self->right_arrow_spacer);
UI_Stack_AddChild(self->bottom_stack, self->right_arrow);
UI_Stack_SetHAlign(self->bottom_stack, UI_STACK_H_ALIGN_RIGHT);
}
self->window = UI_Window_Create(
self->outer_stack, DIALOG_PADDING, DIALOG_PADDING, DIALOG_PADDING * 2,
DIALOG_PADDING);
// Ensure minimum width for page navigation as text content may be empty.
int32_t max_width =
MAX(self->page_content->count == 1 ? 0 : 100,
UI_Label_MeasureTextWidth(self->title));
int32_t max_nav_width = 0;
int32_t max_height = 0;
for (int32_t i = 0; i < self->page_content->count; i++) {
M_SelectPage(self, i);
max_width = MAX(max_width, UI_Label_MeasureTextWidth(self->text));
max_height = MAX(max_height, UI_Label_MeasureTextHeight(self->text));
if (self->bottom_stack != nullptr) {
max_nav_width =
MAX(max_nav_width, UI_Label_MeasureTextWidth(self->page_label));
}
}
UI_Label_SetSize(self->text, max_width, max_height);
if (self->bottom_stack != nullptr) {
UI_Stack_SetSize(self->bottom_stack, max_width, UI_STACK_AUTO_SIZE);
UI_Label_SetSize(self->page_label, max_nav_width, UI_LABEL_AUTO_SIZE);
}
M_SelectPage(self, 0);
M_DoLayout(self);
return (UI_WIDGET *)self;
}

View file

@ -1,6 +0,0 @@
#pragma once
#include <libtrx/game/ui/widgets/base.h>
UI_WIDGET *UI_Paginator_Create(
const char *title, const char *text, int32_t max_lines);

View file

@ -264,7 +264,6 @@ sources = [
'game/text.c',
'game/ui/common.c',
'game/ui/widgets/stats_dialog.c',
'game/ui/widgets/paginator.c',
'game/viewport.c',
'global/enum_map.c',
'global/vars.c',