openmohaa/code/uilib/uifield.cpp
smallmodel 6fe1e86b31
Automatically scale UI elements for high resolutions
This fixes UI elements being tiny on high resolutions like 4K. Now most UI elements will scale automatically with resolutions above 1920x1080.
2024-11-30 22:40:00 +01:00

326 lines
9.9 KiB
C++

/*
===========================================================================
Copyright (C) 2015-2023 the OpenMoHAA team
This file is part of OpenMoHAA source code.
OpenMoHAA source code is free software; you can redistribute it
and/or modify it under the terms of the GNU General Public License as
published by the Free Software Foundation; either version 2 of the License,
or (at your option) any later version.
OpenMoHAA source code is distributed in the hope that it will be
useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with OpenMoHAA source code; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
===========================================================================
*/
#include "ui_local.h"
CLASS_DECLARATION(UIWidget, UIField, NULL) {
{&W_LeftMouseDown, &UIField::Pressed},
{NULL, NULL }
};
UIField::UIField()
{
AllowActivate(qtrue);
m_iPreStep = 0;
}
void UIField::Draw(void)
{
UpdateData();
// the width of the widget that can be filled with valid text
float fVirtualWidth = m_frame.size.width / getVirtualScale()[0] - m_font->getWidth(" ..", -1);
float indentLength = m_indent;
float cursorSize = m_font->getCharWidth('_');
// The total pixel length of the entire string in the editfield buffer
float totalDrawLength = m_font->getWidth(m_edit.m_buffer, -1) + indentLength;
// The index of the character that has to be shown first
// when viewing the end of the string to properly fill out the field
int iMaxPreStep;
float fEndLen = totalDrawLength + cursorSize;
int buflen = strlen(m_edit.m_buffer);
// Calculate the max prestep
for (iMaxPreStep = 0; fEndLen > fVirtualWidth && iMaxPreStep < buflen; iMaxPreStep++) {
float fCharWidth = m_font->getCharWidth(m_edit.m_buffer[iMaxPreStep]);
fEndLen -= fCharWidth;
} // fEndLen is discarded
// Index of the last currently visible character in the textfield
int iLastChar;
fEndLen = indentLength + cursorSize;
for (iLastChar = m_iPreStep; fEndLen < fVirtualWidth; iLastChar++) {
if (char c = m_edit.m_buffer[iLastChar]) {
float fCharWidth = m_font->getCharWidth(c);
fEndLen += fCharWidth;
} else {
// important to quit early!
// otherwise iLastChar would get incremented one extra time
break;
}
}
if (IsActive()) {
if (m_edit.m_cursor >= m_iPreStep) {
// Fixed in OPM
// m_iPreStep < iLastChar check added to avoid passing negative size to Q_strncpyz and crashing the game
while (iLastChar <= m_edit.m_cursor && m_iPreStep < iMaxPreStep && m_iPreStep < iLastChar
) { // is text too long to fit?
fEndLen -= m_font->getCharWidth(m_edit.m_buffer[m_iPreStep]);
m_iPreStep++;
// FIXME: repetitive logic
if (char c = m_edit.m_buffer[iLastChar]) {
float fCharWidth = m_font->getCharWidth(c);
fEndLen += fCharWidth;
} else {
break;
}
while (fEndLen < fVirtualWidth) {
iLastChar++;
if (char c = m_edit.m_buffer[iLastChar]) {
float fCharWidth = m_font->getCharWidth(c);
fEndLen += fCharWidth;
} else {
break;
}
}
}
} else {
m_iPreStep = m_edit.m_cursor;
}
m_iPreStep = Q_min(m_iPreStep, iMaxPreStep);
}
// Put the portion of the text intended for display into the temptext buffer
char temptext[EDITFIELD_BUFSIZE];
Q_strncpyz(temptext, &m_edit.m_buffer[m_iPreStep], iLastChar - m_iPreStep + 1);
// Calculate cursor position
float cursorPos = indentLength - 1.0f;
// Added in OPM
// new i < EDITFIELD_BUFSIZE check for extra safety
for (int i = m_iPreStep; i < m_edit.m_cursor && i < EDITFIELD_BUFSIZE; i++) {
float fCharWidth = m_font->getCharWidth(m_edit.m_buffer[i]);
cursorPos += fCharWidth;
}
cursorPos = Q_min(cursorPos, fVirtualWidth);
// Calculate text height
float y = m_frame.size.height / getVirtualScale()[1] - m_font->getHeight();
if (m_bottomindent * 2 <= y) // upper and lower margin
{
y -= m_bottomindent;
} else {
y /= 2;
}
// Show text
m_font->setColor(m_foreground_color);
m_font->Print(m_indent, y, temptext, -1, getVirtualScale());
// Display blinking cursor if field is in focus
if (IsActive()) {
const char *cursorChar = (uid.time / 250) & 1 ? "|" : "_";
m_font->Print(cursorPos, y, cursorChar, -1, getVirtualScale());
}
Q_strncpyz(temptext, "..", EDITFIELD_BUFSIZE);
if (m_iPreStep) {
// Display leading dots if not starting from the beginning
m_font->Print(2.0f, y, temptext, -1, getVirtualScale());
}
// Added in OPM
// new iLastChar < EDITFIELD_BUFSIZE check for extra safety
if (iLastChar < EDITFIELD_BUFSIZE && m_edit.m_buffer[iLastChar]) {
// Display trailing dots if not ending at the end
fVirtualWidth = m_frame.size.width / getVirtualScale()[0] - m_font->getWidth(temptext, -1) - 2.0f;
m_font->Print(fVirtualWidth, y, temptext, -1, getVirtualScale());
}
}
qboolean UIField::KeyEvent(int key, unsigned int time)
{
if (key == 'l' && uii.Sys_IsKeyDown(K_CTRL)) {
return qtrue;
}
if (key == K_ENTER || key == K_KP_ENTER) {
if (m_cvarname.length()) {
uii.Cvar_Set(m_cvarname, m_edit.m_buffer);
}
if (m_commandhandler) {
m_commandhandler(str(m_edit.m_buffer) + '\n', NULL);
}
if (m_command.length()) {
str result = m_command + ' ' + m_edit.m_buffer + '\n';
Cbuf_AddText(result);
}
return qtrue;
}
if ((key == K_INS || key == K_KP_INS) && uii.Sys_IsKeyDown(K_SHIFT)) {
return qtrue;
}
int len = strlen(m_edit.m_buffer);
switch (key) {
case K_DEL: // DELETE key, backspace is handled in CharEvent
if (m_edit.m_cursor < len) {
memmove(&m_edit.m_buffer[m_edit.m_cursor], &m_edit.m_buffer[m_edit.m_cursor + 1], len - m_edit.m_cursor);
if (m_cvarname.length()) {
uii.Cvar_Set(m_cvarname, m_edit.m_buffer);
}
}
return qtrue;
case K_RIGHTARROW:
if (m_edit.m_cursor < len) {
m_edit.m_cursor++;
}
return qtrue;
case K_LEFTARROW:
if (m_edit.m_cursor > 0) {
m_edit.m_cursor--;
}
return qtrue;
}
// HOME or Ctrl + A
if (key == K_HOME || (tolower(key) == 'a' && uii.Sys_IsKeyDown(K_CTRL))) {
m_edit.m_cursor = 0;
return qtrue;
}
// END or Ctrl + E
if (key == K_END || (tolower(key) == 'e' && uii.Sys_IsKeyDown(K_CTRL))) {
m_edit.m_cursor = len;
return qtrue;
}
return key == K_INS;
}
void UIField::CharEvent(int ch)
{
uii.Snd_PlaySound("sound/menu/typekey.wav");
if (ch == 3 || ch == 22) // Ctrl + C (^C) or Ctrl + V (^V)
{
return;
}
int len = strlen(m_edit.m_buffer);
switch (ch) {
case '\b': // backspace or Ctrl + H (^H) - DEL is handled in KeyEvent
if (m_edit.m_cursor <= 0) {
// already at beginning of field
return;
}
// Move all text after the cursor one position back
memmove(&m_edit.m_buffer[m_edit.m_cursor - 1], &m_edit.m_buffer[m_edit.m_cursor], len + 1 - m_edit.m_cursor);
// move back cursor by one
m_edit.m_cursor--;
if (m_cvarname.length()) {
uii.Cvar_Set(m_cvarname, m_edit.m_buffer);
}
return;
case 1: // Home (^A)
m_edit.m_cursor = 0;
return;
case 5: // End (^E)
m_edit.m_cursor = len;
return;
}
// original check only does ch < 32,
// this one excludes DEL too but it shouldn't be a problem,
// DEL is handled in KeyEvent
if (iscntrl(ch)) {
return;
}
len++; // new printable character was typed
// Fixed in OPM
// originally this was len == EDITFIELD_BUFSIZE, just being extra safe
if (len >= EDITFIELD_BUFSIZE) // is it out of bounds?
{
return;
}
// make room for the character in the buffer
memmove(&m_edit.m_buffer[m_edit.m_cursor + 1], &m_edit.m_buffer[m_edit.m_cursor], len - m_edit.m_cursor);
// insert the new character at the cursor's location
m_edit.m_buffer[m_edit.m_cursor] = ch;
// advance the cursor
m_edit.m_cursor++;
// if the character was inserted at the end of the string,
// make sure it remains NULL-terminated
if (m_edit.m_cursor == len) {
m_edit.m_buffer[m_edit.m_cursor] = 0;
}
if (m_cvarname.length()) {
uii.Cvar_Set(m_cvarname, m_edit.m_buffer);
}
}
void UIField::UpdateData(void)
{
if (m_cvarname.length()) {
str::snprintf(m_edit.m_buffer, EDITFIELD_BUFSIZE, "%s", CvarGetForUI(m_cvarname, ""));
m_edit.m_cursor = Q_min(m_edit.m_cursor, strlen(m_edit.m_buffer));
}
}
void UIField::Pressed(Event *ev)
{
float xpos = ev->GetFloat(1);
float point = (xpos - m_frame.pos.x) / getVirtualScale()[0];
point = Q_max(point, 0.0f); // added in 2.15
int iStep = m_iPreStep;
float fStringWidth = m_indent;
while (char c = m_edit.m_buffer[iStep]) {
iStep++;
fStringWidth += m_font->getCharWidth(c);
if (point <= fStringWidth) {
if (iStep > 0) {
iStep--; // put the cursor BEFORE the clicked character
}
break;
}
}
m_edit.m_cursor = iStep;
}