fix: Fix rendering of multiline listbox items

This commit is contained in:
Vsevolod Kremianskii 2021-01-28 15:49:18 +07:00
parent 2c6955bd22
commit 3bc66751f7
15 changed files with 268 additions and 84 deletions

View file

@ -171,6 +171,7 @@ set(RENDER_HEADERS
src/render/texture.h
src/render/textures.h
src/render/textureutil.h
src/render/textutil.h
src/render/types.h
src/render/walkmesh.h
src/render/walkmeshes.h
@ -204,6 +205,7 @@ set(RENDER_SOURCES
src/render/texture.cpp
src/render/textures.cpp
src/render/textureutil.cpp
src/render/textutil.cpp
src/render/walkmesh.cpp
src/render/walkmeshes.cpp
src/render/window.cpp)

View file

@ -52,6 +52,9 @@ void QuickOrCustom::load() {
if (_version == GameVersion::KotOR) {
setControlDiscardColor("LBL_RBG", glm::vec3(0.0f, 0.0f, 0.082353f));
}
auto &lbDesc = getControl<ListBox>("LB_DESC");
lbDesc.setProtoMatchContent(true);
}
void QuickOrCustom::onClick(const string &control) {
@ -79,7 +82,7 @@ void QuickOrCustom::onFocusChanged(const string &control, bool focus) {
ListBox::Item item;
item.text = text;
ListBox &lbDesc = getControl<ListBox>("LB_DESC");
auto &lbDesc = getControl<ListBox>("LB_DESC");
lbDesc.clearItems();
lbDesc.addItem(move(item));
}

View file

@ -53,6 +53,8 @@ void ComputerGUI::load() {
void ComputerGUI::configureMessage() {
auto &message = getControl<ListBox>(kControlTagMessage);
message.setProtoMatchContent(true);
Control &protoItem = message.protoItem();
protoItem.setHilightColor(getHilightColor(_version));
protoItem.setTextColor(getBaseColor(_version));
@ -60,6 +62,8 @@ void ComputerGUI::configureMessage() {
void ComputerGUI::configureReplies() {
auto &replies = getControl<ListBox>(kControlTagReplies);
replies.setProtoMatchContent(true);
Control &protoItem = replies.protoItem();
protoItem.setHilightColor(getHilightColor(_version));
protoItem.setTextColor(getBaseColor(_version));

View file

@ -145,6 +145,8 @@ void DialogGUI::configureMessage() {
void DialogGUI::configureReplies() {
auto &replies = getControl<ListBox>(kControlTagReplies);
replies.setProtoMatchContent(true);
Control &protoItem = replies.protoItem();
protoItem.setHilightColor(getHilightColor(_version));
protoItem.setTextColor(getBaseColor(_version));

View file

@ -27,6 +27,7 @@
#include "../../render/shaders.h"
#include "../../render/stateutil.h"
#include "../../render/textures.h"
#include "../../render/textutil.h"
#include "../../resource/resources.h"
#include "../gui.h"
@ -165,6 +166,15 @@ void Control::loadText(const GffStruct &gffs) {
_text.color = gffs.getVector("COLOR");
_text.align = static_cast<TextAlign>(gffs.getInt("ALIGNMENT", static_cast<int>(TextAlign::CenterCenter)));
updateTextLines();
}
void Control::updateTextLines() {
_textLines.clear();
if (_text.font && !_text.text.empty()) {
_textLines = breakText(_text.text, *_text.font, _extent.width);
}
}
void Control::loadHilight(const GffStruct &gffs) {
@ -213,7 +223,7 @@ void Control::update(float dt) {
}
}
void Control::render(const glm::ivec2 &offset, const string &textOverride) const {
void Control::render(const glm::ivec2 &offset, const vector<string> &text) const {
if (!_visible) return;
glm::ivec2 size(_extent.width, _extent.height);
@ -223,8 +233,7 @@ void Control::render(const glm::ivec2 &offset, const string &textOverride) const
} else if (_border) {
drawBorder(*_border, offset, size);
}
if (!textOverride.empty() || !_text.text.empty()) {
string text(!textOverride.empty() ? textOverride : _text.text);
if (!text.empty()) {
drawText(text, offset, size);
}
}
@ -411,56 +420,22 @@ const glm::vec3 &Control::getBorderColor() const {
return (_focus && _hilight) ? _hilight->color : _border->color;
}
void Control::drawText(const string &text, const glm::ivec2 &offset, const glm::ivec2 &size) const {
void Control::drawText(const vector<string> &lines, const glm::ivec2 &offset, const glm::ivec2 &size) const {
glm::ivec2 position;
TextGravity gravity;
glm::vec3 color((_focus && _hilight) ? _hilight->color : _text.color);
vector<string> lines(breakText(text, size.x));
getTextPosition(position, static_cast<int>(lines.size()), size, gravity);
glm::vec3 color((_focus && _hilight) ? _hilight->color : _text.color);
for (auto &line : lines) {
glm::mat4 transform(glm::translate(glm::mat4(1.0f), glm::vec3(position.x + offset.x, position.y + offset.y, 0.0f)));
glm::mat4 transform(1.0f);
transform = glm::translate(transform, glm::vec3(position.x + offset.x, position.y + offset.y, 0.0f));
_text.font->render(line, transform, color, gravity);
position.y += static_cast<int>(_text.font->height());
}
}
vector<string> Control::breakText(const string &text, int maxWidth) const {
vector<string> fixedLines;
boost::split(fixedLines, text, boost::is_any_of("\n"));
vector<string> result;
for (auto &fixedLine : fixedLines) {
vector<string> tokens;
boost::split(tokens, fixedLine, boost::is_space(), boost::token_compress_on);
string buffer;
for (auto &token : tokens) {
string test(buffer);
if (!test.empty()) {
test += " ";
}
test += token;
if (_text.font->measure(test) < maxWidth) {
buffer = move(test);
} else {
result.push_back(buffer);
buffer = token;
}
}
if (!buffer.empty()) {
boost::trim_right(buffer);
result.push_back(move(buffer));
}
}
return move(result);
}
void Control::getTextPosition(glm::ivec2 &position, int lineCount, const glm::ivec2 &size, TextGravity &gravity) const {
// Gravity
switch (_text.align) {
@ -571,6 +546,12 @@ void Control::setFocusable(bool focusable) {
_focusable = focusable;
}
void Control::setHeight(int height) {
_extent.height = height;
updateTransform();
updateTextLines();
}
void Control::setVisible(bool visible) {
_visible = visible;
}
@ -586,6 +567,7 @@ void Control::setFocus(bool focus) {
void Control::setExtent(const Extent &extent) {
_extent = extent;
updateTransform();
updateTextLines();
}
void Control::setBorder(const Border &border) {
@ -638,14 +620,17 @@ void Control::setHilightColor(const glm::vec3 &color) {
void Control::setText(const Text &text) {
_text = text;
updateTextLines();
}
void Control::setTextMessage(const string &text) {
_text.text = text;
updateTextLines();
}
void Control::setTextFont(const shared_ptr<Font> &font) {
_text.font = font;
updateTextLines();
}
void Control::setTextColor(const glm::vec3 &color) {

View file

@ -118,6 +118,7 @@ public:
const Border &hilight() const;
const std::string &tag() const;
const Text &text() const;
const std::vector<std::string> &textLines() const { return _textLines; }
void setBorder(const Border &border);
void setBorderFill(const std::string &resRef);
@ -129,6 +130,7 @@ public:
virtual void setExtent(const Extent &extent);
virtual void setFocus(bool focus);
void setFocusable(bool focusable);
void setHeight(int height);
void setHilight(const Border &hilight);
void setHilightColor(const glm::vec3 &color);
void setPadding(int padding);
@ -150,7 +152,7 @@ public:
// Rendering
virtual void render(const glm::ivec2 &offset, const std::string &textOverride = "") const;
virtual void render(const glm::ivec2 &offset, const std::vector<std::string> &text) const;
void render3D(const glm::ivec2 &offset) const;
// END Rendering
@ -176,11 +178,12 @@ protected:
glm::vec3 _discardColor { false };
glm::vec3 _borderColorOverride { 1.0f };
bool _useBorderColorOverride { false };
std::vector<std::string> _textLines;
Control(GUI *, ControlType type);
void drawBorder(const Border &border, const glm::ivec2 &offset, const glm::ivec2 &size) const;
void drawText(const std::string &text, const glm::ivec2 &offset, const glm::ivec2 &size) const;
void drawText(const std::vector<std::string> &lines, const glm::ivec2 &offset, const glm::ivec2 &size) const;
virtual const glm::vec3 &getBorderColor() const;
@ -190,12 +193,13 @@ private:
Control(const Control &) = delete;
Control &operator=(const Control &) = delete;
void updateTransform();
void loadExtent(const resource::GffStruct &gffs);
void loadBorder(const resource::GffStruct &gffs);
void loadText(const resource::GffStruct &gffs);
void loadHilight(const resource::GffStruct &gffs);
std::vector<std::string> breakText(const std::string &text, int maxWidth) const;
void updateTransform();
void updateTextLines();
void getTextPosition(glm::ivec2 &position, int lineCount, const glm::ivec2 &size, render::TextGravity &gravity) const;
};

View file

@ -43,7 +43,7 @@ void ImageButton::load(const GffStruct &gffs) {
void ImageButton::render(
const glm::ivec2 &offset,
const string &textOverride,
const vector<string> &text,
const string &iconText,
const shared_ptr<Texture> &iconTexture,
const shared_ptr<Texture> &iconFrame) const {
@ -63,8 +63,7 @@ void ImageButton::render(
drawIcon(offset, iconText, iconTexture, iconFrame);
if (!textOverride.empty() || !_text.text.empty()) {
string text(!textOverride.empty() ? textOverride : _text.text);
if (!text.empty()) {
drawText(text, borderOffset, size);
}
}

View file

@ -31,7 +31,7 @@ public:
void render(
const glm::ivec2 &offset,
const std::string &textOverride,
const std::vector<std::string> &text,
const std::string &iconText,
const std::shared_ptr<render::Texture> &iconTexture,
const std::shared_ptr<render::Texture> &iconFrame) const;

View file

@ -20,6 +20,7 @@
#include "../../common/log.h"
#include "../../render/mesh/quad.h"
#include "../../render/shaders.h"
#include "../../render/textutil.h"
#include "../../resource/resources.h"
#include "../gui.h"
@ -43,26 +44,19 @@ ListBox::ListBox(GUI *gui) : Control(gui, ControlType::ListBox) {
_clickable = true;
}
void ListBox::updateItems() {
if (!_protoItem) return;
_slotCount = _extent.height / (_protoItem->extent().height + _padding);
if (_scrollBar) {
_scrollBar->setVisible(_items.size() > _slotCount);
}
}
void ListBox::clearItems() {
_items.clear();
_itemOffset = 0;
_hilightedIndex = -1;
updateItems();
}
void ListBox::addItem(Item item) {
if (!_protoItem) return;
item._textLines = breakText(item.text, *_protoItem->text().font, _protoItem->extent().width);
_items.push_back(move(item));
updateItems();
updateItemSlots();
}
void ListBox::clearSelection() {
@ -77,7 +71,6 @@ void ListBox::load(const GffStruct &gffs) {
ControlType type = _protoItemType == ControlType::Invalid ? getType(*protoItem) : _protoItemType;
_protoItem = of(_gui, type, getTag(*protoItem));
_protoItem->load(*protoItem);
updateItems();
}
shared_ptr<GffStruct> scrollBar(gffs.getStruct("SCROLLBAR"));
if (scrollBar) {
@ -97,23 +90,66 @@ int ListBox::getItemIndex(int y) const {
if (!_protoItem) return -1;
const Control::Extent &protoExtent = _protoItem->extent();
int idx = (y - protoExtent.top) / (protoExtent.height + _padding) + _itemOffset;
if (protoExtent.height == 0) return -1;
return idx >= 0 && idx < _items.size() ? idx : -1;
float itemy = protoExtent.top;
if (y < itemy) return -1;
for (size_t i = _itemOffset; i < _items.size(); ++i) {
const Item &item = _items[i];
if (_protoMatchContent) {
itemy += item._textLines.size() * _protoItem->text().font->height();
} else {
itemy += protoExtent.height;
}
itemy += _padding;
if (y < itemy) return i;
}
return -1;
}
bool ListBox::handleMouseWheel(int x, int y) {
if (y < 0) {
if (_items.size() - _itemOffset > _slotCount) _itemOffset++;
if (_items.size() - _itemOffset > _slotCount) {
_itemOffset++;
updateItemSlots();
}
return true;
} else if (y > 0) {
if (_itemOffset > 0) _itemOffset--;
if (_itemOffset > 0) {
_itemOffset--;
updateItemSlots();
}
return true;
}
return false;
}
void ListBox::updateItemSlots() {
_slotCount = 0;
// Increase the number of slots until they no longer fit vertically
float y = 0.0f;
for (size_t i = _itemOffset; i < _items.size(); ++i) {
if (_protoMatchContent) {
y += _items[i]._textLines.size() * _protoItem->text().font->height();
} else {
y += _protoItem->extent().height;
}
y += _padding;
if (y > _extent.height) break;
++_slotCount;
}
if (_scrollBar) {
_scrollBar->setVisible(_items.size() > _slotCount);
}
}
bool ListBox::handleClick(int x, int y) {
int itemIdx = getItemIndex(y);
if (itemIdx == -1) return false;
@ -130,37 +166,44 @@ bool ListBox::handleClick(int x, int y) {
return true;
}
void ListBox::render(const glm::ivec2 &offset, const string &textOverride) const {
void ListBox::render(const glm::ivec2 &offset, const vector<string> &text) const {
if (!_visible) return;
Control::render(offset, textOverride);
Control::render(offset, text);
if (!_protoItem) return;
glm::vec2 itemOffset(offset);
const Control::Extent &protoExtent = _protoItem->extent();
for (int i = 0; i < _slotCount; ++i) {
int itemIdx = i + _itemOffset;
if (itemIdx >= _items.size()) break;
const Item &item = _items[itemIdx];
if (_protoMatchContent) {
_protoItem->setHeight(item._textLines.size() * (_protoItem->text().font->height() + _padding));
}
_protoItem->setFocus(_hilightedIndex == itemIdx);
auto imageButton = dynamic_pointer_cast<ImageButton>(_protoItem);
if (imageButton) {
imageButton->render(itemOffset, _items[itemIdx].text, _items[itemIdx].iconText, _items[itemIdx].iconTexture, _items[itemIdx].iconFrame);
imageButton->render(itemOffset, item._textLines, item.iconText, item.iconTexture, item.iconFrame);
} else {
_protoItem->render(itemOffset, _items[itemIdx].text);
_protoItem->render(itemOffset, item._textLines);
}
itemOffset.y += protoExtent.height + _padding;
if (_protoMatchContent) {
itemOffset.y += item._textLines.size() * (_protoItem->text().font->height() + _padding);
} else {
itemOffset.y += _protoItem->extent().height + _padding;
}
}
if (_scrollBar) {
auto &scrollBar = static_cast<ScrollBar &>(*_scrollBar);
scrollBar.setCanScrollUp(_itemOffset > 0);
scrollBar.setCanScrollDown(_items.size() - _itemOffset > _slotCount);
scrollBar.render(offset, textOverride);
scrollBar.render(offset, vector<string>());
}
}
@ -186,7 +229,7 @@ void ListBox::setFocus(bool focus) {
void ListBox::setExtent(const Extent &extent) {
Control::setExtent(extent);
updateItems();
updateItemSlots();
}
void ListBox::setProtoItemType(ControlType type) {
@ -197,6 +240,10 @@ void ListBox::setSelectionMode(SelectionMode mode) {
_mode = mode;
}
void ListBox::setProtoMatchContent(bool match) {
_protoMatchContent = match;
}
const ListBox::Item &ListBox::getItemAt(int index) const {
return _items[index];
}

View file

@ -40,6 +40,8 @@ public:
std::string iconText;
std::shared_ptr<render::Texture> iconTexture;
std::shared_ptr<render::Texture> iconFrame;
std::vector<std::string> _textLines;
};
ListBox(GUI *gui);
@ -53,13 +55,14 @@ public:
bool handleMouseMotion(int x, int y) override;
bool handleMouseWheel(int x, int y) override;
bool handleClick(int x, int y) override;
void render(const glm::ivec2 &offset, const std::string &textOverride) const override;
void render(const glm::ivec2 &offset, const std::vector<std::string> &text) const override;
void stretch(float x, float y, int mask) override;
void setFocus(bool focus) override;
void setExtent(const Extent &extent) override;
void setProtoItemType(ControlType type);
void setSelectionMode(SelectionMode mode);
void setProtoMatchContent(bool match);
const Item &getItemAt(int index) const;
@ -78,8 +81,10 @@ private:
int _itemOffset { 0 };
int _hilightedIndex { -1 };
int _itemMargin { 0 };
bool _protoMatchContent { false }; /**< proto item height must match its content */
void updateItemSlots();
void updateItems();
int getItemIndex(int y) const;
};

View file

@ -45,7 +45,7 @@ void ScrollBar::load(const GffStruct &gffs) {
}
}
void ScrollBar::render(const glm::ivec2 &offset, const string &textOverride) const {
void ScrollBar::render(const glm::ivec2 &offset, const vector<string> &text) const {
if (!_dir.image) return;
setActiveTextureUnit(0);

View file

@ -28,7 +28,7 @@ public:
ScrollBar(GUI *gui);
void load(const resource::GffStruct &gffs) override;
void render(const glm::ivec2 &offset, const std::string &textOverride) const override;
void render(const glm::ivec2 &offset, const std::vector<std::string> &text) const override;
void setCanScrollUp(bool scroll);
void setCanScrollDown(bool scroll);

View file

@ -284,10 +284,10 @@ void GUI::update(float dt) {
void GUI::render() const {
if (_background) drawBackground();
if (_rootControl) _rootControl->render(_rootOffset);
if (_rootControl) _rootControl->render(_rootOffset, _rootControl->textLines());
for (auto &control : _controls) {
control->render(_controlOffset);
control->render(_controlOffset, control->textLines());
}
}

96
src/render/textutil.cpp Normal file
View file

@ -0,0 +1,96 @@
/*
* Copyright (c) 2020-2021 The reone project contributors
*
* This program 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 3 of the License, or
* (at your option) any later version.
*
* This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include "textutil.h"
#include <sstream>
#include <boost/algorithm/string.hpp>
#include "font.h"
using namespace std;
namespace reone {
namespace render {
vector<string> breakText(const string &text, Font &font, int maxWidth) {
vector<string> result;
string line;
ostringstream tokenBuffer;
for (char ch : text) {
switch (ch) {
case '\n': {
string token(tokenBuffer.str());
if (!token.empty()) {
tokenBuffer.str("");
if (!line.empty()) {
line += " ";
}
line += token;
}
if (!line.empty()) {
result.push_back(line);
line = "";
}
break;
}
case ' ': {
string token(tokenBuffer.str());
if (!token.empty()) {
tokenBuffer.str("");
string test(line);
if (!test.empty()) {
test += " ";
}
test += token;
if (font.measure(test) <= maxWidth) {
line = move(test);
} else {
result.push_back(line);
line = move(token);
}
}
break;
}
default:
tokenBuffer.put(ch);
break;
}
}
string token(tokenBuffer.str());
if (!token.empty()) {
if (!line.empty()) {
line += " ";
}
line += token;
}
if (!line.empty()) {
result.push_back(move(line));
}
return move(result);
}
} // namespace render
} // namespace reone

37
src/render/textutil.h Normal file
View file

@ -0,0 +1,37 @@
/*
* Copyright (c) 2020-2021 The reone project contributors
*
* This program 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 3 of the License, or
* (at your option) any later version.
*
* This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <string>
#include <vector>
namespace reone {
namespace render {
class Font;
/**
* Breaks the specified text into multiple strings to not exceed the maximum
* width, when rendering using the specified font.
*/
std::vector<std::string> breakText(const std::string &text, Font &font, int maxWidth);
} // namespace render
} // namespace reone