PEBL 2.2
Psychology Experiment Building Language - Cross-platform psychological experiment development system
sdl/PlatformTextBox.cpp
Go to the documentation of this file.
1//* -*- mode:C++; tab-width:4; c-basic-offset:4; indent-tabs-mode:nil -*- */
3// Name: src/platforms/sdl/PlatformTextBox.cpp
4// Purpose: Contains SDL-specific interface for the text boxes.
5// Author: Shane T. Mueller, Ph.D.
6// Copyright: (c) 2003-2026 Shane T. Mueller <smueller@obereed.net>
7// License: GPL 2
8//
9//
10//
11// This file is part of the PEBL project.
12//
13// PEBL is free software; you can redistribute it and/or modify
14// it under the terms of the GNU General Public License as published by
15// the Free Software Foundation; either version 2 of the License, or
16// (at your option) any later version.
17//
18// PEBL is distributed in the hope that it will be useful,
19// but WITHOUT ANY WARRANTY; without even the implied warranty of
20// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21// GNU General Public License for more details.
22//
23// You should have received a copy of the GNU General Public License
24// along with PEBL; if not, write to the Free Software
25// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
27
28#include "PlatformTextBox.h"
29#include "../../objects/PTextBox.h"
30#include "PlatformFont.h"
31#include "SDLUtility.h"
32
33
34#include "../../base/PComplexData.h"
35#include "../../devices/PKeyboard.h"
36
37#include "../../utility/rc_ptrs.h"
38#include "../../utility/PError.h"
39#include "../../utility/PEBLUtility.h"
40#include "../../utility/FormatParser.h"
41#include "../../utility/FontCache.h"
42
43
44#ifdef PEBL_OSX
45#include "SDL.h"
46#include "SDL_ttf.h"
47#else
48#include "SDL.h"
49#include "SDL_ttf.h"
50#endif
51#include "SDL_scancode.h"
52
53#include <stdio.h>
54#include <string>
55#include <algorithm>
56#include <cmath>
57
58//Unicode/utf-8 handling.
59#include "../../../libs/utfcpp/source/utf8.h"
60
61// cout removed - use cerr for debug output
62using std::cerr;
63using std::endl;
64using std::flush;
65using std::list;
66using std::ostream;
67using std::string;
68
69// Detect if text contains RTL (Right-to-Left) characters
70// Used for auto-adjusting textbox justification
71static bool has_rtl_text(const std::string & text) {
72 if (!utf8::is_valid(text.begin(), text.end())) {
73 return false;
74 }
75
76 const unsigned char *bytes = (const unsigned char *)text.c_str();
77 while (*bytes) {
78 // Hebrew: U+0590 to U+05FF (0xD6 0x90 to 0xD7 0xBF in UTF-8)
79 if ((bytes[0] == 0xD6 && bytes[1] >= 0x90) ||
80 (bytes[0] == 0xD7 && bytes[1] <= 0xBF)) {
81 return true;
82 }
83
84 // Arabic: U+0600 to U+06FF (0xD8 0x80 to 0xDB 0xBF in UTF-8)
85 if (bytes[0] >= 0xD8 && bytes[0] <= 0xDB) {
86 return true;
87 }
88
89 // Skip to next codepoint
90 if (bytes[0] < 0x80) bytes += 1;
91 else if ((bytes[0] & 0xE0) == 0xC0) bytes += 2;
92 else if ((bytes[0] & 0xF0) == 0xE0) bytes += 3;
93 else if ((bytes[0] & 0xF8) == 0xF0) bytes += 4;
94 else bytes += 1;
95 }
96
97 return false;
98}
99
100// Helper function to determine if a line should be right-justified
101// Used in cursor positioning to account for both RTL text and explicit RIGHT justification
102static bool is_line_right_justified(const std::string & line_text, const Variant & justify_property) {
103 // RTL text is always right-justified
104 if (has_rtl_text(line_text)) {
105 return true;
106 }
107 // Check if justification is explicitly set to RIGHT
108 if (justify_property == "RIGHT") {
109 return true;
110 }
111 return false;
112}
113
114
115PlatformTextBox::PlatformTextBox(string text, counted_ptr<PEBLObjectBase> font, int width, int height):
116 PTextObject(text), // Must initialize virtual base class directly
118 PTextBox(text, width, height)
119
120
121{
122 //this records if the text is valid UTF8. IF not (maybe it is iso 8859),
123 //we will skip all the utf8 multi-byte stuff.
124 mIsUTF8= utf8::is_valid(text.begin(),text.end());
125 mCDT = CDT_TEXTBOX;
126
127 mWidth = width;
128 mHeight = height;
129 mTextureWidth=mWidth;
130 mTextureHeight=mHeight;
131
132
133 mRenderer = NULL;
134 mSurface = NULL;
135 mTexture = NULL;
136 SetFont(font);
137 // Don't call SetText() - text already set by PTextBox(text, width, height) constructor
138 // Calling it again during construction causes crashes
139 mChanged = true;
140
141 // Note: We need to call FindBreaks() here even without a renderer
142 // so that numTextLines and other properties are calculated
143 FindBreaks();
144}
145
146
148 PTextObject(text.GetText()), // Must initialize virtual base class directly
150 PTextBox(text.GetText(), (int)(text.GetWidth()), (int)(text.GetHeight()))
151
152
153{
154 mSurface = NULL;
155 mTexture = NULL;
156 mRenderer = NULL;
157 mIsUTF8= utf8::is_valid(mText.begin(),mText.end());
159 mWidth = text.GetWidth();
160 mHeight = text.GetHeight();
163
164 SetFont(text.GetFont());
165 mChanged = true;
166 // Don't call Draw() here - no renderer yet since object hasn't been added to window
167 // Draw() will be called when object is added to window or explicitly drawn
168}
169
170
173{
174
175 // PlatformWidget frees mSurface,
176}
177
178// Inheritable function that is called by friend method << operator of PComplexData
179ostream & PlatformTextBox::SendToStream(ostream& out) const
180{
181 out << "<SDL PlatformTextBox: [" << mText << "] in " << *GetPlatformFont() << ">" <<flush;
182 return out;
183}
184
185
186
187
191{
192
193 //free the memory if it is currently pointing at something.
194 //#ifdef SDL2_DELETE
195 if(mSurface) SDL_FreeSurface(mSurface);
196 //#endif
197
198 //create a new surface on which to render the text.
199
200#if SDL_BYTEORDER == SDL_BIG_ENDIAN
201
202 Uint32 rmask = 0xff000000;
203 Uint32 gmask = 0x00ff0000;
204 Uint32 bmask = 0x0000ff00;
205 Uint32 amask = 0x00000000;
206
207#else
208
209 Uint32 rmask = 0x000000ff;
210 Uint32 gmask = 0x0000ff00;
211 Uint32 bmask = 0x00ff0000;
212 Uint32 amask = 0x00000000;
213
214#endif
215
216 //we might get a Draw() command before the renderer is set,
217 //such as if the text is set before the object is added to a
218 //parent that has a renderer.
219
220
221 if(!mRenderer)
222 {
223 //cerr << "No renderer " << SDL_GetTicks() << endl;
224
225 return false;
226 }
227
228 //cerr << "creating new surface in platformtextbox::rendertext\n";
229
230 //Make a surface of the prescribed size.
231 mSurface = SDL_CreateRGBSurface(SDL_SWSURFACE,
232 (int)mTextureWidth,
233 (int)mTextureHeight, 32,
234 rmask, gmask, bmask, amask);
235 if(!mSurface) PError::SignalFatalError("Surface not created in TextBox::RenderText.");
236
237 //cerr << "fillingbackground rec platformtextbox::rendertext\n";
238 //Fill the box with the background color of the font (from property system in case it was modified)
239 PColor bgcolor = GetPlatformFont()->GetBackgroundColor(); // Gets current color from font property system
240 SDL_FillRect(mSurface, NULL, SDL_MapRGBA(mSurface->format,
241 bgcolor.GetRed(),
242 bgcolor.GetGreen(),
243 bgcolor.GetBlue(),
244 bgcolor.GetAlpha()));
245
246
247 //First, find the height of the text when rendered with the font.
248 int height = GetPlatformFont()->GetTextHeight(mText);
249
250 //Now, go through the text letter by letter and word by word until it won't fit on a line any longer.
251 unsigned int linestart = 0;
252 unsigned int linelength = 0;
253 unsigned int totalheight = 0;
254
255 SDL_Surface * tmpSurface=NULL;
256 std::vector<int>::iterator i = mBreaks.begin();
257 linestart = 0;
258
259 //cerr << "Textbox: "<< mHeight << " " <<totalheight << endl;
260
261 // Check if formatted text mode is enabled
262 Variant formattedVar = PEBLObjectBase::GetProperty("FORMATTED");
263 bool isFormatted = (formattedVar.GetInteger() != 0);
264
265 // Parse formatted text once if needed
266 std::vector<FormatParser::FormatSegment> segments;
267 if (isFormatted) {
268 // Calculate character width for indent calculations
269 int charWidth = GetPlatformFont()->GetTextWidth("M"); // Use 'M' as average char width
270 segments = FormatParser::ParseFormattedText(mText, charWidth);
271 }
272
273 while(i != mBreaks.end() && totalheight < (unsigned int) mHeight)
274 {
275
276
277 //mBreaks holds the 'starting' positions of each line.
278 linelength = *i - linestart;
279 if(linelength>0)
280 {
281
282 SDL_Rect to;
283
284 // Auto-detect RTL for THIS LINE and adjust justification
285 // For formatted text, use stripped text for line extraction
286 std::string line_text;
287 if (isFormatted) {
288 line_text = mStrippedText.substr(linestart, linelength);
289 } else {
290 line_text = mText.substr(linestart, linelength);
291 }
292 bool isLineRTL = has_rtl_text(line_text);
293
294 // Determine effective justification: RTL overrides mJustify
295 Variant effectiveJustify;
296 if (isLineRTL) {
297 effectiveJustify = "RIGHT";
298 } else {
299 effectiveJustify = mJustify;
300 }
301
302 if (isFormatted) {
303 // Cache fonts for this line to avoid repeated creation/deletion
304 // Key: (style, size, color_as_int) -> Value: PlatformFont*
305 std::map<std::tuple<int, int, unsigned int>, PlatformFont*> fontCache;
306
307 // FIRST PASS: Find maximum ascent, total width, justification for baseline alignment
308 int maxAscent = 0;
309 int maxLineHeight = 0; // Track maximum font height on this line
310 unsigned int segmentTextPos = 0;
311 bool hasHorizontalRule = false;
312 int lineIndent = 0; // Track indent for this line
313 int totalLineWidth = 0; // Total width of all segments on this line
314 FormatParser::Justification lineJustification = FormatParser::JUSTIFY_NONE; // Line justification
315
316 // Get base font properties once
317 PlatformFont* baseFont = GetPlatformFont();
318 std::string fontFileName = baseFont->GetFontFileName();
319 int baseFontSize = baseFont->GetFontSize();
320 PColor baseFgColor = baseFont->GetFontColor();
321 PColor baseBgColor = baseFont->GetBackgroundColor();
322 bool antiAliased = baseFont->GetAntiAliased();
323
324 for (const FormatParser::FormatSegment& seg : segments) {
325 unsigned int segStart = segmentTextPos;
326 unsigned int segEnd = segmentTextPos + seg.text.length();
327
328 if (segEnd > linestart && segStart < linestart + linelength) {
329 // Check for horizontal rule
330 if (seg.isHorizontalRule) {
331 hasHorizontalRule = true;
332 }
333
334 // Track indent (accumulates across segments on same line)
335 if (seg.indentPixels > 0) {
336 lineIndent = seg.indentPixels;
337 }
338
339 // Track justification (use first non-NONE justification found on line)
340 if (lineJustification == FormatParser::JUSTIFY_NONE && seg.justification != FormatParser::JUSTIFY_NONE) {
341 lineJustification = seg.justification;
342 }
343
344 unsigned int overlapStart = std::max(segStart, (unsigned int)linestart);
345 unsigned int overlapEnd = std::min(segEnd, (unsigned int)(linestart + linelength));
346 std::string segmentText = seg.text.substr(overlapStart - segStart, overlapEnd - overlapStart);
347
348 if (!segmentText.empty()) {
349 int segStyle = seg.style;
350 // Calculate proportional size: sizeOverride is now a percentage (100 = base font)
351 int segSize = baseFontSize;
352 if (seg.hasSizeOverride) {
353 segSize = (baseFontSize * seg.sizeOverride) / 100;
354 // Sanity check: ensure size is reasonable
355 if (segSize < 1) segSize = 1;
356 if (segSize > 200) segSize = 200;
357 }
358 PColor segFgColor = seg.hasColorOverride ? seg.colorOverride : baseFgColor;
359
360 // Create cache key (color encoded as R*256*256 + G*256 + B)
361 unsigned int colorKey = segFgColor.GetRed() * 65536 +
362 segFgColor.GetGreen() * 256 +
363 segFgColor.GetBlue();
364 auto cacheKey = std::make_tuple(segStyle, segSize, colorKey);
365
366 // Get or create font
367 PlatformFont* renderFont;
368 auto it = fontCache.find(cacheKey);
369 if (it != fontCache.end()) {
370 renderFont = it->second;
371 } else {
372 renderFont = new PlatformFont(
373 fontFileName, segStyle, segSize,
374 segFgColor, baseBgColor, antiAliased);
375 fontCache[cacheKey] = renderFont;
376 }
377
378 // Get the ascent (height above baseline) for this font
379 int ascent = TTF_FontAscent(renderFont->GetTTFFont());
380 if (ascent > maxAscent) maxAscent = ascent;
381
382 // Get the full line height (ascent + descent) for this font
383 int lineHeight = TTF_FontHeight(renderFont->GetTTFFont());
384 if (lineHeight > maxLineHeight) maxLineHeight = lineHeight;
385
386 // Calculate width for this segment
387 totalLineWidth += renderFont->GetTextWidth(segmentText);
388 }
389 }
390 segmentTextPos += seg.text.length();
391 }
392
393 // Calculate justification offset
394 // Use HTML alignment if specified, otherwise fall back to
395 // the textbox .justify property (mJustify)
396 int justifyOffset = 0;
397 if (lineJustification == FormatParser::JUSTIFY_CENTER) {
398 justifyOffset = (mWidth - totalLineWidth) / 2;
399 } else if (lineJustification == FormatParser::JUSTIFY_RIGHT) {
400 justifyOffset = mWidth - totalLineWidth;
401 } else if (lineJustification == FormatParser::JUSTIFY_NONE) {
402 // No HTML alignment — fall back to .justify property
403 if (effectiveJustify == "CENTER") {
404 justifyOffset = (mWidth - totalLineWidth) / 2;
405 } else if (is_line_right_justified(line_text, effectiveJustify)) {
406 justifyOffset = mWidth - totalLineWidth;
407 }
408 }
409
410 // SECOND PASS: Render segments with baseline alignment and justification
411 int xOffset = justifyOffset; // Start with justification offset
412 segmentTextPos = 0;
413
414 // Render horizontal rule if present
415 if (hasHorizontalRule) {
416 // Draw a horizontal line across the textbox width
417 PColor lineColor = GetPlatformFont()->GetFontColor();
418 int lineY = static_cast<int>(totalheight) + maxAscent / 2;
419 int lineWidth = mWidth - (lineIndent * 2); // Leave margins
420
421 // Draw horizontal line (simple rectangle)
422 SDL_Rect hrRect = {lineIndent, lineY, lineWidth, 2};
423 SDL_FillRect(mSurface, &hrRect,
424 SDL_MapRGBA(mSurface->format,
425 lineColor.GetRed(),
426 lineColor.GetGreen(),
427 lineColor.GetBlue(),
428 255));
429 }
430
431 for (const FormatParser::FormatSegment& seg : segments) {
432 unsigned int segStart = segmentTextPos;
433 unsigned int segEnd = segmentTextPos + seg.text.length();
434
435 if (segEnd > linestart && segStart < linestart + linelength) {
436 unsigned int overlapStart = std::max(segStart, (unsigned int)linestart);
437 unsigned int overlapEnd = std::min(segEnd, (unsigned int)(linestart + linelength));
438
439 std::string segmentText = seg.text.substr(
440 overlapStart - segStart,
441 overlapEnd - overlapStart);
442
443 if (!segmentText.empty()) {
444 // If this segment has an indent, use it as absolute x-position (but only if greater than current xOffset)
445 if (seg.indentPixels > xOffset) {
446 xOffset = seg.indentPixels;
447 }
448
449 int segStyle = seg.style;
450 // Calculate proportional size: sizeOverride is now a percentage (100 = base font)
451 int segSize = baseFontSize;
452 if (seg.hasSizeOverride) {
453 segSize = (baseFontSize * seg.sizeOverride) / 100;
454 // Sanity check: ensure size is reasonable
455 if (segSize < 1) segSize = 1;
456 if (segSize > 200) segSize = 200;
457 }
458 PColor segFgColor = seg.hasColorOverride ? seg.colorOverride : baseFgColor;
459
460 // Create cache key (same as first pass)
461 unsigned int colorKey = segFgColor.GetRed() * 65536 +
462 segFgColor.GetGreen() * 256 +
463 segFgColor.GetBlue();
464 auto cacheKey = std::make_tuple(segStyle, segSize, colorKey);
465
466 // Get font from cache (should always exist since we created it in first pass)
467 PlatformFont* renderFont = fontCache[cacheKey];
468
469 tmpSurface = renderFont->RenderText(segmentText.c_str());
470
471 // Get ascent for this specific font
472 int thisAscent = TTF_FontAscent(renderFont->GetTTFFont());
473
474 // Calculate y-offset to align baseline with max baseline
475 int yOffset = maxAscent - thisAscent;
476
477 // Position segment with baseline alignment
478 SDL_Rect segRect = {xOffset, static_cast<int>(totalheight) + yOffset,
479 tmpSurface->w, tmpSurface->h};
480
481 SDL_BlitSurface(tmpSurface, NULL, mSurface, &segRect);
482 SDL_FreeSurface(tmpSurface);
483
484 xOffset += segRect.w;
485 }
486 }
487
488 segmentTextPos += seg.text.length();
489 }
490
491 // Clean up cached fonts for this line
492 for (auto& pair : fontCache) {
493 delete pair.second;
494 }
495
496 // For formatted text, use the maximum line height found on this line
497 // instead of the base font height
498 totalheight += maxLineHeight;
499 } else {
500 // Normal (non-formatted) rendering
501 if(effectiveJustify == "RIGHT")
502 {
503 //right flush
504 tmpSurface = GetPlatformFont()->RenderText(line_text.c_str());
505 SDL_Rect tmprect = {mSurface->w - tmpSurface->w,
506 static_cast<int>(totalheight),
507 tmpSurface->w,tmpSurface->h};
508 to = tmprect;
509 }
510 else if (effectiveJustify == "CENTER"){
511 //centered
512 tmpSurface = GetPlatformFont()->RenderText(line_text.c_str());
513 int xval = (mSurface->w - tmpSurface->w)/2;
514 SDL_Rect tmprect = {xval,static_cast<int>(totalheight),
515 tmpSurface->w,tmpSurface->h};
516 to = tmprect;
517 }
518 else{
519 // LEFT justification (default)
520 tmpSurface = GetPlatformFont()->RenderText(line_text.c_str());
521 SDL_Rect tmprect = {0,static_cast<int>(totalheight),tmpSurface->w,tmpSurface->h};
522 to = tmprect;
523 }
524
525
526 if(0)
527 {
528 //This was originally used for international right-to-left font layout, but
529 //it never worked.
530 std::string tmptext = mText.substr(linestart,linelength);
531
532 //
533 std::string rtext = PEBLUtility::strrev_utf8(tmptext);
534
535
536
537 //Re-render the text using the associated font.
538 tmpSurface = GetPlatformFont()->RenderText(rtext.c_str());
539
540
541 SDL_Rect tmprect = {(mSurface->w - tmpSurface->w),static_cast<int>(totalheight),
542 tmpSurface->w, tmpSurface->h};
543 to = tmprect;
544
545 }
546
547
548 SDL_BlitSurface(tmpSurface, NULL, mSurface,&to);
549 SDL_FreeSurface(tmpSurface);
550
551 // For normal text, use the base font height
552 totalheight += height;
553 }
554 }
555 else
556 {
557 // No line content, still need to increment height for empty lines
558 totalheight += height;
559 }
560
561 linestart = *i;
562 i++;
563
564 }
565
566 if(mTexture)
567 {
568
569
570 SDL_DestroyTexture(mTexture);
571 mTexture = SDL_CreateTexture(mRenderer, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET,
572 (int)mTextureWidth,
573 (int)mTextureHeight);
574 // Enable best quality filtering (anisotropic) for zoomed textures
575 SDL_SetTextureScaleMode(mTexture, SDL_ScaleModeBest);
576 SDL_SetTextureBlendMode(mTexture, SDL_BLENDMODE_BLEND);
577 }
578 else
579 {
580
581 mTexture = SDL_CreateTexture(mRenderer, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET,
582 (int)mTextureWidth,(int)mTextureHeight);
583 // Enable best quality filtering (anisotropic) for zoomed textures
584 SDL_SetTextureScaleMode(mTexture, SDL_ScaleModeBest);
585 SDL_SetTextureBlendMode(mTexture, SDL_BLENDMODE_BLEND);
586
587 }
588
589
590
591
592 //This will work except for drawing the cursor line on it. instead, create
593 //a texture from the surface, then copy it to mtexture.
594 //mTexture = SDL_CreateTextureFromSurface(mRenderer, mSurface);
595
596 SDL_Texture * tmp = SDL_CreateTextureFromSurface(mRenderer, mSurface);
597 SDL_FreeSurface(mSurface);
598 mSurface = NULL;
599 SDL_SetRenderTarget(mRenderer, mTexture);
600 SDL_RenderCopy(mRenderer, tmp, NULL, NULL);
601 SDL_SetRenderTarget(mRenderer,NULL);
602 SDL_DestroyTexture(tmp);
603
604 if(mEditable)
605 {
606 DrawCursor();
607 }
608
609
610 //If mTexture is null, then rendering failed.
611 if(mTexture)
612 {
613
614 mChanged = false;
615 return true;
616 }
617 else
618 {
619
620 mChanged = true;
621 return false;
622 }
623}
624
625
626bool PlatformTextBox::SetProperty(std::string name, Variant v)
627{
628
629 if(name == "TEXT")
630 {
631 SetText(v);
632 }
633 else if(PTextBox::SetProperty(name,v))
634 {
635 // If we set it at higher level, don't worry.
636 // This includes FORMATTED property which is handled in PTextBox
637 }
638 else if (name == "FONT")
639 {
641 }
642 else if(name=="WIDTH")
643 {
644 SetWidth(v);
645 }
646 else if(name=="HEIGHT")
647 {
648 SetHeight(v);
649 }
650 else return false;
651
652 return true;
653}
654
656{
657 if(name == "NUMTEXTLINES")
658 {
659 return Variant((int)mBreaks.size());
660 }
661 else
662 {
663 return PTextBox::GetProperty(name);
664 }
665}
666
667//These shadow higher accessors in widget, because
668//they need to set the textchanged flag
670{
671 mTextureHeight = h;
673
674 // CRITICAL: Clear cached stripped text when height changes
675 // This ensures FindBreaksFormatted() recalculates line breaks with new height
676 mStrippedText.clear();
677}
678
680{
681 mTextureWidth = w;
683
684 // CRITICAL: Clear cached stripped text when width changes
685 // This forces FindBreaksFormatted() to recalculate line breaks with new width
686 // Without this, changing width after textbox creation causes incorrect line wrapping
687 mStrippedText.clear();
688}
689
690
692{
693
694 mFontObject = font;
695
696 // Update the FONT property so nested access works correctly
697 PComplexData * pcd = new PComplexData(mFontObject);
699 delete pcd;
700
701 // Set requestedFontSize when font is set (needed for adaptive textbox scaling)
702 // Only set it if it hasn't been set before (is 0)
703 Variant currentRequestedSize = PEBLObjectBase::GetProperty("REQUESTEDFONTSIZE");
704 if (currentRequestedSize.GetInteger() == 0) {
705 int fontSize = GetPlatformFont()->GetFontSize();
706 PEBLObjectBase::SetProperty("REQUESTEDFONTSIZE", Variant(fontSize));
707 }
708
710 mChanged = true;
711 //Re-render the text onto mSurface
712 //if(!RenderText()) cerr << "Unable to render text.\n";
713
714}
715
716
717
719{
720
721
722 //Chain up to parent method.
724 mIsUTF8= utf8::is_valid(text.begin(),text.end());
725
726 //mCursorPos = 0;
727 mCursorChanged = true;
728 mChanged = true;
729
730 // Don't call Draw() here - it will be called when the object is actually drawn
731 // Calling Draw() during SetText() can cause crashes if the renderer isn't ready
732 // or if this is called during object construction/destruction
733
734 //Re-render the text onto mSurface
735 // if(!RenderText()) cerr << "Unable to render text.\n";
736
737}
738
740{
741 // Check if this is a formatted textbox
742 Variant formattedVar = PEBLObjectBase::GetProperty("FORMATTED");
743 bool isFormatted = (formattedVar.GetInteger() != 0);
744
745 if (val && isFormatted) {
746 // Disable formatted mode to allow editing of raw markdown text
747 // Tags will become visible as literal text for editing
748 std::cerr << "INFO: Disabling formatted mode for editing." << std::endl;
749 std::cerr << " Formatting tags will be visible as text and can be edited directly." << std::endl;
750
751 PEBLObjectBase::SetProperty("FORMATTED", Variant(0));
752 mChanged = true; // Force re-render to show raw tags
753 }
754
755 // Call parent implementation
757
758 // When making a textbox editable, position cursor at end of text
759 // This works for all justifications - the rendering code handles visual positioning
760 if (val) {
761 mCursorPos = mText.length();
762 mCursorChanged = true;
763 }
764}
765
766
767
768void PlatformTextBox::FindBreaks()
769{
770
771 //First, find the height and width of the text when rendered with the font.
772 int height = GetPlatformFont()->GetTextHeight(mText);
773
774 //Set this directly as a property; no need to check with PTextBox, which doesn't do anything with it.
775 PEBLObjectBase::SetProperty("LINEHEIGHT",Variant(height));
776
777 //Now, go through the text letter by letter and word by word until
778 //it won't fit on a line any longer.
779
780 unsigned int linestart = 0; //The start of the current line
781 unsigned int newlinestart = 0; //The start of the NEXT line
782 unsigned int linelength = 0;
783 unsigned int totalheight = 0;
784
785 mBreaks.clear();
786
787 // Check if formatted text mode is enabled
788 // If so, we need to use size-aware line breaking
789 Variant formattedVar = PEBLObjectBase::GetProperty("FORMATTED");
790 bool isFormatted = (formattedVar.GetInteger() != 0);
791
792 if (isFormatted) {
793 // Calculate character width for indent calculations
794 int charWidth = GetPlatformFont()->GetTextWidth("M"); // Use 'M' as average char width
795
796 // Parse formatted text segments
797 std::vector<FormatParser::FormatSegment> segments = FormatParser::ParseFormattedText(mText, charWidth);
798
799 // Use size-aware line breaking that accounts for proportional font sizes
800 FindBreaksFormatted(segments);
801 return; // Done - formatted line breaking complete
802 }
803
804 // For non-formatted text, use original algorithm
805 std::string textForBreaking = mText;
806
807 //Now, let's reserve space in mBreaks, roughly twice the amount we
808 //think we need. This will make adding elements take less time.
809
810 int width = GetPlatformFont()->GetTextWidth(mText);
811 if (mWidth > 0) {
812 mBreaks.reserve(width / mWidth * 2);
813 }
814
815
816 // Calculate line breaks until we either finish the text or exceed the height
817 // For adaptive mode, we need ALL breaks to know exact line count
818 // For normal mode, we can stop once we know text doesn't fit
819 Variant isAdaptiveVar = PEBLObjectBase::GetProperty("ISADAPTIVE");
820 bool isAdaptive = isAdaptiveVar.GetInteger() != 0;
821
822 while(newlinestart < mText.size())
823 {
824
825
826 linelength = FindNextLineBreak(linestart);
827
828 //Increment the placekeepers:
829 //This is where the next line will start.
830
831 newlinestart = linestart+ linelength;
832 totalheight += height;
833
834 mBreaks.push_back(newlinestart);
835 linestart=newlinestart;
836
837 // If not adaptive and we've exceeded height with more text remaining,
838 // we know text doesn't fit - no need to calculate more breaks
839 if (!isAdaptive && totalheight > (unsigned int)mHeight && newlinestart < mText.size()) {
840 break;
841 }
842
843 }
844
845 //Update NUMTEXTLINES property to reflect the number of lines
846 PEBLObjectBase::SetProperty("NUMTEXTLINES",Variant((int)mBreaks.size()));
847
848 //Set TEXTCOMPLETE property: 1 if all text rendered, 0 if truncated
849 //Text is complete only if we processed all text AND it fits in the height
850 //The rendering loop uses totalheight < mHeight, so the last line starts at (totalheight - height)
851 //For the last line to fully fit without cutoff, we need: (totalheight - height) + height <= mHeight
852 //which simplifies to: totalheight <= mHeight
853 //However, we also need to ensure the last line would actually render, which requires:
854 //(totalheight - height) < mHeight, which is equivalent to: totalheight < mHeight + height
855 //To be safe and detect cutoffs correctly, we check if there's enough room for the last line
856 //to both start AND end within the box
857 bool allTextProcessed = (newlinestart >= mText.size());
858 bool lastLineFits = true;
859 if (allTextProcessed && mBreaks.size() > 0 && mHeight > 0) {
860 // Calculate where the last line would start rendering
861 // totalheight is the sum of ALL line heights including the last
862 // So the last line starts at (totalheight - height) and ends at totalheight
863 unsigned int lastLineStart = (totalheight > (unsigned int)height) ? (totalheight - height) : 0;
864 unsigned int lastLineEnd = totalheight;
865 // For no cutoff: line must start before mHeight (so rendering begins)
866 // AND line must end at or before mHeight (so it's fully visible)
867 lastLineFits = (lastLineStart < (unsigned int)mHeight) && (lastLineEnd <= (unsigned int)mHeight);
868 }
869 bool textComplete = allTextProcessed && lastLineFits;
870 PEBLObjectBase::SetProperty("TEXTCOMPLETE",Variant(textComplete ? 1 : 0));
871}
872
873
876void PlatformTextBox::FindBreaksFormatted(const std::vector<FormatParser::FormatSegment>& segments)
877{
878 mBreaks.clear();
879 mStrippedText.clear();
880
881 int baseFontSize = GetPlatformFont()->GetFontSize();
882 std::string fontFileName = GetPlatformFont()->GetFontFileName();
883 PColor fgColor = GetPlatformFont()->GetFontColor();
884 PColor bgColor = GetPlatformFont()->GetBackgroundColor();
885 bool antiAliased = GetPlatformFont()->GetAntiAliased();
886
887 // Track current line state
888 int currentLineWidth = 0;
889 int currentLineMaxHeight = 0;
890 std::string currentLineText;
891 int strippedTextPos = 0;
892 int totalHeight = 0;
893
894 // Font cache for this operation (key: style, size -> font)
895 std::map<std::tuple<int, int>, PlatformFont*> fontCache;
896
897 // Check if adaptive mode to determine if we need all breaks
898 Variant isAdaptiveVar = PEBLObjectBase::GetProperty("ISADAPTIVE");
899 bool isAdaptive = isAdaptiveVar.GetInteger() != 0;
900
901 for (const FormatParser::FormatSegment& seg : segments) {
902 // Calculate proportional size for this segment
903 int segSize = baseFontSize;
904 if (seg.hasSizeOverride) {
905 segSize = (baseFontSize * seg.sizeOverride) / 100;
906 segSize = std::max(1, std::min(200, segSize));
907 }
908
909 // Get or create font for this segment
910 auto cacheKey = std::make_tuple(seg.style, segSize);
911 PlatformFont* segFont;
912 auto it = fontCache.find(cacheKey);
913 if (it != fontCache.end()) {
914 segFont = it->second;
915 } else {
916 segFont = new PlatformFont(fontFileName, seg.style, segSize,
917 fgColor, bgColor, antiAliased);
918 fontCache[cacheKey] = segFont;
919 }
920
921 // Process segment text, updating line state
922 FindBreaksInSegment(seg, segFont, currentLineWidth,
923 currentLineMaxHeight, currentLineText,
924 strippedTextPos, totalHeight);
925
926 // Check if we've exceeded height (for non-adaptive mode)
927 if (!isAdaptive && totalHeight > (unsigned int)mHeight && strippedTextPos < (int)mStrippedText.length()) {
928 break;
929 }
930 }
931
932 // Flush final line if there's remaining text
933 if (!currentLineText.empty()) {
934 mBreaks.push_back(strippedTextPos);
935 totalHeight += currentLineMaxHeight;
936 }
937
938 // Clean up font cache
939 for (auto& pair : fontCache) {
940 delete pair.second;
941 }
942
943 // Update properties
944 PEBLObjectBase::SetProperty("NUMTEXTLINES", Variant((int)mBreaks.size()));
945
946 // Check if text is complete (all text fits without cutoff)
947 // For formatted text, we need to track the last line's height separately
948 bool allTextProcessed = (strippedTextPos >= (int)mStrippedText.length());
949 bool lastLineFits = true;
950 if (allTextProcessed && mBreaks.size() > 0 && mHeight > 0) {
951 // The last line's height is currentLineMaxHeight (from the final flush at line 926)
952 // totalHeight now includes this last line's height
953 // Calculate where the last line would start and end
954 int lastLineHeight = currentLineMaxHeight; // Height of the last line
955 unsigned int lastLineStart = (totalHeight > lastLineHeight) ? (totalHeight - lastLineHeight) : 0;
956 unsigned int lastLineEnd = totalHeight;
957 // For no cutoff: line must start before mHeight AND end at or before mHeight
958 lastLineFits = (lastLineStart < (unsigned int)mHeight) && (lastLineEnd <= (unsigned int)mHeight);
959 }
960 bool textComplete = allTextProcessed && lastLineFits;
961 PEBLObjectBase::SetProperty("TEXTCOMPLETE", Variant(textComplete ? 1 : 0));
962}
963
964
967void PlatformTextBox::FindBreaksInSegment(
969 PlatformFont* segFont,
970 int& currentLineWidth,
971 int& currentLineMaxHeight,
972 std::string& currentLineText,
973 int& strippedTextPos,
974 int& totalHeight)
975{
976 std::string segText = seg.text;
977 size_t wordStart = 0;
978
979 // Track line height for this segment
980 int segHeight = TTF_FontHeight(segFont->GetTTFFont());
981 if (segHeight > currentLineMaxHeight) {
982 currentLineMaxHeight = segHeight;
983 }
984
985 // Process word by word
986 while (wordStart < segText.length()) {
987 // Find next word boundary (space, newline, or end)
988 size_t wordEnd = segText.find_first_of(" \n", wordStart);
989 if (wordEnd == std::string::npos) {
990 wordEnd = segText.length();
991 }
992
993 // Check if we found a standalone newline (no text before it)
994 // If there's text before the newline, process it as a word first
995 if (wordEnd < segText.length() && segText[wordEnd] == '\n' && wordEnd == wordStart) {
996 // Standalone newline - flush current line
997 // Add newline character to stripped text
998 mStrippedText += '\n';
999 strippedTextPos++;
1000
1001 // Flush current line (including newline in break position)
1002 mBreaks.push_back(strippedTextPos);
1003
1004 // Add this line's height to total before resetting
1005 totalHeight += currentLineMaxHeight;
1006
1007 // Reset line state
1008 currentLineWidth = 0;
1009 currentLineMaxHeight = 0;
1010 currentLineText.clear();
1011
1012 // Move past the newline
1013 wordStart = wordEnd + 1;
1014 continue;
1015 }
1016
1017 // Extract word (including trailing space if present)
1018 // NOTE: If wordEnd points to a newline, we DON'T include it - we'll process it next iteration
1019 bool hasSpace = (wordEnd < segText.length() && segText[wordEnd] == ' ');
1020 std::string word = segText.substr(wordStart, wordEnd - wordStart + (hasSpace ? 1 : 0));
1021
1022 if (!word.empty()) {
1023 // Measure word width using segment's font
1024 int wordWidth = segFont->GetTextWidth(word);
1025
1026 // Check if word fits on current line
1027 // Add 10-pixel margin to account for font rendering rounding/kerning differences
1028 // This prevents text from being clipped at the exact boundary
1029 if (currentLineWidth + wordWidth + 10 >= mWidth && !currentLineText.empty()) {
1030
1031 // Word doesn't fit - break line before this word
1032 mBreaks.push_back(strippedTextPos);
1033
1034 // Add previous line's height to total before starting new line
1035 totalHeight += currentLineMaxHeight;
1036
1037 // Start new line with this word
1038 currentLineWidth = wordWidth;
1039 currentLineMaxHeight = segHeight;
1040 currentLineText = word;
1041 } else {
1042 // Word fits - add to current line
1043 currentLineWidth += wordWidth;
1044 currentLineText += word;
1045 }
1046
1047 mStrippedText += word;
1048 strippedTextPos += word.length();
1049 }
1050
1051 wordStart = wordEnd + (hasSpace ? 1 : 0);
1052 }
1053}
1054
1055
1059// #if 0
1060// int PlatformTextBox::FindNextLineBreakOld(unsigned int curposition)
1061
1062// {
1063// unsigned int sublength = 0;
1064// unsigned int lastsep = 0;
1065// unsigned int sep = 0;
1066// std::string tmpstring;
1067
1068// //loop through the entire text from curposition on.
1069// while (curposition + sublength < mText.size()+1)
1070// {
1071
1072
1073
1074// //Get the width of the line right now.
1075// tmpstring = mText.substr(curposition,sublength);
1076// int tmpWidth = GetPlatformFont()->GetTextWidth(tmpstring);
1077
1078// // int time1 = SDL_GetTicks();
1079// cerr << "................findnextlinebreak: " <<mLineWrap<< " " << (curposition ) << ":" << (sublength ) << ": "<< tmpWidth << "***" << ( time1) << endl;
1080
1081// //Test to see if curposition is a '10' or a '0' (a hard/explicit line break). If so, this is a line break.
1082// if(mText[curposition + sublength] == 10
1083// || mText[curposition+sublength]==0
1084// || tmpWidth>=mWidth)
1085// //|| curposition + sublength == mText.size())
1086// {
1087
1088// //If the width of the current line is too big, break at the last separator.
1089// if(tmpWidth >= (unsigned int)(mWidth))
1090// {
1091
1092
1093// //if we are now too long, we should back up to the previous
1094// //break; unless that break was the beginning of the line. If
1095// //that is the case, back up one character, and crudely break
1096// //within a text line.
1097
1098// //we need to return the best linebreak here.
1099// if(sep >0)
1100// {
1101
1102// return sep+1;
1103// }
1104// if(lastsep>0)
1105// {
1106
1107// return lastsep+1;
1108// }
1109// else
1110// {
1111
1112
1113// if(mLineWrap)
1114// {
1115// cerr << "linewrap 4\n";
1116// cerr << "Need to wrap but nowhere to wrap\n" << endl;
1117// cerr << tmpstring << endl;
1118// //We have no natural break 'sep' on this line,
1119// //and the current line is too long.
1120// while(GetPlatformFont()->GetTextWidth(tmpstring) >(unsigned int)mWidth)
1121// {
1122
1123// //we need to back off, but
1124// //sublength might break a character.
1125// sublength--;
1126// end = start+sublength;
1127// tmpstring = mText.substr(start,end);
1128
1129// }
1130
1131// return sublength;
1132// }
1133// else
1134// {
1135// //We are past the end of the line, but still need to find the next line break on the text.
1136// while (curposition + sublength < mText.size()+1)
1137// {
1138// if(mText[curposition + sublength] == 10
1139// || mText[curposition+sublength]==0)
1140
1141// return sublength+1;
1142// else
1143// sublength++;
1144// }
1145// return sublength+2;
1146
1147// }
1148// }
1149// }
1150// else
1151// {
1152
1153// return sublength+1;
1154// }
1155// }
1156
1157
1158// //if we allow line wrapping, allow lines to wrap at other places too.
1159
1160// if(mLineWrap)
1161// {
1162// cerr << "linerwrap\n";
1163
1164// if(mText[curposition + sublength] == ' '
1165// || mText[curposition + sublength] == '-'
1166// || curposition + sublength == mText.size())
1167// {
1168// //either of these are word breaks; potential line breaks.
1169// //Increment word separator holders
1170// lastsep = sep;
1171// sep = sublength ;
1172
1173// tmpstring = mText.substr(curposition, sublength);
1174// //Check the size of the line.
1175// if(GetPlatformFont()->GetTextWidth(tmpstring) > (unsigned int)mWidth)
1176// {
1177
1178// //the text is too big for a single line, so return the last word break, but only
1179// //if the size is greater than 0. In that case, return the current separator, which
1180// //will not fit on the line, but it will get chopped off.
1181
1182// if(lastsep != 0)
1183// {
1184
1185// return lastsep+1;
1186// }
1187// else
1188// {
1189
1190// return sep+1;
1191// }
1192// }
1193
1194// }
1195
1196
1197// }
1198// else{
1199// cerr << "NO linerwrap\n";
1200// }
1201// sublength++;
1202// }
1203// //The rest of the text must fit in the space allotted; return that number.
1204
1205
1206// return sublength-1;
1207// }
1208
1209// #endif
1210
1211//This uses the utf headers to permit handling layout with UTF-8
1212//encoded text. The text is maintained in a std::string. Line breaks
1213//should return indices of the std::string--not codepoint positions.
1214//the important thing is that as you move through the string,
1215//you need to check that the text is legal UTF-8 (and not breaking)
1216//halfway through a codepoint. Note that the length of the string is
1217//not the number of characters in the utf-8 string.
1218
1219int PlatformTextBox::FindNextLineBreak(unsigned int curposition)
1220{
1221 unsigned int sublength = 0;
1222 unsigned int lastsep = 0;
1223 unsigned int sep = 0;
1224 std::string tmpstring;
1225
1226
1227 bool cont = true;
1228 std::string::iterator start;
1229 std::string::iterator end;
1230
1231 //loop through the entire text from curposition on.
1232 while (curposition + sublength < mText.size()+1)
1233 {
1234 //cerr << mText << endl;
1235 //cerr << curposition << "->" << (curposition+sublength) << "==<" << mText.size() <<endl;
1236
1237 //Get the width of the line right now.
1238 tmpstring = mText.substr(curposition,sublength);
1239 int tmpWidth = GetPlatformFont()->GetTextWidth(tmpstring);
1240
1241 // int time1 = SDL_GetTicks();
1242
1243
1244 //Test to see if curposition is a '10' or a '0' (a hard/explicit line break).
1245 //If so, this is a line break.
1246 if(mText[curposition + sublength] == 10
1247 || mText[curposition+sublength]==0
1248 || tmpWidth>=mWidth)
1249 //|| curposition + sublength == mText.size())
1250 {
1251
1252
1253 //If the width of the current line is too big, break at the last separator.
1254 if(tmpWidth >= (unsigned int)(mWidth))
1255 {
1256
1257
1258 //if we are now too long, we should back up to the previous
1259 //break; unless that break was the beginning of the line. If
1260 //that is the case, back up one character, and crudely break
1261 //within a text line.
1262
1263 //we need to return the best linebreak here.
1264 if(sep >0)
1265 {
1266
1267 return sep+1;
1268 }
1269 if(lastsep>0)
1270 {
1271
1272 return lastsep+1;
1273 }
1274 else
1275 {
1276
1277
1278 if(mLineWrap)
1279 {
1280
1281 //We have no natural break 'sep' on this line,
1282 //and the current line is too long.
1283 while(GetPlatformFont()->GetTextWidth(tmpstring) >(unsigned int)mWidth)
1284 {
1285
1286
1287 //we need to back off, but
1288 //sublength might break a character.
1289 cont = true;
1290 start =mText.begin()+curposition;
1291 end = start+sublength;
1292
1293 while(cont)
1294 {
1295
1296 sublength--;
1297 end = start+sublength;
1298 if(mIsUTF8)
1299 cont = false;
1300 else
1301 cont = !utf8::is_valid(start,end);
1302 }
1303
1304 tmpstring = mText.substr(curposition,sublength);
1305
1306
1307 }
1308
1309
1310 return sublength;
1311 }
1312 else
1313 {
1314
1315 //No line-wrap here.
1316 //We are past the end of the line, but still need to find
1317 //the next line break on the text.
1318 while (curposition + sublength < mText.size()+1)
1319 {
1320
1321 if(mText[curposition + sublength] == 10
1322 || mText[curposition+sublength]==0)
1323 {
1324
1325
1326 return sublength+1;
1327 }
1328 else
1329 {
1330
1331
1332 cont = true;
1333 start =mText.begin()+curposition;
1334 end = start+sublength;
1335
1336 if(mIsUTF8)
1337 {
1338 //This will advance one legal (utf) character:
1339 while(cont)
1340 {
1341
1342 sublength++;
1343 end = start+sublength;
1344 cont = !utf8::is_valid(start,end);
1345
1346 }
1347 }else{
1348
1349 sublength++;
1350 end=start+sublength;
1351 }
1352 }
1353 }
1354
1355 return sublength+2;
1356
1357 }
1358 }}
1359
1360 else
1361 {
1362
1363
1364 //originally sublength+1
1365 return sublength+1;
1366 }
1367 }
1368
1369
1370 //if we allow line wrapping, allow lines to wrap at other places too.
1371
1372
1373 if(mLineWrap)
1374 {
1375
1376
1377 if(mText[curposition + sublength] == ' '
1378 || mText[curposition + sublength] == '-'
1379 || curposition + sublength == mText.size())
1380 {
1381 //either of these are word breaks; potential line breaks.
1382 //Increment word separator holders
1383 lastsep = sep;
1384 sep = sublength ;
1385
1386 tmpstring = mText.substr(curposition, sublength);
1387 //Check the size of the line.
1388 if(GetPlatformFont()->GetTextWidth(tmpstring) > (unsigned int)mWidth)
1389 {
1390
1391 //the text is too big for a single line, so return the last word break, but only
1392 //if the size is greater than 0. In that case, return the current separator, which
1393 //will not fit on the line, but it will get chopped off.
1394
1395 if(lastsep != 0)
1396 {
1397
1398 return lastsep+1;
1399 }
1400 else
1401 {
1402
1403 return sep+1;
1404 }
1405 }
1406
1407 }
1408
1409
1410 }else{
1411 //here, we don't need to worry about mid-line linewraps.
1412
1413
1414 }
1415
1416
1417
1418 //This is a 'normal' increment of sublength. Increment until the thing is legal UTF.
1419
1420 cont = true;
1421 start =mText.begin()+curposition;
1422 end = start+sublength;
1423
1424
1425 while(cont)
1426 {
1427 // cerr << "Checking:["<< mText.substr(curposition,sublength) << "]" << curposition << "|" << sublength << ">>" << mText.length()<< "\n";
1428 sublength++;
1429 end = start+sublength;
1430 if(mIsUTF8)
1431 cont = !utf8::is_valid(start,end);
1432 else
1433 cont = false;
1434
1435 // cont = cont & (curposition+sublength)< mText.length(); //end when you get to the end of the text.
1436
1437 }
1438 }
1439 //The rest of the text must fit in the space allotted; return that number.
1440
1441
1442 return sublength-1;
1443}
1444
1445
1446
1447
1454int PlatformTextBox::FindCursorPosition(long int x, long int y)
1455{
1456
1457
1458 if(mText.length()==0)
1459 {
1460 return 0;
1461 }
1462
1463 //Find the height of a line.
1464 int height = GetPlatformFont()->GetTextHeight(mText);
1465
1466 if(y > mHeight) y = mHeight;
1467 if(y < 0) y = 0;
1468
1469 //The line will just be y / height, rounded down
1470 unsigned long int linenum = y / height; //this is 0-based.
1471
1472
1473 //Change the line number to the last one if it is too large.
1474 //The last element of mBreaks is the 'end' of the text; not really a break for
1475 //our purposes.
1476
1477 if(linenum > mBreaks.size())
1478 linenum = mBreaks.size();
1479
1480
1481 //find the starting character on the line we care about.
1482 int startchar;
1483 if(linenum==0)
1484 startchar = 0;
1485 else
1486 startchar = mBreaks[linenum-1];
1487
1488
1489
1490 if(startchar >= mBreaks[mBreaks.size()-1])
1491 {
1492 return startchar;
1493 }
1494
1495 //find the length in bytes of the current line:
1496 int length = mBreaks[linenum] - startchar;
1497
1498 // Get the line text to check for RTL
1499 std::string line_text = mText.substr(startchar, length);
1500
1501 // For right-justified text, x is the visual position from left edge of textbox
1502 // We need to convert it to what GetPosition() expects
1503 unsigned int x_adjusted = x;
1504 if (is_line_right_justified(line_text, mJustify)) {
1505 // Get line width to calculate where text starts
1506 int line_width = GetPlatformFont()->GetTextWidth(line_text);
1507 int text_start_x = mWidth - line_width; // Right justification offset
1508
1509 if ((int)x >= text_start_x) {
1510 // Click is within the text area
1511 // For RTL text, GetPosition() expects x from LEFT edge of text,
1512 // but RTL text renders right-to-left, so we need to flip:
1513 // Visual position from left of text = (x - text_start_x)
1514 // For RTL, we need position from RIGHT = line_width - (x - text_start_x)
1515 if (has_rtl_text(line_text)) {
1516 x_adjusted = line_width - ((int)x - text_start_x);
1517 } else {
1518 // Explicit RIGHT justification with LTR text
1519 x_adjusted = (int)x - text_start_x;
1520 }
1521 } else {
1522 // Click is to the left of the text (in the padding area)
1523 // For RTL, this means beginning of text (which is visually on right)
1524 // For LTR right-justified, this means before text starts
1525 x_adjusted = has_rtl_text(line_text) ? line_width : 0;
1526 }
1527 }
1528
1529 int charnum = GetPlatformFont()->GetPosition(line_text, x_adjusted);
1530
1531 //finally, if the current cursor position is a non-printing character (i.e. a carriage return)
1532 //back up one
1533
1534 if(!AtPrintableCharacter(charnum + startchar -1))
1535 {
1536
1537 if(charnum + startchar > 0)
1538 charnum--;
1539 }
1540
1541
1542 return charnum + startchar;
1543}
1544
1545
1546//This will draw a 'cursor' at a specified character.
1547void PlatformTextBox::DrawCursor()
1548{
1549 //Find x and y of position.
1550 unsigned int x = 0;
1551 unsigned int y = 0;
1552
1553 unsigned int height = GetPlatformFont()->GetTextHeight(mText);
1554 unsigned int width = (unsigned int)GetWidth();
1555 unsigned int i = 0;
1556 int linestart = 0;
1557
1558
1559 x = width-1; //Initialize x with the biggest value it can have.
1560 if(mCursorPos==0)
1561 {
1562 // For empty textbox, cursor starts at left for LTR, right for RTL
1563 if (has_rtl_text(mText)) {
1564 x = width - 1; // RTL: cursor starts on the right
1565 } else {
1566 x = 0; // LTR: cursor starts on the left
1567 }
1568
1569 } else {
1570 //The cursor is not at the beginning.
1571 //mBreaks has found the line breaks already.
1572 while(i < mBreaks.size())
1573 {
1574 //increment x,y while we go, so that if we get orphaned text,
1575 //we still get a decent cursor position.
1576 y = i * height;
1577
1578 //mBreaks is the position of the first character on each line
1579 if(mBreaks[i] >= mCursorPos)
1580 {
1581
1582 std::string subst = mText.substr(linestart, mCursorPos - linestart);
1583
1584 // Check if this line is right-justified (RTL text or explicit RIGHT)
1585 int line_end = mBreaks[i];
1586 std::string full_line = mText.substr(linestart, line_end - linestart);
1587
1588 if (is_line_right_justified(full_line, mJustify)) {
1589 // Right-justified: text starts at (width - line_width)
1590 // For RTL, cursor is at (line_width - text_before_cursor) from right edge of text
1591 // For LTR right-justified, cursor is at text_before_cursor from left edge of text
1592 int line_width = GetPlatformFont()->GetTextWidth(full_line);
1593 int text_before_cursor_width = GetPlatformFont()->GetTextWidth(subst);
1594 int text_start_x = width - line_width; // RIGHT justification offset
1595
1596 if (has_rtl_text(full_line)) {
1597 // RTL: cursor position is flipped
1598 x = text_start_x + (line_width - text_before_cursor_width);
1599 } else {
1600 // LTR right-justified: cursor position is normal
1601 x = text_start_x + text_before_cursor_width;
1602 }
1603 x = ( x >= width ) ? width-1: x;
1604 } else {
1605 // Left-justified: cursor is positioned from the left edge (original behavior)
1606 x = GetPlatformFont()->GetTextWidth(subst);
1607 x = ( x >= width ) ? width-1: x;
1608 }
1609 break;
1610 }
1611
1612 linestart = mBreaks[i];
1613 i++;
1614 }
1615 //x should be accurate, unless the cursor is at the last character,
1616 //and that last character is a carriage return.
1617
1618
1619 }
1620
1621 //The current position should be OK, UNLESS the character at mCursorPos is a CR.
1622 //Then, we actually want to render CR on the next line.
1623
1624 // Check bounds before accessing mText[mCursorPos-1]
1625 if(mCursorPos > 0 && mCursorPos <= (int)mText.size() && mText[mCursorPos-1]== 10)
1626 {
1627 y+=height;
1628 x=1;
1629 }
1630
1631
1632
1633 //x,y specifies the top of the cursor.
1634
1635
1636 // here, if the textbox is attached to the window, things render fine.
1637 // If the textbox is a child of another widget (a canvas),
1638 // everything falls apart.
1639
1640
1641 SDLUtility::DrawLine(mRenderer, this,x, y, x, y+height,
1642 (GetPlatformFont()->GetFontColor()));
1643
1644
1645}
1646
1647
1648
1649//This overrides the parent Draw() method so that
1650//things can be re-rendered if necessary.
1652{
1653 // Check if font properties changed and update widget bgcolor if needed
1654 if(GetPlatformFont()->HasChanged())
1655 {
1656 PWidget::SetBackgroundColor(GetPlatformFont()->GetBackgroundColor());
1657 mChanged = true;
1658 GetPlatformFont()->ClearChanged();
1659 }
1660
1661 // CRITICAL: Always call FindBreaks() when text has changed
1662 // This was in the backup version and is needed for proper rendering
1663 if(mChanged)
1664 {
1665 FindBreaks();
1666 }
1667
1668 if(mChanged)
1669 {
1670 // Check if adaptive mode is enabled
1671 Variant isAdaptiveVar = PEBLObjectBase::GetProperty("ISADAPTIVE");
1672 if (isAdaptiveVar.GetInteger()) {
1673 // Track whether this is the first time we're creating an adaptive font
1674 bool hadAdaptiveFont = (mAdaptiveFontObject.get() != NULL);
1675
1676 // Get the requested font size (original size before adaptation)
1677 Variant requestedSizeVar = PEBLObjectBase::GetProperty("REQUESTEDFONTSIZE");
1678 int requestedSize = requestedSizeVar.GetInteger();
1679
1680 // If requestedFontSize is 0, it hasn't been set yet - use current font size
1681 if (requestedSize == 0) {
1682 requestedSize = GetPlatformFont()->GetFontSize();
1683 PEBLObjectBase::SetProperty("REQUESTEDFONTSIZE", Variant(requestedSize));
1684 }
1685
1686 // Iterate to find the right font size
1687 // Keep previous adaptive font in mIntermediateFonts during iteration
1688 int maxIterations = 20;
1689 int iteration = 0;
1690 bool needsAdaptation = true;
1691
1692 while (iteration < maxIterations && needsAdaptation) {
1693 // Calculate line breaks with current font
1694 FindBreaks();
1695
1696 // Check if text currently fits
1697 Variant textCompleteVar = PEBLObjectBase::GetProperty("TEXTCOMPLETE");
1698 bool textComplete = textCompleteVar.GetInteger() != 0;
1699
1700 if (textComplete) {
1701 // Text fits - stop iteration
1702 needsAdaptation = false;
1703 break;
1704 }
1705
1706 // Text doesn't fit - calculate target font size
1707 int minFontSize = 8;
1708 int currentFontSize = GetPlatformFont()->GetFontSize();
1709 int targetSize = currentFontSize;
1710
1711 // Text doesn't fit (TEXTCOMPLETE=0), so we need to shrink the font
1712 // Reduce by 10% (minimum 1 point) each iteration
1713 {
1714 int reduction = std::max(currentFontSize / 10, 1);
1715 targetSize = std::max(currentFontSize - reduction, minFontSize);
1716 }
1717
1718 // Safety: ensure we're actually shrinking, never growing
1719 if (targetSize >= currentFontSize) {
1720 targetSize = std::max(currentFontSize - 1, minFontSize);
1721 }
1722
1723 // Create new font if we calculated a different size
1724 if (targetSize != currentFontSize && targetSize >= minFontSize) {
1725 std::string fontFileName = GetPlatformFont()->GetFontFileName();
1726 int fontStyle = GetPlatformFont()->GetFontStyle();
1727 PColor fontColor = GetPlatformFont()->GetFontColor();
1728 PColor bgColor = GetPlatformFont()->GetBackgroundColor();
1729 bool antiAliased = GetPlatformFont()->GetAntiAliased();
1730
1731 // Keep previous adaptive font alive during iteration
1732 if (mAdaptiveFontObject.get()) {
1733 mIntermediateFonts.push_back(mAdaptiveFontObject);
1734 }
1735
1736 // Create new font and immediately wrap in counted_ptr
1737 PlatformFont* newFont = new PlatformFont(fontFileName, fontStyle, targetSize,
1738 fontColor, bgColor, antiAliased);
1739 counted_ptr<PEBLObjectBase> newFontPtr(newFont);
1740
1741 // Set as new adaptive font
1742 mAdaptiveFontObject = newFontPtr;
1743
1744 // Update widget background color
1745 PWidget::SetBackgroundColor(GetPlatformFont()->GetBackgroundColor());
1746 } else {
1747 needsAdaptation = false;
1748 }
1749
1750 iteration++;
1751 }
1752
1753 // If we created an adaptive font, make sure it's set
1754 if (mAdaptiveFontObject.get()) {
1755 mChanged = true;
1756 }
1757
1758 // Clear intermediate fonts AFTER rendering is complete
1759 // This happens later in the function after RenderText() is called
1760 }
1761
1762 mChanged = true;
1763 }
1764
1766 {
1767 RenderText();
1768 }
1769
1770 // Don't clear intermediate fonts - let them persist for the textbox's lifetime
1771 // They will be cleaned up automatically when the textbox destructor runs
1772 // This avoids triggering destructor cascades during drawing
1773
1774 mCursorChanged = false;
1775
1776
1777 bool ret = PlatformWidget::Draw();
1778
1779 if(mEditable&false)
1780 {
1781 DrawCursor();
1782 }
1783
1784
1785 return ret;
1786}
1787
1788
1789//Some key presses can only be handled by the platform-specific code.
1790//Do this here.
1791void PlatformTextBox::HandleKeyPress(int keycode, int modkeys, Uint16 unicode)
1792{
1793#if 0
1794 cerr << "handling keypress in PlatformTextBox: " << keycode << endl;
1795 cerr << "(" << PEBL_KEYCODE_RETURN << "|" << PEBL_KEYCODE_RETURN2 << "|" << PEBL_KEYCODE_KP_ENTER << ")"<< std::endl;
1796 cerr << "PEBL_KEYCODE_RIGHT:" << PEBL_KEYCODE_RIGHT << std::endl;
1797 cerr << "PEBL_KEYCODE_LEFT:" << PEBL_KEYCODE_LEFT << std::endl;
1798#endif
1799
1800 switch(keycode)
1801 {
1802 case PEBL_KEYCODE_UP:
1803 case PEBL_KEYCODE_DOWN:
1804 {
1805 int change;
1806 if(keycode == PEBL_KEYCODE_UP) change = -1;
1807 else change = 1;
1808
1809 //Find x and y of position.
1810 int x = 0;
1811 int y = 0;
1812
1813 int height=GetPlatformFont()->GetTextHeight(mText);
1814 unsigned int i = 0;
1815 int linestart = 0;
1816
1817
1818 while(i < mBreaks.size())
1819 {
1820 if(mBreaks[i] > mCursorPos)
1821 {
1822 y = i * height;
1823
1824 // Calculate x position (visual position from left edge of textbox)
1825 std::string text_before_cursor = mText.substr(linestart, mCursorPos - linestart);
1826
1827 // Get the full line to check justification
1828 int line_end = mBreaks[i];
1829 std::string full_line = mText.substr(linestart, line_end - linestart);
1830
1831 if (is_line_right_justified(full_line, mJustify)) {
1832 // Right-justified: text starts at (width - line_width)
1833 // Cursor is at distance (line_width - text_before_cursor_width) from right edge of text
1834 // Visual x = text_start + (line_width - text_before_cursor_width)
1835 // = (width - line_width) + (line_width - text_before_cursor_width)
1836 // = width - text_before_cursor_width
1837 int text_before_cursor_width = GetPlatformFont()->GetTextWidth(text_before_cursor);
1838 x = mWidth - text_before_cursor_width;
1839 } else {
1840 // Left-justified: x is simply width of text before cursor
1841 x = GetPlatformFont()->GetTextWidth(text_before_cursor);
1842 }
1843 break;
1844 }
1845 linestart = mBreaks[i];
1846 i++;
1847 }
1848
1849
1850 mCursorPos = FindCursorPosition(x, y + change * height);
1851 mCursorChanged = true;
1852 break;
1853 }
1857 {
1858
1859 std::string lb = std::string() + (char(10));
1860
1861 InsertText(lb);
1862 //mCursorPos++;
1863 if(mCursorPos > (int) (mText.length()))
1864 mCursorPos = mText.length();
1865
1866 }
1867 break;
1869
1870 DeleteText(1);
1871 break;
1873
1874 DeleteText(-1);
1875 break;
1876
1877 case PEBL_KEYCODE_LEFT:
1878 case PEBL_KEYCODE_RIGHT:
1879 {
1880 // For RTL text, LEFT/RIGHT arrows should move in visual direction, not logical
1881 // Find which line the cursor is on to check if it's RTL
1882 bool isRTLLine = false;
1883
1884 if (mBreaks.size() > 0 && mText.length() > 0) {
1885 unsigned int i = 0;
1886 int linestart = 0;
1887
1888 while(i < mBreaks.size()) {
1889 if(mBreaks[i] >= mCursorPos) {
1890 // Found the current line
1891 int line_end = mBreaks[i];
1892 std::string current_line = mText.substr(linestart, line_end - linestart);
1893 isRTLLine = has_rtl_text(current_line);
1894 break;
1895 }
1896 linestart = mBreaks[i];
1897 i++;
1898 }
1899 }
1900
1901 // In RTL text, swap the behavior:
1902 // - LEFT arrow moves visually left (logically forward) = IncrementCursor
1903 // - RIGHT arrow moves visually right (logically backward) = DecrementCursor
1904 if (isRTLLine) {
1905 if (keycode == PEBL_KEYCODE_LEFT) {
1907 } else {
1909 }
1910 } else {
1911 // LTR text: normal behavior
1912 if (keycode == PEBL_KEYCODE_LEFT) {
1914 } else {
1916 }
1917 }
1918 }
1919 break;
1920
1921
1922 //case PEBL_KEYCODE_BACKSLASH:
1923 //InsertText("\\");
1924 //cerr << "backslash\n";
1925 // InsertText(PEBLUtility::TranslateKeycode((PEBL_Keycode)keycode, modkeys));
1926 //break;
1927
1928 default:
1929 //cerr << "----------------------\n" << keycode << endl;
1930 //cerr << "["<< (PEBLUtility::TranslateKeyCode(PEBLKey(keycode), modkeys))<<"]"<<endl;
1931 //cerr << "----------------------\n";
1932
1933
1934 // InsertText(SDL_GetKeyName(keycode));
1935 ;
1936 }
1937 mCursorChanged=true;
1938 //PTextBox::HandleKeyPress(keycode, modkeys, unicode);
1939}
1940
1941
1942
1943//Some key presses can only be handled by the platform-specific code.
1944//Do this here.
1946{
1947
1949}
1950
1951
1953{
1954 //Not sure about this, but let's not refind line breaks when this is called--just get
1955 //whatever has been calculated.
1956
1957 std::vector<int>::iterator i = mBreaks.begin();
1958
1959 PList * outlist = new PList();
1960 while(i != mBreaks.end())
1961 {
1962 outlist->PushBack(*i);
1963 i++;
1964 }
1965
1966
1968
1969 PComplexData * PCD =(new PComplexData(newList2));
1970 Variant tmp = Variant(PCD);
1971 delete PCD;
1972 PCD=NULL;
1973 return tmp;
1974
1975}
#define NULL
Definition BinReloc.cpp:317
@ CDT_TEXTBOX
Definition PEBLObject.h:57
@ PEBL_KEYCODE_RETURN2
Definition PKeyboard.h:231
@ PEBL_KEYCODE_DELETE
Definition PKeyboard.h:171
@ PEBL_KEYCODE_DOWN
Definition PKeyboard.h:176
@ PEBL_KEYCODE_KP_ENTER
Definition PKeyboard.h:183
@ PEBL_KEYCODE_LEFT
Definition PKeyboard.h:175
@ PEBL_KEYCODE_RIGHT
Definition PKeyboard.h:174
@ PEBL_KEYCODE_BACKSPACE
Definition PKeyboard.h:82
@ PEBL_KEYCODE_RETURN
Definition PKeyboard.h:80
@ PEBL_KEYCODE_UP
Definition PKeyboard.h:177
int GetRed() const
Definition PColor.cpp:226
int GetAlpha() const
Definition PColor.cpp:229
int GetBlue() const
Definition PColor.cpp:228
int GetGreen() const
Definition PColor.cpp:227
counted_ptr< PEBLObjectBase > GetObject() const
virtual bool SetProperty(std::string name, Variant v)
ComplexDataType mCDT
Definition PEBLObject.h:109
Variant GetProperty(std::string) const
virtual PColor GetBackgroundColor() const
Definition PFont.cpp:296
virtual int GetFontStyle() const
Definition PFont.h:84
virtual PColor GetFontColor() const
Definition PFont.cpp:289
virtual std::string GetFontFileName() const
Definition PFont.h:83
virtual bool GetAntiAliased() const
Definition PFont.h:88
virtual int GetFontSize() const
Definition PFont.h:85
Definition PList.h:45
void PushBack(const Variant &v)
Definition PList.cpp:149
This class is the basic generic text box.
Definition PTextBox.h:40
virtual void HandleTextInput(std::string text)
Definition PTextBox.cpp:414
virtual long unsigned int IncrementCursor()
Definition PTextBox.cpp:295
virtual void InsertText(const std::string character)
Definition PTextBox.cpp:189
virtual void SetWidth(int w)
Definition PTextBox.cpp:373
virtual long unsigned int DecrementCursor()
Definition PTextBox.cpp:334
Variant mJustify
Definition PTextBox.h:94
virtual bool AtPrintableCharacter(unsigned long int x)
Definition PTextBox.cpp:424
virtual bool SetProperty(std::string, Variant v)
Definition PTextBox.cpp:124
unsigned long int mCursorPos
Definition PTextBox.h:91
virtual void SetEditable(bool val)
Definition PTextBox.h:61
virtual void SetHeight(int h)
Definition PTextBox.cpp:367
bool mLineWrap
Definition PTextBox.h:93
virtual Variant GetProperty(std::string) const
Definition PTextBox.cpp:154
bool mCursorChanged
Definition PTextBox.h:92
bool mEditable
Definition PTextBox.h:90
virtual void DeleteText(int length)
Definition PTextBox.cpp:210
This class simply represent an abstract text-based object.
Definition PTextObject.h:41
std::string mText
Definition PTextObject.h:70
virtual void SetText(const std::string &text)
virtual pInt GetWidth() const
Definition PWidget.h:85
virtual void SetBackgroundColor(const PColor &color)
Definition PWidget.cpp:287
virtual pInt GetHeight() const
Definition PWidget.h:86
pInt mWidth
Definition PWidget.h:136
pInt mHeight
Definition PWidget.h:136
virtual PColor GetBackgroundColor()
Definition PWidget.h:92
unsigned int GetTextHeight(const std::string &text)
SDL_Surface * RenderText(const std::string &text)
This takes care of all the busy work of rendering the text.
unsigned int GetTextWidth(const std::string &text)
TTF_Font * GetTTFFont() const
unsigned int GetPosition(const std::string &text, unsigned int x)
void ClearChanged()
Clear all changed flags.
Validator platform textbox - no rendering, used only for compilation.
virtual void SetHeight(int h)
virtual counted_ptr< PEBLObjectBase > GetFont() const
virtual int FindCursorPosition(long int x, long int y)
virtual void SetFont(counted_ptr< PEBLObjectBase > font)
virtual void HandleKeyPress(int keycode, int modkeys, Uint16 unicode)
virtual void SetText(std::string text)
virtual void HandleTextInput(std::string input)
PlatformTextBox(std::string text, counted_ptr< PEBLObjectBase > font, int width, int height)
virtual void SetEditable(bool val)
virtual bool Draw()
This method initiates everything needed to display the main window
virtual Variant GetProperty(std::string name) const
virtual ~PlatformTextBox()
Standard Destructor.
virtual bool RenderText()
std::vector< int > mBreaks
virtual bool SetProperty(std::string, Variant v)
virtual std::ostream & SendToStream(std::ostream &out) const
An inheritable printing class used by PEBLObjectBase::operator<<.
virtual void SetWidth(int w)
SDL_Surface * mSurface
SDL_Texture * mTexture
virtual bool Draw()
This method initiates everything needed to display the main window
SDL_Renderer * mRenderer
pInt GetInteger() const
Definition Variant.cpp:997
PComplexData * GetComplexData() const
Definition Variant.cpp:1299
X * get() const
Definition rc_ptrs.h:110
Justification
Justification types for paragraph-level alignment.
std::vector< FormatSegment > ParseFormattedText(const std::string &input, int charWidth)
Parse formatted text into segments.
Variant SetFont(Variant v)
void strrev_utf8(char *p)
void SignalFatalError(const std::string &message)
void DrawLine(SDL_Renderer *renderer, PlatformWidget *pwidget, int x1, int y1, int x2, int y2, PColor color)
This sets a pixel to be a certain color.