FifeGUI 0.3.0
A C++ GUI library designed for games.
text.cpp
1// SPDX-License-Identifier: LGPL-2.1-or-later OR BSD-3-Clause
2// SPDX-FileCopyrightText: 2004 - 2008 Olof Naessén and Per Larsson
3// SPDX-FileCopyrightText: 2013 - 2026 Fifengine contributors
4
5// Corresponding header include
6#include "fifechan/text.hpp"
7
8// Standard library includes
9#include <algorithm>
10#include <cassert>
11#include <numeric>
12#include <string>
13#include <utility>
14
15// Third-party library includes
16#include <utf8cpp/utf8.h>
17
18// Project headers (subdirs before local)
19#include "fifechan/exception.hpp"
20#include "fifechan/font.hpp"
21#include "fifechan/rectangle.hpp"
22
23namespace fcn
24{
25 Text::Text()
26 {
27 // Ensure at least one empty row exists for default constructed Text
28 mRows.emplace_back();
29 }
30
31 Text::Text(std::string const & content)
32 {
33 std::string::size_type lastPos = 0;
34 std::string::size_type pos = content.find('\n', lastPos);
35
36 for (; pos != std::string::npos; pos = content.find('\n', lastPos)) {
37 int const length = pos - lastPos;
38 std::string const sub = content.substr(lastPos, length);
39 mRows.push_back(sub);
40 lastPos = pos + 1;
41 }
42
43 // Add the last part after the last newline or the entire string if no newline is found
44 std::string const sub = content.substr(lastPos);
45 if (!sub.empty()) {
46 mRows.push_back(sub);
47 }
48
49 // Ensure at least one row exists
50 if (mRows.empty()) {
51 mRows.emplace_back();
52 }
53 }
54
55 Text::~Text() = default;
56
57 void Text::setContent(std::string const & content)
58 {
59 // Reset caret
61 mCaretRow = 0;
62 mCaretColumn = 0;
63
64 mRows.clear();
65 std::string::size_type lastPos = 0;
66 std::string::size_type pos = 0;
67
68 for (; (pos = content.find('\n', lastPos)) != std::string::npos; lastPos = pos + 1) {
69 int const length = pos - lastPos;
70 std::string const sub = content.substr(lastPos, length);
71 mRows.push_back(sub);
72 }
73
74 // Add the last part after the last newline or the entire string if no newline is found
75 std::string const sub = content.substr(lastPos);
76 if (!sub.empty()) {
77 mRows.push_back(sub);
78 }
79
80 // Ensure at least one row exists
81 if (mRows.empty()) {
82 mRows.emplace_back();
83 }
84 }
85
86 std::string Text::getContent() const
87 {
88 if (mRows.empty()) {
89 return {};
90 }
91
92 std::string result;
93 for (size_t i = 0; i < mRows.size() - 1; ++i) {
94 result.append(mRows.at(i)).append("\n");
95 }
96
97 result.append(mRows.back());
98
99 return result;
100 }
101
102 void Text::setRow(unsigned int row, std::string const & content)
103 {
104 if (row >= mRows.size()) {
105 throwException("Row out of bounds!");
106 }
107 assert("content is valid utf8" && utf8::is_valid(content.begin(), content.end()));
108
109 mRows.at(row) = content;
110 }
111
112 void Text::addRow(std::string const & row)
113 {
114 for (char const i : row) {
115 if (i == '\n') {
116 throwException("Line feed not allowed in the row to be added!");
117 }
118 }
119 assert("row is valid utf8" && utf8::is_valid(row.begin(), row.end()));
120
121 mRows.push_back(row);
122 }
123
124 void Text::insertRow(std::string const & row, unsigned int position)
125 {
126 unsigned int const totalRows = mRows.size();
127
128 if (position >= totalRows) {
129 if (position == totalRows) {
130 addRow(row);
131 return;
132 }
133 throwException("Position out of bounds!");
134 }
135
136 for (char const i : row) {
137 if (i == '\n') {
138 throwException("Line feed not allowed in the row to be inserted!");
139 }
140 }
141 assert("row is valid utf8" && utf8::is_valid(row.begin(), row.end()));
142
143 mRows.insert(mRows.begin() + position, row);
144 }
145
146 void Text::eraseRow(unsigned int row)
147 {
148 if (row >= mRows.size()) {
149 throwException("Row to be erased out of bounds!");
150 }
151
152 mRows.erase(mRows.begin() + row);
153 }
154
155 std::string& Text::getRow(unsigned int row)
156 {
157 if (row >= mRows.size()) {
158 throwException("Row out of bounds!");
159 }
160
161 return mRows.at(row);
162 }
163
164 void Text::insert(int character)
165 {
166 assert("character is valid unicode" && character >= 0 && character <= 0x10FFFF);
167 assert("caret row is within bounds" && mCaretRow < mRows.size());
168
169 char const c = static_cast<char>(character);
170
171 if (mRows.empty()) {
172 if (c == '\n') {
173 mRows.emplace_back("");
174 } else {
175 mRows.emplace_back(1, c);
176 }
177 } else {
178 if (c == '\n') {
179 std::string const tail =
180 mRows.at(mCaretRow).substr(mCaretColumn, mRows.at(mCaretRow).size() - mCaretColumn);
181 mRows.at(mCaretRow).resize(mCaretColumn);
182 mRows.insert(mRows.begin() + mCaretRow + 1, tail);
183 } else {
184 mRows.at(mCaretRow).insert(mCaretColumn, std::string(1, c));
185 }
186 }
187
189 }
190
191 void Text::remove(int numberOfCharacters)
192 {
193 // cppcheck-suppress-begin knownConditionTrueFalse
194 // False positive: numberOfCharacters and branch logic is correct.
195
196 if (mRows.empty() || numberOfCharacters == 0) {
197 return;
198 }
199
200 // We should remove characters left of the caret position.
201 if (numberOfCharacters < 0) {
202 while (numberOfCharacters != 0) {
203 // If the caret position is zero there is nothing
204 // more to do.
205 if (mCaretPosition == 0) {
206 break;
207 }
208
209 // If we are at the end of the row
210 // and the row is not the first row we
211 // need to merge two rows.
212 if (mCaretColumn == 0 && mCaretRow != 0) {
213 mRows.at(mCaretRow - 1) += mRows.at(mCaretRow);
214 mRows.erase(mRows.begin() + mCaretRow);
217 } else {
218 mRows.at(mCaretRow).erase(mCaretColumn - 1, 1);
220 }
221
222 numberOfCharacters++;
223 }
224 } else if (numberOfCharacters > 0) {
225 // We should remove characters right of the caret position.
226 while (numberOfCharacters != 0) {
227 // If all rows have been removed or
228 // caret is out of bounds there is nothing more to do.
229 if (mRows.empty() || mCaretRow >= mRows.size()) {
230 break;
231 }
232
233 // If we are at the end of row and the row
234 // is not the last row we need to merge two rows.
235 if (mCaretRow < mRows.size() && mCaretColumn == mRows.at(mCaretRow).size() &&
236 mCaretRow < (mRows.size() - 1)) {
237 mRows.at(mCaretRow) += mRows.at(mCaretRow + 1);
238 mRows.erase(mRows.begin() + mCaretRow + 1);
239 } else {
240 mRows.at(mCaretRow).erase(mCaretColumn, 1);
241 }
242
243 numberOfCharacters--;
244 }
245 }
246
247 // cppcheck-suppress-end knownConditionTrueFalse
248 }
249
251 {
252 return mCaretPosition;
253 }
254
255 void Text::setCaretPosition(int position)
256 {
257 if (mRows.empty() || position < 0) {
258 mCaretPosition = 0;
259 mCaretRow = 0;
260 mCaretColumn = 0;
261 return;
262 }
263
264 unsigned int const pos = static_cast<unsigned int>(position);
265 unsigned int total = 0;
266
267 for (unsigned int i = 0; i < mRows.size(); ++i) {
268 unsigned int const rowLen = static_cast<unsigned int>(mRows.at(i).size());
269
270 if (pos < total + rowLen) {
271 mCaretRow = i;
272 mCaretColumn = static_cast<int>(pos - total);
273 mCaretPosition = position;
274 return;
275 }
276
277 if (pos == total + rowLen) {
278 // Caret at the boundary between this row and the next row.
279 if (i + 1 < mRows.size()) {
280 mCaretRow = i + 1;
281 mCaretColumn = 0;
282 mCaretPosition = position;
283 return;
284 }
285
286 // At end of content (last row), place caret at end of last row.
287 mCaretRow = i;
288 mCaretColumn = static_cast<int>(rowLen);
289 mCaretPosition = position;
290 return;
291 }
292
293 // Move to the start of the next row (include newline)
294 total += rowLen + 1;
295 }
296
297 // Position is beyond the content: clamp to end.
298 unsigned int const contentChars = getNumberOfCharacters();
299 if (contentChars == 0) {
300 mCaretPosition = 0;
301 mCaretRow = 0;
302 mCaretColumn = 0;
303 return;
304 }
305
306 mCaretPosition = static_cast<int>(contentChars - 1);
307 mCaretRow = mRows.size() - 1;
308 mCaretColumn = static_cast<int>(mRows.back().size());
309 }
310
311 void Text::setCaretPosition(int x, int y, Font* font)
312 {
313 assert("font is not null" && font != nullptr);
314 assert("font height is positive" && font->getHeight() > 0);
315
316 if (mRows.empty()) {
317 return;
318 }
319
320 setCaretRow(y / font->getHeight());
321 setCaretColumn(font->getStringIndexAt(mRows.at(mCaretRow), x));
322 }
323
325 {
326 return mCaretColumn;
327 }
328
330 {
331 return mCaretRow;
332 }
333
334 void Text::setCaretColumn(int column)
335 {
336 if (mRows.empty() || column < 0) {
337 mCaretColumn = 0;
338 } else if (std::cmp_greater(column, mRows.at(mCaretRow).size())) {
339 mCaretColumn = mRows.at(mCaretRow).size();
340 } else {
341 mCaretColumn = column;
342 }
343
345 }
346
347 void Text::setCaretRow(int row)
348 {
349 if (mRows.empty() || row < 0) {
350 mCaretRow = 0;
351 } else if (std::cmp_greater_equal(row, mRows.size())) {
352 mCaretRow = mRows.size() - 1;
353 } else {
354 mCaretRow = row;
355 }
356
358 }
359
360 int Text::getCaretX(Font* font) const
361 {
362 assert("font is not null" && font != nullptr);
363
364 if (mRows.empty()) {
365 return 0;
366 }
367
368 return font->getWidth(mRows.at(mCaretRow).substr(0, mCaretColumn));
369 }
370
371 int Text::getCaretY(Font* font) const
372 {
373 assert("font is not null" && font != nullptr);
374 assert("font height is positive" && font->getHeight() > 0);
375
376 return mCaretRow * font->getHeight();
377 }
378
380 {
381 assert("font is not null" && font != nullptr);
382 assert("font height is positive" && font->getHeight() > 0);
383
384 if (mRows.empty()) {
385 return {0, 0, font->getWidth(" "), font->getHeight()};
386 }
387
388 int width = 0;
389 for (auto const & mRow : mRows) {
390 int const w = font->getWidth(mRow);
391 width = std::max(width, w);
392 }
393
394 auto h = static_cast<int>(font->getHeight() * mRows.size());
395 auto w = width + font->getWidth(" ");
396
397 return {0, 0, w, h};
398 }
399
401 {
402 assert("font is not null" && font != nullptr);
403 assert("font height is positive" && font->getHeight() > 0);
404
405 Rectangle dim;
406 dim.x = !mRows.empty() ? font->getWidth(mRows.at(mCaretRow).substr(0, mCaretColumn)) : 0;
407 dim.y = font->getHeight() * mCaretRow;
408 dim.width = font->getWidth(" ");
409 dim.height = font->getHeight() + 2;
410 return dim;
411 }
412
413 int Text::getWidth(int /*row*/, Font* /*font*/) const
414 {
415 return 0;
416 }
417
418 unsigned int Text::getMaximumCaretRow() const
419 {
420 return 0;
421 }
422
423 unsigned int Text::getMaximumCaretRow(unsigned int /*row*/) const
424 {
425 return 0;
426 }
427
428 unsigned int Text::getNumberOfCharacters() const
429 {
430 return std::accumulate(mRows.begin(), mRows.end(), size_t{0}, [](size_t sum, auto const & row) {
431 return sum + row.size() + 1;
432 });
433 }
434
435 unsigned int Text::getNumberOfRows() const
436 {
437 return mRows.size();
438 }
439
440 unsigned int Text::getNumberOfCharacters(unsigned int row) const
441 {
442 if (row >= mRows.size()) {
443 return 0;
444 }
445
446 return mRows.at(row).size();
447 }
448
450 {
451 unsigned int total = 0;
452 for (auto i = 0; std::cmp_less(i, mCaretRow); i++) {
453 // Add one for the line feed.
454 total += mRows.at(i).size() + 1;
455 }
456
458 }
459} // namespace fcn
Abstract interface for font rendering.
Definition font.hpp:26
Represents a rectangular area (X, Y, Width, Height).
Definition rectangle.hpp:22
int width
Holds the width of the rectangle.
int y
Holds the x coordinate of the rectangle.
int x
Holds the x coordinate of the rectangle.
int height
Holds the height of the rectangle.
unsigned int mCaretColumn
Holds the column the caret is in.
Definition text.hpp:319
virtual void setCaretColumn(int column)
Sets the column the caret should be in.
Definition text.cpp:334
virtual unsigned int getNumberOfCharacters() const
Gets the number of characters in the text.
Definition text.cpp:428
virtual unsigned int getMaximumCaretRow() const
Gets the maximum row the caret can be in.
Definition text.cpp:418
virtual void setContent(std::string const &content)
Sets the content of the text.
Definition text.cpp:57
virtual unsigned int getNumberOfRows() const
Gets the number of rows in the text.
Definition text.cpp:435
virtual int getWidth(int row, Font *font) const
Gets the width in pixels of a row.
Definition text.cpp:413
virtual std::string & getRow(unsigned int row)
Gets a reference to a row.
Definition text.cpp:155
virtual void setCaretRow(int row)
Sets the row the caret should be in.
Definition text.cpp:347
virtual int getCaretPosition() const
Gets the caret position.
Definition text.cpp:250
virtual ~Text()
Virtual destructor.
virtual void addRow(std::string const &row)
Adds a row to the content.
Definition text.cpp:112
virtual void setRow(unsigned int row, std::string const &content)
Sets the content of a row.
Definition text.cpp:102
virtual void remove(int numberOfCharacters)
Removes a given number of characters at starting at the current caret position.
Definition text.cpp:191
unsigned int mCaretRow
Holds the row the caret is in.
Definition text.hpp:313
virtual int getCaretColumn() const
Gets the column the caret is currently in.
Definition text.cpp:324
virtual void insertRow(std::string const &row, unsigned int position)
Inserts a row before the specified row position.
Definition text.cpp:124
virtual void setCaretPosition(int position)
Sets the caret position.
Definition text.cpp:255
virtual int getCaretX(Font *font) const
Gets the x coordinate of the caret in pixels given a font.
Definition text.cpp:360
unsigned int mCaretPosition
Holds the position of the caret.
Definition text.hpp:307
virtual int getCaretRow() const
Gets the row the caret is currently in.
Definition text.cpp:329
virtual void eraseRow(unsigned int row)
Erases the given row.
Definition text.cpp:146
virtual int getCaretY(Font *font) const
Gets the y coordinate of the caret in pixels given a font.
Definition text.cpp:371
std::vector< std::string > mRows
Holds the text row by row.
Definition text.hpp:301
virtual std::string getContent() const
Gets the content of the text.
Definition text.cpp:86
virtual void insert(int character)
Inserts a character at the current caret position.
Definition text.cpp:164
void calculateCaretPositionFromRowAndColumn()
Calculates the caret position from the caret row and caret column.
Definition text.cpp:449
virtual Rectangle getDimension(Font *font) const
Gets the dimension in pixels of the text given a font.
Definition text.cpp:379
virtual Rectangle getCaretDimension(Font *font) const
Gets the caret dimension relative to this text.
Definition text.cpp:400
Used replacement tokens by configure_file():
void throwException(std::string const &message, std::source_location location=std::source_location::current())
Throw an Exception capturing the current source location.